Skip to content

Commit a35d2be

Browse files
Release the credentials sweep timer held by ClientManager
The interval sweeping inactive credentials was started and never stopped: it kept the event loop alive, so a process could not exit on its own, and it kept the whole ClientManager reachable along with its keepAlive agents and client caches. The lifecycle processors meant to clear it, but did so on themselves rather than on the ClientManager, which never had anything to clear it with. Give the class a close() and call it from the services owning a manager, once their consumers and producers are done with the clients. Issue: BB-877
1 parent 322f768 commit a35d2be

12 files changed

Lines changed: 227 additions & 17 deletions

File tree

extensions/gc/GarbageCollector.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,12 @@ class GarbageCollector extends EventEmitter {
184184
close(cb) {
185185
this._logger.debug('closing garbage collector consumer');
186186
if (this._consumer) {
187-
this._consumer.close(cb);
187+
this._consumer.close(err => {
188+
this.clientManager.close();
189+
cb(err);
190+
});
188191
} else {
192+
this.clientManager.close();
189193
cb();
190194
}
191195
}

extensions/lifecycle/bucketProcessor/LifecycleBucketProcessor.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -529,10 +529,6 @@ class LifecycleBucketProcessor {
529529
* @return {undefined}
530530
*/
531531
close(cb) {
532-
if (this._deleteInactiveCredentialsInterval) {
533-
clearInterval(this._deleteInactiveCredentialsInterval);
534-
}
535-
536532
async.parallel([
537533
done => {
538534
this._log.debug('closing bucket tasks consumer');
@@ -542,7 +538,10 @@ class LifecycleBucketProcessor {
542538
this._log.debug('closing producer');
543539
this._producer.close(done);
544540
},
545-
], () => cb());
541+
], () => {
542+
this.clientManager.close();
543+
cb();
544+
});
546545
}
547546

548547
isReady() {

extensions/lifecycle/conductor/LifecycleConductor.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,7 @@ class LifecycleConductor {
12051205
});
12061206
},
12071207
], err => {
1208+
this.clientManager.close();
12081209
this._circuitBreaker.stop();
12091210
this._started = false;
12101211
return done(err);

extensions/lifecycle/objectProcessor/LifecycleObjectProcessor.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,13 @@ class LifecycleObjectProcessor extends EventEmitter {
142142
close(cb) {
143143
this._log.debug('closing object tasks consumer');
144144

145-
if (this._deleteInactiveCredentialsInterval) {
146-
clearInterval(this._deleteInactiveCredentialsInterval);
147-
}
148-
149145
if (this._consumers) {
150-
this._consumers.close(cb);
146+
this._consumers.close(err => {
147+
this.clientManager.close();
148+
cb(err);
149+
});
151150
} else {
151+
this.clientManager.close();
152152
cb();
153153
}
154154
}

extensions/replication/queueProcessor/QueueProcessor.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,14 @@ class QueueProcessor extends EventEmitter {
865865
});
866866
return next();
867867
},
868-
], done);
868+
], err => {
869+
// the tasks hold a reference to this map, so empty it in place
870+
Object.keys(this.sourceClientManagers).forEach(key => {
871+
this.sourceClientManagers[key].close();
872+
delete this.sourceClientManagers[key];
873+
});
874+
return done(err);
875+
});
869876
}
870877

871878
/**

lib/clients/ClientManager.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class ClientManager {
3838
}
3939

4040
this._stsConfig = null;
41+
this._deleteInactiveCredentialsInterval = null;
4142
this.s3Clients = {};
4243
this.backbeatClients = {};
4344
this.credentialsManager = new CredentialsManager(this._id, this._log);
@@ -71,6 +72,21 @@ class ClientManager {
7172
this._deleteInactiveCredentialsInterval = setInterval(() => {
7273
this.credentialsManager.removeInactiveCredentials(MAX_INACTIVE_DURATION);
7374
}, DELETE_INACTIVE_CREDENTIALS_INTERVAL);
75+
// housekeeping only: it must not keep the process alive on its own
76+
this._deleteInactiveCredentialsInterval.unref();
77+
}
78+
79+
/**
80+
* Release the resources held by this client manager. Safe to call
81+
* whether or not initCredentialsManager() was called.
82+
* @return {undefined}
83+
*/
84+
close() {
85+
if (this._deleteInactiveCredentialsInterval) {
86+
clearInterval(this._deleteInactiveCredentialsInterval);
87+
this._deleteInactiveCredentialsInterval = null;
88+
}
89+
this.credentialsManager.removeAllListeners('deleteCredentials');
7490
}
7591

