Skip to content

Commit e1c5b13

Browse files
committed
Add tests for scan monitoring metrics
This commit adds unit tests for new metrics and conductorScanId generation. Updates functional tests to handle conductorScanId in messages. Normalizes conductorScanId in BackbeatTestConsumer for test comparisons (similar to reqId handling). Issue: BB-740
1 parent 2c29efc commit e1c5b13

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)