Skip to content

Commit 6442f99

Browse files
committed
Seed zero-value metrics for empty buckets in count-items
count-items only produced metric documents for buckets that hold objects. An empty bucket yielded no document in __infostore, which makes Scuba's deep health check fail and disables the quota service cluster-wide (ARTESCA-17063). Seed zero-value bucket, account and location entries in getObjectMDStats() when a bucket scan yields no objects. The bucket key is derived from the __usersbucket creation date, exactly like populated buckets (S3UTILS-131), so the seeded document matches the key quota lookups use; if that date is missing the bucket is skipped with a warning rather than written under an unmatchable key. Issue: S3UTILS-224
1 parent 128b8c7 commit 6442f99

3 files changed

Lines changed: 175 additions & 5 deletions

File tree

tests/functional/countItems.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const USERSBUCKET = '__usersbucket';
2020
const expectedCountItems = {
2121
objects: 90,
2222
versions: 60,
23-
buckets: 9,
23+
buckets: 10,
2424
dataManaged: {
2525
total: { curr: 15000, prev: 12000 },
2626
byLocation: {
@@ -100,6 +100,10 @@ const expectedDataMetrics = {
100100
objectCount: { current: 10n, deleteMarker: 0n, nonCurrent: 10n },
101101
usedCapacity: { current: 1000n, nonCurrent: 1000n },
102102
},
103+
[`bucket_test-bucket-empty_${testBucketCreationDate}`]: {
104+
objectCount: { current: 0n, deleteMarker: 0n, nonCurrent: 0n },
105+
usedCapacity: { current: 0n, nonCurrent: 0n },
106+
},
103107
'location_secondary-location-1': {
104108
objectCount: { current: 30n, deleteMarker: 0n, nonCurrent: 30n }, usedCapacity: { current: 3000n, nonCurrent: 3000n },
105109
},
@@ -199,6 +203,25 @@ function populateMongo(client, callback) {
199203
], cb), callback);
200204
}
201205