7692
/**
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
const assert = require('assert');
2+
3+
const ClientManager = require('../../../lib/clients/ClientManager');
4+
5+
const fakeLogger = require('../../utils/fakeLogger');
6+
7+
function createClientManager() {
8+
return new ClientManager({
9+
id: 'test-extension',
10+
authConfig: { type: 'account', account: 'bart' },
11+
s3Config: { host: 's3.zenko.local', port: 80 },
12+
transport: 'http',
13+
}, fakeLogger);
14+
}
15+
16+
describe('ClientManager', () => {
17+
let clientManager;
18+
19+
beforeEach(() => {
20+
clientManager = createClientManager();
21+
});
22+
23+
afterEach(() => {
24+
clientManager.close();
25+
});
26+
27+
describe('initCredentialsManager', () => {
28+
it('should not let the credentials sweep hold the event loop open', () => {
29+
clientManager.initCredentialsManager();
30+
31+
assert.strictEqual(
32+
clientManager._deleteInactiveCredentialsInterval.hasRef(), false);
33+
});
34+
35+
it('should drop the clients of credentials that got removed', () => {
36+
clientManager.initCredentialsManager();
37+
clientManager.s3Clients['123456789012'] = {};
38+
clientManager.backbeatClients['123456789012'] = {};
39+
40+
clientManager.credentialsManager.emit('deleteCredentials', '123456789012');
41+
42+
assert.deepStrictEqual(clientManager.s3Clients, {});
43+
assert.deepStrictEqual(clientManager.backbeatClients, {});
44+
});
45+
});
46+
47+
describe('close', () => {
48+
it('should clear the credentials sweep', () => {
49+
clientManager.initCredentialsManager();
50+
51+
clientManager.close();
52+
53+
assert.strictEqual(clientManager._deleteInactiveCredentialsInterval, null);
54+
});
55+
56+
it('should stop listening for credentials removal', () => {
57+
clientManager.initCredentialsManager();
58+
59+
clientManager.close();
60+
61+
assert.strictEqual(
62+
clientManager.credentialsManager.listenerCount('deleteCredentials'), 0);
63+
});
64+
65+
it('should do nothing when initCredentialsManager was never called', () => {
66+
assert.doesNotThrow(() => clientManager.close());
67+
assert.strictEqual(clientManager._deleteInactiveCredentialsInterval, null);
68+
});
69+
70+
it('should be safe to call twice', () => {
71+
clientManager.initCredentialsManager();
72+
73+
clientManager.close();
74+
assert.doesNotThrow(() => clientManager.close());
75+
});
76+
});
77+
});

tests/unit/gc/GarbageCollector.spec.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,5 +144,35 @@ describe('garbage collector', function garbageCollector() {
144144
done();
145145
});
146146
});
147+
148+
it('should close the client manager', done => {
149+
const gcToClose = new GarbageCollector({
150+
kafkaConfig: {},
151+
s3Config: { host: 'localhost', port: 7777 },
152+
gcConfig: {
153+
topic: 'backbeat-gc',
154+
auth: { type: 'account', account: 'bart' },
155+
consumer: { groupId: 'backbeat-gc-consumer-group' },
156+
},
157+
});
158+
gcToClose.clientManager.initCredentialsManager();
159+
let consumerClosed = false;
160+
gcToClose._consumer = {
161+
close: cb => {
162+
consumerClosed = true;
163+
cb();
164+
},
165+
};
166+
167+
gcToClose.close(err => {
168+
assert.ifError(err);
169+
assert.strictEqual(
170+
gcToClose.clientManager._deleteInactiveCredentialsInterval, null);
171+
done();
172+
});
173+
174+
// the consumer must stop using the clients before they are released
175+
assert.strictEqual(consumerClosed, true);
176+
});
147177
});
148178
});

tests/unit/lifecycle/LifecycleBucketProcessor.spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,4 +340,21 @@ describe('Lifecycle Bucket Processor', () => {
340340
});
341341
});
342342
});
343+
344+
describe('close', () => {
345+
it('should close the client manager once the consumer and producer are closed', done => {
346+
const closeStub = sinon.stub(lbp.clientManager, 'close');
347+
const consumerClosed = sinon.stub();
348+
const producerClosed = sinon.stub();
349+
lbp._consumer = { close: cb => { consumerClosed(); cb(); } };
350+
lbp._producer = { close: cb => { producerClosed(); cb(); } };
351+
352+
lbp.close(() => {
353+
assert(closeStub.calledOnce);
354+
assert(closeStub.calledAfter(consumerClosed));
355+
assert(closeStub.calledAfter(producerClosed));
356+
done();
357+
});
358+
});
359+
});
343360
});

tests/unit/lifecycle/LifecycleConductor.spec.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,4 +1277,19 @@ describe('Lifecycle Conductor', () => {
12771277
});
12781278
});
12791279
});
1280+
1281+
describe('stop', () => {
1282+
it('should close the client manager once the producer is closed', done => {
1283+
const closeStub = sinon.stub(conductor.clientManager, 'close');
1284+
const producerClosed = sinon.stub();
1285+
conductor._producer = { close: cb => { producerClosed(); cb(); } };
1286+
1287+
conductor.stop(err => {
1288+
assert.ifError(err);
1289+
assert(closeStub.calledOnce);
1290+
assert(closeStub.calledAfter(producerClosed));
1291+
done();
1292+
});
1293+
});
1294+
});
12801295
});

0 commit comments

Comments
 (0)