diff --git a/tests/unit/handlers/metrics.handlers.test.js b/tests/unit/handlers/metrics.handlers.test.js index 4252943..568583c 100644 --- a/tests/unit/handlers/metrics.handlers.test.js +++ b/tests/unit/handlers/metrics.handlers.test.js @@ -13,6 +13,7 @@ const { calculateGroupedEfficiencySummary, getMinerStatus, getMinersByContainer, + getMinerCountsByContainer, getInventorySummary, processMinerStatusData, processGroupedMinerStatusData, @@ -2133,6 +2134,30 @@ test('getMinersByContainer - no data returns empty container map', async (t) => t.pass() }) +test('getMinerCountsByContainer - fetches only count fields and sums them per container', async (t) => { + let payload = null + const mockCtx = withDataProxy({ + conf: { orks: [{ rpcPublicKey: 'a' }, { rpcPublicKey: 'b' }] }, + net_r0: { + jRequest: async (key, method, p) => { + payload = p + return key === 'a' + ? [[{ ts: 1, offline_cnt: { c1: 3 }, power_mode_normal_cnt: { c1: 10, c2: 4 } }]] + : [[{ ts: 1, error_cnt: { c1: 1 } }]] + } + } + }) + + const result = await getMinerCountsByContainer(mockCtx) + + t.is(payload.keys[0].key, 'stat-rtd', 'should read the realtime snapshot') + t.absent(payload.aggrFields.hashrate_mhs_5m_container_group_sum_aggr, 'should not request hashrate') + t.absent(payload.aggrFields.temperature_c_group_max_aggr, 'should not request temperature') + t.absent(payload.aggrFields.hashrate_mhs_5m_active_container_group_cnt, 'should not request active count') + t.alike(result.containers, { c1: { minerCount: 14 }, c2: { minerCount: 4 } }, 'should sum status counts across orks per container') + t.pass() +}) + // ==================== Inventory Summary Tests ==================== test('getInventorySummary - rolls up miner and spare-part counts by status/location', async (t) => { @@ -3831,7 +3856,9 @@ test('getInventoryMinerDistribution - builds per-type rows with locations and ca [{ miner_inventory_location_group_cnt_aggr: { 'site.warehouse': 1 } }] ] } - if (method === 'tailLogMulti') return [[{}]] + if (method === 'tailLogMulti') { + return [[{ offline_cnt: { c1: 1 }, power_mode_normal_cnt: { c1: 1 } }]] + } return [] } } @@ -3846,6 +3873,22 @@ test('getInventoryMinerDistribution - builds per-type rows with locations and ca { 'info.site': { $eq: 'site-a' } }, 'should scope miners to the configured site' ) + t.absent(minerListCall.payload.status, 'should not request snap data for miners') + + const containerCountCall = calls.find(c => c.method === 'tailLogMulti' && !c.payload.aggrFields.miner_inventory_location_group_cnt_aggr) + t.alike( + Object.keys(containerCountCall.payload.aggrFields).sort(), + [ + 'error_cnt', + 'not_mining_cnt', + 'offline_cnt', + 'power_mode_high_cnt', + 'power_mode_low_cnt', + 'power_mode_normal_cnt', + 'power_mode_sleep_cnt' + ], + 'should only request per-container count fields' + ) const locationCall = calls.find(c => c.method === 'tailLogMulti' && c.payload.aggrFields.miner_inventory_location_group_cnt_aggr) t.alike( @@ -3859,6 +3902,7 @@ test('getInventoryMinerDistribution - builds per-type rows with locations and ca t.is(s19Row.locations['miner.room'], 1, 'should report aggregated locations') t.is(s19Row.locations.unknown, 1, 'unknown = count minus located miners') t.is(s19Row.totalPositions, 3, 'capacity from container miner tag') + t.is(s19Row.freePositions, 1, 'free = capacity minus connected miner counts') const m53Row = result.rows.find(r => r.type === 'miner-wm-m53s') t.is(m53Row.locations.unknown, 0, 'located miners leave no unknowns') diff --git a/workers/lib/server/handlers/metrics.handlers.js b/workers/lib/server/handlers/metrics.handlers.js index 7ffb545..3611bfe 100644 --- a/workers/lib/server/handlers/metrics.handlers.js +++ b/workers/lib/server/handlers/metrics.handlers.js @@ -1060,6 +1060,39 @@ function processMinersByType (results) { return { types } } +const CONTAINER_MINER_COUNT_AGGR_FIELDS = { + [AGGR_FIELDS.OFFLINE_CNT]: 1, + [AGGR_FIELDS.ERROR_CNT]: 1, + [AGGR_FIELDS.NOT_MINING_CNT]: 1, + [AGGR_FIELDS.SLEEP_CNT]: 1, + [AGGR_FIELDS.POWER_MODE_LOW_CNT]: 1, + [AGGR_FIELDS.POWER_MODE_NORMAL_CNT]: 1, + [AGGR_FIELDS.POWER_MODE_HIGH_CNT]: 1 +} + +async function getMinerCountsByContainer (ctx) { + const results = await ctx.dataProxy.requestDataMap(RPC_METHODS.TAIL_LOG_MULTI, { + keys: [{ key: LOG_KEYS.STAT_RTD, type: WORKER_TYPES.MINER, tag: WORKER_TAGS.MINER }], + limit: 1, + aggrFields: CONTAINER_MINER_COUNT_AGGR_FIELDS + }) + + const counts = {} + for (const orkResult of results) { + const entry = extractKeyEntry(orkResult, 0) + if (!entry) continue + for (const field of Object.keys(CONTAINER_MINER_COUNT_AGGR_FIELDS)) { + mergeGroupedField(counts, entry[field]) + } + } + + const containers = {} + for (const [id, minerCount] of Object.entries(counts)) { + containers[id] = { minerCount } + } + return { containers } +} + const CONTAINER_MINER_TAG_REGEX = /container_miner-[^_]+_(.+)/ function computeInstalledCapacity (containers, byContainer) { @@ -1092,15 +1125,13 @@ async function getInventoryMinerDistribution (ctx, req) { const [minerResults, containerResults, byContainer] = await Promise.all([ ctx.dataProxy.requestDataAllPages(RPC_METHODS.LIST_THINGS, { query: minersQuery, - fields: { id: 1, type: 1 }, - status: 1 + fields: { id: 1, type: 1 } }), ctx.dataProxy.requestDataAllPages(RPC_METHODS.LIST_THINGS, { query: { tags: { $in: [WORKER_TAGS.CONTAINER] } }, - fields: { id: 1, tags: 1, 'info.container': 1, 'info.nominalMinerCapacity': 1 }, - status: 1 + fields: { id: 1, tags: 1, 'info.container': 1, 'info.nominalMinerCapacity': 1 } }), - getMinersByContainer(ctx, req) + getMinerCountsByContainer(ctx) ]) const miners = flattenRpcResults(minerResults) @@ -1754,6 +1785,7 @@ module.exports = { processGroupedMinerStatusData, getMinersByContainer, processMinersByContainer, + getMinerCountsByContainer, getInventorySummary, processInventorySummary, getMinersByType,