Skip to content

Commit 488ff86

Browse files
committed
Add unit tests for scan monitoring metrics
Issue: BB-740
1 parent 3b78c2d commit 488ff86

File tree

4 files changed

+170
-6
lines changed

4 files changed

+170
-6
lines changed

tests/functional/lifecycle/LifecycleConductor.spec.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ const expected2Messages = (version='v2') => ([
4141
{
4242
value: {
4343
action: 'processObjects',
44-
contextInfo: { reqId: 'test-request-id' },
44+
contextInfo: { reqId: 'test-request-id', conductorScanId: 'test-scan-id' },
4545
target: { bucket: 'bucket1', owner: 'owner1', taskVersion: version },
4646
details: {},
4747
},
4848
},
4949
{
5050
value: {
5151
action: 'processObjects',
52-
contextInfo: { reqId: 'test-request-id' },
52+
contextInfo: { reqId: 'test-request-id', conductorScanId: 'test-scan-id' },
5353
target: { bucket: 'bucket1-2', owner: 'owner1', taskVersion: version },
5454
details: {},
5555
},
@@ -60,31 +60,31 @@ const expected4Messages = (version='v2') => ([
6060
{
6161
value: {
6262
action: 'processObjects',
63-
contextInfo: { reqId: 'test-request-id' },
63+
contextInfo: { reqId: 'test-request-id', conductorScanId: 'test-scan-id' },
6464
target: { bucket: 'bucket1', owner: 'owner1', taskVersion: version },
6565
details: {},
6666
},
6767
},
6868
{
6969
value: {
7070
action: 'processObjects',
71-
contextInfo: { reqId: 'test-request-id' },
71+
contextInfo: { reqId: 'test-request-id', conductorScanId: 'test-scan-id' },
7272
target: { bucket: 'bucket1-2', owner: 'owner1', taskVersion: version },
7373
details: {},
7474
},
7575
},
7676
{
7777
value: {
7878
action: 'processObjects',
79-
contextInfo: { reqId: 'test-request-id' },
79+
contextInfo: { reqId: 'test-request-id', conductorScanId: 'test-scan-id' },
8080
target: { bucket: 'bucket3', owner: 'owner3', taskVersion: version },
8181
details: {},
8282
},
8383
},
8484
{
8585
value: {
8686
action: 'processObjects',
87-
contextInfo: { reqId: 'test-request-id' },
87+
contextInfo: { reqId: 'test-request-id', conductorScanId: 'test-scan-id' },
8888
target: { bucket: 'bucket4', owner: 'owner4', taskVersion: version },
8989
details: {},
9090
},

tests/unit/lifecycle/LifecycleConductor.spec.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const { splitter } = require('arsenal').constants;
77

88
const LifecycleConductor = require(
99
'../../../extensions/lifecycle/conductor/LifecycleConductor');
10+
const { LifecycleMetrics } = require('../../../extensions/lifecycle/LifecycleMetrics');
1011
const {
1112
lifecycleTaskVersions,
1213
indexesForFeature
@@ -190,6 +191,64 @@ describe('Lifecycle Conductor', () => {
190191
conductor.processBuckets(done);
191192
});
192193

194+
it('should publish full scan metrics at end of scan', done => {
195+
conductor._mongodbClient = {
196+
getIndexingJobs: (_, cb) => cb(null, ['job1']),
197+
getCollection: () => ({
198+
find: () => ({
199+
project: () => ({
200+
hasNext: () => Promise.resolve(false),
201+
}),
202+
}),
203+
}),
204+
};
205+
conductor._zkClient = {
206+
getData: (_, cb) => cb(null, null, null),
207+
setData: (path, data, version, cb) => cb(null, { version: 1 }),
208+
};
209+
210+
sinon.stub(conductor, '_controlBacklog').callsFake(cb => cb(null));
211+
const metricStub = sinon.stub(LifecycleMetrics, 'onConductorFullScan');
212+
213+
conductor.processBuckets(err => {
214+
assert.ifError(err);
215+
assert(metricStub.calledOnce);
216+
const [, , bucketCount, workflowCount, lifecycleBucketCount] =
217+
metricStub.firstCall.args;
218+
assert.strictEqual(bucketCount, 0);
219+
assert.strictEqual(workflowCount, 0);
220+
assert.strictEqual(lifecycleBucketCount, 0);
221+
done();
222+
});
223+
});
224+
225+
it('should generate a conductorScanId', done => {
226+
conductor._mongodbClient = {
227+
getIndexingJobs: (_, cb) => cb(null, []),
228+
getCollection: () => ({
229+
find: () => ({
230+
project: () => ({
231+
hasNext: () => Promise.resolve(false),
232+
}),
233+
}),
234+
}),
235+
};
236+
conductor._zkClient = {
237+
getData: (_, cb) => cb(null, null, null),
238+
setData: (path, data, version, cb) => cb(null, { version: 1 }),
239+
};
240+
241+
sinon.stub(conductor, '_controlBacklog').callsFake(cb => cb(null));
242+
243+
conductor.processBuckets(err => {
244+
assert.ifError(err);
245+
assert(conductor._scanId);
246+
assert(typeof conductor._scanId === 'string');
247+
assert(conductor._scanId.length > 0);
248+
done();
249+
});
250+
});
251+
193252
// tests that `activeIndexingJobRetrieved` is not reset until the e
194253
it('should not reset `activeIndexingJobsRetrieved` while async operations are in progress', done => {
195254
const order = [];
@@ -244,6 +303,14 @@ describe('Lifecycle Conductor', () => {
244303
});
245304

246305
describe('_indexesGetOrCreate', () => {
306+
it('should include conductor scan id in task context', () => {
307+
conductor._scanId = 'scan-id-test';
308+
309+
const taskMessage = conductor._taskToMessage(getTask(true), lifecycleTaskVersions.v2, log);
310+
const parsed = JSON.parse(taskMessage.message);
311+
assert.strictEqual(parsed.contextInfo.conductorScanId, 'scan-id-test');
312+
});
313+
247314
it('should return v2 for bucketd bucket sources', done => {
248315
conductor._bucketSource = 'bucketd';
249316
conductor._indexesGetOrCreate(getTask(undefined), log, (err, taskVersion) => {

tests/unit/lifecycle/LifecycleMetrics.spec.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,72 @@ describe('LifecycleMetrics', () => {
7979
}));
8080
});
8181

82+
it('should catch errors in onConductorFullScan', () => {
83+
const metric = ZenkoMetrics.getMetric('s3_lifecycle_conductor_full_scan_elapsed_seconds');
84+
sinon.stub(metric, 'set').throws(new Error('Metric error'));
85+
86+
LifecycleMetrics.onConductorFullScan(log, 5000, 10, 8, 5);
87+
88+
assert(log.error.calledOnce);
89+
assert(log.error.calledWithMatch('failed to update prometheus metrics', {
90+
method: 'LifecycleMetrics.onConductorFullScan',
91+
elapsedMs: 5000,
92+
bucketCount: 10,
93+
workflowCount: 8,
94+
lifecycleBucketCount: 5,
95+
}));
96+
});
97+
98+
it('should set and reset bucket processor scan gauges', () => {
99+
const scanStartMetric = ZenkoMetrics.getMetric(
100+
's3_lifecycle_bucket_processor_scan_start_time');
101+
const bucketsMetric = ZenkoMetrics.getMetric(
102+
's3_lifecycle_bucket_processor_buckets_count');
103+
const startSet = sinon.stub(scanStartMetric, 'set');
104+
const bucketsSet = sinon.stub(bucketsMetric, 'set');
105+
const bucketsInc = sinon.stub(bucketsMetric, 'inc');
106+
107+
// start a scan
108+
LifecycleMetrics.onBucketProcessorScanStart(
109+
log, 1706000000000);
110+
assert(startSet.calledOnce);
111+
assert(startSet.calledWithMatch(
112+
{ origin: 'bucket_processor' }, 1706000000000));
113+
assert(bucketsSet.calledOnce);
114+
assert(bucketsSet.calledWithMatch(
115+
{ origin: 'bucket_processor' }, 0));
116+
117+
// process a bucket
118+
LifecycleMetrics.onBucketProcessorBucketDone(log);
119+
assert(bucketsInc.calledOnce);
120+
121+
// end the scan
122+
startSet.resetHistory();
123+
LifecycleMetrics.onBucketProcessorScanEnd(log);
124+
assert(startSet.calledOnce);
125+
assert(startSet.calledWithMatch(
126+
{ origin: 'bucket_processor' }, 0));
127+
128+
assert(log.error.notCalled);
129+
});
130+
131+
it('should catch errors in onBucketProcessorScanStart', () => {
132+
const scanStartMetric = ZenkoMetrics.getMetric(
133+
's3_lifecycle_bucket_processor_scan_start_time');
134+
sinon.stub(scanStartMetric, 'set')
135+
.throws(new Error('Metric error'));
136+
137+
LifecycleMetrics.onBucketProcessorScanStart(
138+
log, 1706000000000);
139+
140+
assert(log.error.calledOnce);
141+
assert(log.error.calledWithMatch(
142+
'failed to update prometheus metrics', {
143+
method:
144+
'LifecycleMetrics.onBucketProcessorScanStart',
145+
}));
146+
});
147+
82148
it('should catch errors in onLifecycleTriggered', () => {
83149
LifecycleMetrics.onLifecycleTriggered(log, 'conductor', 'expiration', 'us-east-1', NaN);
84150

@@ -169,5 +235,28 @@ describe('LifecycleMetrics', () => {
169235
process: 'conductor',
170236
}));
171237
});
238+
239+
it('should set full scan metrics including lifecycle bucket count', () => {
240+
const elapsedMetric = ZenkoMetrics.getMetric(
241+
's3_lifecycle_conductor_full_scan_elapsed_seconds');
242+
const countMetric = ZenkoMetrics.getMetric(
243+
's3_lifecycle_conductor_scan_count');
244+
const elapsedSet = sinon.stub(elapsedMetric, 'set');
245+
const countSet = sinon.stub(countMetric, 'set');
246+
247+
LifecycleMetrics.onConductorFullScan(log, 3000, 7, 6, 4);
248+
249+
assert(elapsedSet.calledOnce);
250+
assert(elapsedSet.calledWithMatch(
251+
{ origin: 'conductor' }, 3));
252+
assert.strictEqual(countSet.callCount, 3);
253+
assert(countSet.getCall(0).calledWithMatch(
254+
{ origin: 'conductor', type: 'bucket' }, 7));
255+
assert(countSet.getCall(1).calledWithMatch(
256+
{ origin: 'conductor', type: 'lifecycle_bucket' }, 4));
257+
assert(countSet.getCall(2).calledWithMatch(
258+
{ origin: 'conductor', type: 'workflow' }, 6));
259+
assert(log.error.notCalled);
260+
});
172261
});
173262
});

tests/utils/BackbeatTestConsumer.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ class BackbeatTestConsumer extends BackbeatConsumer {
3232
// present
3333
assert(parsedMsg.contextInfo?.reqId, 'expected contextInfo.reqId field');
3434
parsedMsg.contextInfo.reqId = expectedMsg.value.contextInfo?.reqId;
35+
// conductorScanId is also generated per scan: check it exists,
36+
// then normalize for comparison
37+
if (expectedMsg.value.contextInfo?.conductorScanId === 'test-scan-id') {
38+
assert(parsedMsg.contextInfo?.conductorScanId,
39+
'expected contextInfo.conductorScanId field');
40+
parsedMsg.contextInfo.conductorScanId =
41+
expectedMsg.value.contextInfo.conductorScanId;
42+
}
3543
}
3644
assert.deepStrictEqual(
3745
parsedMsg, expectedMsg.value,

0 commit comments

Comments
 (0)