From c954e09667fdd0869b3a6888f64d77a91e9e81b2 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Tue, 3 Mar 2026 18:01:00 +0100 Subject: [PATCH] S3UTILS-224: pre-seed zero-value metrics for empty buckets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When all buckets are empty (0 objects), count-items produces no bucket metric documents in __infostore. This is because getObjectMDStats() only populates dataMetrics.bucket entries when the MongoDB cursor yields objects — with 0 objects, the cursor yields nothing, _processEntryData() is never called, and dataMetrics.bucket remains {}. The flatMap in updateStorageConsumptionMetrics() then produces zero documents, and the atomic collection rename leaves __infostore with only the countitems marker. Downstream, Scuba's deep health check queries __infostore for any metric document and crashes when it finds none, causing CloudServer to mark the quota service as unavailable (ARTESCA-17063). Pre-seed dataMetrics.bucket with zero-value entries for every known bucket in CountManager.addWork(), using consolidateDataMetrics() to create the canonical zero structure. When workers return results for non-empty buckets, consolidation adds on top of these zeros. For empty buckets, the zero entries survive through to updateStorageConsumptionMetrics, ensuring every bucket gets a document in __infostore. Issue: S3UTILS-224 --- CountItems/CountManager.js | 4 +++ tests/unit/CountItems/CountManager.js | 45 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/CountItems/CountManager.js b/CountItems/CountManager.js index 34f0fc3e..e5646f22 100644 --- a/CountItems/CountManager.js +++ b/CountItems/CountManager.js @@ -180,6 +180,10 @@ class CountManager { this.store.bucketList = this.store.bucketList.concat(transformedInfos); bucketInfos.forEach(bucketInfo => { this.q.push(bucketInfo); + const key = `${bucketInfo.getName()}_${new Date(bucketInfo.getCreationDate()).getTime()}`; + if (!this.dataMetrics.bucket[key]) { + this.dataMetrics.bucket[key] = consolidateDataMetrics(null, null); + } }); this.log.debug('added work', { workInQueue: this.q.length(), diff --git a/tests/unit/CountItems/CountManager.js b/tests/unit/CountItems/CountManager.js index 53fbed41..a8e8f149 100644 --- a/tests/unit/CountItems/CountManager.js +++ b/tests/unit/CountItems/CountManager.js @@ -974,6 +974,51 @@ describe('CountItems::CountManager', () => { expect(m.q.paused).toBeTruthy(); }); + test('should pre-seed dataMetrics.bucket with zero-value entries for all buckets', () => { + const workers = createWorkers(1); + const m = new CountManager({ + log: new DummyLogger(), + workers, + maxConcurrent: 1, + }); + const bucketInfos = [ + BucketInfo.deSerialize(stringifiedBucketMD), + ]; + const bucketList = { + bucketCount: bucketInfos.length, + bucketInfos, + }; + m.addWork(bucketList); + const expectedKey = `${bucketInfos[0].getName()}_${new Date(bucketInfos[0].getCreationDate()).getTime()}`; + expect(m.dataMetrics.bucket[expectedKey]).toBeDefined(); + expect(m.dataMetrics.bucket[expectedKey].usedCapacity.current).toEqual(0n); + expect(m.dataMetrics.bucket[expectedKey].usedCapacity.nonCurrent).toEqual(0n); + expect(m.dataMetrics.bucket[expectedKey].objectCount.current).toEqual(0n); + expect(m.dataMetrics.bucket[expectedKey].objectCount.nonCurrent).toEqual(0n); + expect(m.dataMetrics.bucket[expectedKey].objectCount.deleteMarker).toEqual(0n); + }); + + test('should not overwrite existing dataMetrics.bucket entries on addWork', () => { + const workers = createWorkers(1); + const m = new CountManager({ + log: new DummyLogger(), + workers, + maxConcurrent: 1, + }); + const bucketInfo = BucketInfo.deSerialize(stringifiedBucketMD); + const key = `${bucketInfo.getName()}_${new Date(bucketInfo.getCreationDate()).getTime()}`; + m.dataMetrics.bucket[key] = { + usedCapacity: { current: 100n, nonCurrent: 50n }, + objectCount: { current: 10n, nonCurrent: 5n, deleteMarker: 0n }, + }; + m.addWork({ + bucketCount: 1, + bucketInfos: [bucketInfo], + }); + expect(m.dataMetrics.bucket[key].usedCapacity.current).toEqual(100n); + expect(m.dataMetrics.bucket[key].objectCount.current).toEqual(10n); + }); + test('should only allow queue to be started once', done => { const workers = createWorkers(1); const m = new CountManager({