206+
const emptyBucket = BucketInfo.fromObj({ ...testBucketMD, _name: 'test-bucket-empty' });
207+
208+
function populateEmptyBucket(client, callback) {
209+
return async.series([
210+
next => client.createBucket(emptyBucket.getName(), emptyBucket, logger, next),
211+
next => client.putObject(
212+
USERSBUCKET,
213+
`${emptyBucket.getOwner()}${constants.splitter}${emptyBucket.getName()}`,
214+
testUserBucketInfo.value,
215+
{
216+
versioning: false,
217+
versionId: null,
218+
},
219+
logger,
220+
next,
221+
),
222+
], callback);
223+
}
224+
202225
jest.setTimeout(120000);
203226
describe('CountItems', () => {
204227
const oldEnv = process.env;
@@ -222,6 +245,7 @@ describe('CountItems', () => {
222245
async.series([
223246
next => client.setup(next),
224247
next => populateMongo(client, next),
248+
next => populateEmptyBucket(client, next),
225249
], done);
226250
setTimeoutSpy = jest.spyOn(global, 'setTimeout');
227251
setTimeoutSpy.mockImplementation((callback, delay) => callback());

tests/unit/utils/S3UtilsMongoClient.js

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,7 +1152,7 @@ describe('S3UtilsMongoClient, tests', () => {
11521152

11531153
const tests = [
11541154
[
1155-
'getObjectMDStats() should return zero-result when no objects in the bucket',
1155+
'getObjectMDStats() should seed zero-value metrics for an empty bucket, account and location',
11561156
{
11571157
bucketName: 'test-bucket',
11581158
isVersioned: false,
@@ -1167,9 +1167,89 @@ describe('S3UtilsMongoClient, tests', () => {
11671167
stalled: 0,
11681168
versions: 0,
11691169
dataMetrics: {
1170-
bucket: {},
1171-
location: {},
1172-
account: {},
1170+
bucket: {
1171+
[`test-bucket_${testBucketCreationDate}`]: {
1172+
usedCapacity: {
1173+
current: 0n,
1174+
nonCurrent: 0n,
1175+
_currentCold: 0n,
1176+
_nonCurrentCold: 0n,
1177+
_currentRestored: 0n,
1178+
_currentRestoring: 0n,
1179+
_nonCurrentRestored: 0n,
1180+
_nonCurrentRestoring: 0n,
1181+
_incompleteMPUParts: 0n,
1182+
},
1183+
objectCount: {
1184+
current: 0n,
1185+
nonCurrent: 0n,
1186+
_currentCold: 0n,
1187+
_nonCurrentCold: 0n,
1188+
_currentRestored: 0n,
1189+
_currentRestoring: 0n,
1190+
_nonCurrentRestored: 0n,
1191+
_nonCurrentRestoring: 0n,
1192+
_incompleteMPUUploads: 0n,
1193+
deleteMarker: 0n,
1194+
},
1195+
locations: {},
1196+
},
1197+
},
1198+
location: {
1199+
'us-east-1': {
1200+
usedCapacity: {
1201+
current: 0n,
1202+
nonCurrent: 0n,
1203+
_currentCold: 0n,
1204+
_nonCurrentCold: 0n,
1205+
_currentRestored: 0n,
1206+
_currentRestoring: 0n,
1207+
_nonCurrentRestored: 0n,
1208+
_nonCurrentRestoring: 0n,
1209+
_incompleteMPUParts: 0n,
1210+
},
1211+
objectCount: {
1212+
current: 0n,
1213+
nonCurrent: 0n,
1214+
_currentCold: 0n,
1215+
_nonCurrentCold: 0n,
1216+
_currentRestored: 0n,
1217+
_currentRestoring: 0n,
1218+
_nonCurrentRestored: 0n,
1219+
_nonCurrentRestoring: 0n,
1220+
_incompleteMPUUploads: 0n,
1221+
deleteMarker: 0n,
1222+
},
1223+
},
1224+
},
1225+
account: {
1226+
[testAccountCanonicalId]: {
1227+
usedCapacity: {
1228+
current: 0n,
1229+
nonCurrent: 0n,
1230+
_currentCold: 0n,
1231+
_nonCurrentCold: 0n,
1232+
_currentRestored: 0n,
1233+
_currentRestoring: 0n,
1234+
_nonCurrentRestored: 0n,
1235+
_nonCurrentRestoring: 0n,
1236+
_incompleteMPUParts: 0n,
1237+
},
1238+
objectCount: {
1239+
current: 0n,
1240+
nonCurrent: 0n,
1241+
_currentCold: 0n,
1242+
_nonCurrentCold: 0n,
1243+
_currentRestored: 0n,
1244+
_currentRestoring: 0n,
1245+
_nonCurrentRestored: 0n,
1246+
_nonCurrentRestoring: 0n,
1247+
_incompleteMPUUploads: 0n,
1248+
deleteMarker: 0n,
1249+
},
1250+
locations: {},
1251+
},
1252+
},
11731253
},
11741254
},
11751255
],

utils/S3UtilsMongoClient.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,39 @@ const baseMetricsObject = {
5252
mpuPartsData: 0n,
5353
};
5454

55+
// Zero-value entry matching the shape _handleResults produces, to seed empty resources.
56+
function zeroDataMetrics(withLocations) {
57+
const entry = {
58+
usedCapacity: {
59+
current: 0n,
60+
nonCurrent: 0n,
61+
_currentCold: 0n,
62+
_nonCurrentCold: 0n,
63+
_currentRestored: 0n,
64+
_currentRestoring: 0n,
65+
_nonCurrentRestored: 0n,
66+
_nonCurrentRestoring: 0n,
67+
_incompleteMPUParts: 0n,
68+
},
69+
objectCount: {
70+
current: 0n,
71+
nonCurrent: 0n,
72+
_currentCold: 0n,
73+
_nonCurrentCold: 0n,
74+
_currentRestored: 0n,
75+
_currentRestoring: 0n,
76+
_nonCurrentRestored: 0n,
77+
_nonCurrentRestoring: 0n,
78+
_incompleteMPUUploads: 0n,
79+
deleteMarker: 0n,
80+
},
81+
};
82+
if (withLocations) {
83+
entry.locations = {};
84+
}
85+
return entry;
86+
}
87+
5588
class S3UtilsMongoClient extends MongoClientInterface {
5689
async updateInflightDeltas(allMetrics, log) {
5790
let cursor;
@@ -403,6 +436,39 @@ class S3UtilsMongoClient extends MongoClientInterface {
403436
const retResult = this._handleResults(collRes, isVer);
404437
retResult.stalled = stalledCount;
405438

439+
// Empty buckets produce no metric document, which disables quotas
440+
// cluster-wide (ARTESCA-17063); seed zeros so every resource has one.
441+
// Key off the __usersbucket creation date, like populated buckets (S3UTILS-131).
442+
if (bucketCreationDate) {
443+
const bucketResource = `${bucketName}_${new Date(bucketCreationDate).getTime()}`;
444+
if (!retResult.dataMetrics.bucket[bucketResource]) {
445+
retResult.dataMetrics.bucket[bucketResource] = zeroDataMetrics(true);
446+
const owner = bucketInfo.getOwner();
447+
if (owner) {
448+
if (!retResult.dataMetrics.account[owner]) {
449+
retResult.dataMetrics.account[owner] = zeroDataMetrics(true);
450+
}
451+
// so the inflight block below tags it with the owner
452+
if (!accountBucket) {
453+
accountBucket = owner;
454+
}
455+
}
456+
const bucketLocation = bucketInfo.getLocationConstraint();
457+
if (bucketLocation && locationConfig && locationConfig[bucketLocation]) {
458+
const locationResource = this._getLocName(locationConfig[bucketLocation].objectId);
459+
if (!retResult.dataMetrics.location[locationResource]) {
460+
retResult.dataMetrics.location[locationResource] = zeroDataMetrics(false);
461+
}
462+
}
463+
}
464+
} else {
465+
log.warn('unable to seed zero metrics for empty bucket: no creation date in __usersbucket', {
466+
method: 'getObjectMDStats',
467+
bucketName,
468+
owner: bucketInfo.getOwner(),
469+
});
470+
}
471+
406472
if (inflightsPreScan > 0 && retResult && retResult.dataMetrics) {
407473
Object.keys(retResult.dataMetrics.bucket).forEach(key => {
408474
retResult.dataMetrics.bucket[key].usedCapacity = {

0 commit comments

Comments
 (0)