local fun = require('fun') local json = require('json') -- I believe that we won't maintain clusters over 1k replicasets -- in the nearest future local SELECT_LIMIT = 1000 local VERSION_PLACEHOLDER = '??.??' local DEFAULT_FUTURE_TIMEOUT = 60 local INSTANCE_GRADE = { ONLINE = 'Online', OffLINE = 'Offline', } local function table_len(table) local i = 0 for _ in pairs(table) do i = i + 1 end return i end local function get_instances() local tuples = box.space._pico_instance:select({}, { limit = SELECT_LIMIT }) return fun.iter(tuples) :map(function(tuple) return tuple:tomap({ names_only = true }) end) :totable() end local function get_replicasets() local tuples = box.space._pico_replicaset:select({}, { limit = SELECT_LIMIT }) return fun.iter(tuples) :map(function(tuple) return tuple:tomap({ names_only = true }) end) :totable() end -- returns leader's box.slab.info local function get_memory_info() local replicasets = vshard.router.routeall() -- we only need to watch for leader's capacity local leaders = fun.iter(replicasets) :reduce(function(acc, uuid, replicaset) acc[uuid] = replicaset.master return acc end,{}) local slab_info_futures = fun.iter(leaders) :reduce(function(acc, uuid, instance) local future = instance.conn:call('box.slab.info', {}, { is_async = true }) acc[uuid] = future return acc end, {}) local results = fun.iter(slab_info_futures) :reduce(function(acc, uuid, future) acc[uuid] = future:wait_result(DEFAULT_FUTURE_TIMEOUT)[1] return acc end, {}) return results end local function is_leader(instance, replicasets) local replicaset = fun.iter(replicasets) :filter(function(replicaset) return replicaset.replicaset_uuid == instance.replicaset_uuid end) :totable()[1] return instance.instance_id == replicaset.master_id end local function cast_instance(instance, is_leader) return { name = instance.instance_id, targetGrade = instance.target_grade[1], currentGrade = instance.current_grade[1], failureDomain = instance.failure_domain, version = VERSION_PLACEHOLDER, isLeader = is_leader, } end -- the expected response: -- -------------------------------------------------- -- export interface ReplicasetType { -- id: string; -- instanceCount: number; -- instances: InstanceType[]; -- version: string; -- grade: string; -- capacity: string; -- } -- export interface InstanceType { -- name: string; -- targetGrade: string; -- currentGrade: string; -- failureDomain: string; -- version: string; -- isLeader: boolean; -- } -- -------------------------------------------------- local function list_replicasets() local replicasets = get_replicasets() local instances = get_instances() local memory_info = get_memory_info() local replicaset_table = fun.iter(instances) :reduce(function(acc, instance) local is_leader = is_leader(instance, replicasets) local instance_memory_info = memory_info[instance.replicaset_uuid] if acc[instance.replicaset_uuid] == nil then acc[instance.replicaset_uuid] = { id = instance.replicaset_id, uuid = instance.replicaset_uuid, instanceCount = 1, instances = { cast_instance(instance, is_leader) }, version = VERSION_PLACEHOLDER, grade = is_leader and instance.current_grade[1], -- frontend expects number capacityUsage = is_leader and instance_memory_info.quota_used / instance_memory_info.quota_size * 100, memory = is_leader and { used = instance_memory_info.quota_used, usable = instance_memory_info.quota_size, }, } else local replicaset = acc[instance.replicaset_uuid] table.insert(replicaset.instances, cast_instance(instance)) replicaset.instanceCount = replicaset.instanceCount + 1 if is_leader then replicaset.grade = instance.current_grade[1] replicaset.capacity = instance_memory_info.quota_used / instance_memory_info.quota_size * 100 end end return acc end, {}) local replicasets = fun.iter(replicaset_table) :map(function(_, replicaset) return replicaset end) :totable() return { status = 200, body = json.encode(replicasets), headers = { ['content-type'] = 'application/json' } } end -- the expected response: -- -------------------------------------------------- -- export interface ClusterInfoType { -- capacityUsage: string; -- memory: { -- used: string; -- usable: string; -- }; -- replicasetsCount: number; -- instancesCurrentGradeOnline: number; -- instancesCurrentGradeOffline: number; -- currentInstaceVersion: string; -- } -- -------------------------------------------------- local function cluster_info() local replicasets = get_replicasets() local replicasetsCount = table_len(replicasets) local instances = get_instances() local instancesOnlineIter, instancesOfflineIter = fun.iter(instances) :partition(function(instance) return instance.current_grade[1] == INSTANCE_GRADE.ONLINE end) local memory_info = get_memory_info() -- '#' operator does not work for map-like tables local memory_info_count = fun.iter(memory_info):length() local avg_capacity = fun.iter(memory_info) :map(function(_, slab_info) -- i believe that calculating quota_used_ratio one more time is -- easier than parsing it from '54.23%' string return slab_info.quota_used / slab_info.quota_size end) :sum() / memory_info_count * 100 local total_used = fun.iter(memory_info) :map(function(_, slab_info) return slab_info.quota_used end) :sum() local total_size = fun.iter(memory_info) :map(function(_, slab_info) return slab_info.quota_size end) :sum() local resp = { capacityUsage = avg_capacity, memory = { used = total_used, usable = total_size, }, replicasetsCount = replicasetsCount, instancesCurrentGradeOnline = instancesOnlineIter:length(), instancesCurrentGradeOffline = instancesOfflineIter:length(), currentInstaceVersion = pico.PICODATA_VERSION, } return { status = 200, body = json.encode(resp), headers = { ['content-type'] = 'application/json' } } end local host, port = ...; local httpd = require('http.server').new(host, port); httpd:route({ method = 'GET', path = 'api/v1/replicaset' }, list_replicasets) httpd:route({ method = 'GET', path = 'api/v1/cluster' }, cluster_info) httpd:start(); _G.pico.httpd = httpd