Skip to content

Commit 98e06df

Browse files
Reuse the destination clients across replication entries
A queue processor built a new ClientManager for every entry it replicated, and another one each time a retry moved to the next host. Each of them was left behind with its credentials cache, its keepAlive agents and the sweep timer holding it, so a role was assumed again for every single object. Hold them on the queue processor, keyed by destination host and role, the way the copy location tasks already share theirs, and release them on shutdown. They assume their role on the destination STS, so they cannot share the cache the copy location tasks read their sources with. Issue: BB-877
1 parent a35d2be commit 98e06df

5 files changed

Lines changed: 126 additions & 29 deletions

File tree

extensions/replication/queueProcessor/QueueProcessor.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,16 @@ class QueueProcessor extends EventEmitter {
229229
this.logger = new Logger(
230230
`Backbeat:Replication:QueueProcessor:${this.site}`);
231231

232-
// clients to read data straight from the sites we replicate to,
233-
// keyed by endpoint and role, shared by all copy location tasks
232+
// clients to read data straight from the sites we replicate to, keyed
233+
// by endpoint and role, shared by all copy location tasks: they assume
234+
// their role on the remote site's STS
234235
this.sourceClientManagers = {};
235236

237+
// clients to write to the destination site, keyed by host and role,
238+
// shared by all replication tasks: they assume their role on the
239+
// destination STS, so they cannot be shared with the ones above
240+
this.destClientManagers = {};
241+
236242
// global variables
237243
if (sourceConfig.transport === 'https') {
238244
this.sourceHTTPAgent = new HttpsAgent.Agent({
@@ -703,6 +709,7 @@ class QueueProcessor extends EventEmitter {
703709
vaultclientCache: this.vaultclientCache,
704710
accountCredsCache: this.accountCredsCache,
705711
sourceClientManagers: this.sourceClientManagers,
712+
destClientManagers: this.destClientManagers,
706713
replicationStatusProducer: this.replicationStatusProducer,
707714
mProducer: this._mProducer,
708715
logger: this.logger,
@@ -866,11 +873,15 @@ class QueueProcessor extends EventEmitter {
866873
return next();
867874
},
868875
], err => {
869-
// the tasks hold a reference to this map, so empty it in place
876+
// the tasks hold a reference to these maps, so empty them in place
870877
Object.keys(this.sourceClientManagers).forEach(key => {
871878
this.sourceClientManagers[key].close();
872879
delete this.sourceClientManagers[key];
873880
});
881+
Object.keys(this.destClientManagers).forEach(key => {
882+
this.destClientManagers[key].close();
883+
delete this.destClientManagers[key];
884+
});
874885
return done(err);
875886
});
876887
}

extensions/replication/tasks/ReplicateObject.js

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -837,21 +837,26 @@ class ReplicateObject extends BackbeatTask {
837837
if (this.destConfig.auth.type === authTypeAssumeRole) {
838838
const accountId = _extractAccountIdFromRole(targetRole);
839839
const roleName = _extractRoleNameFromRole(targetRole);
840-
this.clientManager = new ClientManager({
841-
id: accountId,
842-
authConfig: {
843-
type: authTypeAssumeRole,
844-
roleName,
845-
sts: this.destConfig.auth.sts,
846-
},
847-
s3Config: {
848-
host: this.destBackbeatHost.host,
849-
port: this.destBackbeatHost.port,
850-
},
851-
transport: this.destConfig.transport,
852-
}, this.logger);
853-
this.clientManager.initSTSConfig();
854-
this.clientManager.initCredentialsManager();
840+
const { host, port } = this.destBackbeatHost;
841+
// one manager per destination host and role, shared by every entry:
842+
// it holds the assumed-role credentials and the clients they opened
843+
const cacheKey = `${host}:${port}::${targetRole}`;
844+
this.clientManager = this.destClientManagers[cacheKey];
845+
if (!this.clientManager) {
846+
this.clientManager = new ClientManager({
847+
id: accountId,
848+
authConfig: {
849+
type: authTypeAssumeRole,
850+
roleName,
851+
sts: this.destConfig.auth.sts,
852+
},
853+
s3Config: { host, port },
854+
transport: this.destConfig.transport,
855+
}, this.logger);
856+
this.clientManager.initSTSConfig();
857+
this.clientManager.initCredentialsManager();
858+
this.destClientManagers[cacheKey] = this.clientManager;
859+
}
855860
this.backbeatDest = this.clientManager.getBackbeatClient(accountId);
856861
return;
857862
}

tests/unit/replication/CopyLocationTask.spec.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,24 @@ describe('CopyLocationTask', () => {
627627
assert(getBackbeatClient.alwaysCalledWith('123456789012'));
628628
});
629629

630+
it('should reuse the client manager across tasks', () => {
631+
const sourceClientManagers = task.sourceClientManagers;
632+
const otherEntry = new CopyLocationTask({
633+
getStateVars: () => ({
634+
mProducer: { getProducer: () => {} },
635+
sourceConfig: { transport: 'http' },
636+
logger: fakeLogger,
637+
sourceClientManagers,
638+
}),
639+
});
640+
641+
const client1 = task._getAssumedRoleS3Client(siteConfig, roleArn, fakeLogger);
642+
const client2 = otherEntry._getAssumedRoleS3Client(siteConfig, roleArn, fakeLogger);
643+
644+
assert.strictEqual(client1, client2);
645+
assert.strictEqual(Object.keys(sourceClientManagers).length, 1);
646+
});
647+
630648
it('should default the port when the location carries none', () => {
631649
task._getAssumedRoleS3Client(
632650
{ ...siteConfig, endpoint: 'production.example.com' }, roleArn, fakeLogger);

tests/unit/replication/QueueProcessor.spec.js

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,15 @@ describe('Queue Processor', () => {
335335
});
336336
});
337337

338+
describe('getStateVars', () => {
339+
it('should share the client manager caches with the tasks', () => {
340+
const stateVars = qp.getStateVars();
341+
342+
assert.strictEqual(stateVars.sourceClientManagers, qp.sourceClientManagers);
343+
assert.strictEqual(stateVars.destClientManagers, qp.destClientManagers);
344+
});
345+
});
346+
338347
describe('constructor', () => {
339348
it('should use s3c site\'s host as a destination host', () => {
340349
const config = getQueueProcessorConfig();
@@ -378,13 +387,17 @@ describe('Queue Processor', () => {
378387
});
379388

380389
describe('stop', () => {
381-
it('should close the cached source client managers', done => {
382-
const close = sinon.stub();
383-
qp.sourceClientManagers['http://site:8000::read-role'] = { close };
390+
it('should close the cached source and destination client managers', done => {
391+
const closeSource = sinon.stub();
392+
const closeDest = sinon.stub();
393+
qp.sourceClientManagers['http://site:8000::read-role'] = { close: closeSource };
394+
qp.destClientManagers['site:8000::write-role'] = { close: closeDest };
384395

385396
qp.stop(() => {
386-
assert(close.calledOnce);
397+
assert(closeSource.calledOnce);
398+
assert(closeDest.calledOnce);
387399
assert.deepStrictEqual(qp.sourceClientManagers, {});
400+
assert.deepStrictEqual(qp.destClientManagers, {});
388401
done();
389402
});
390403
});
@@ -393,7 +406,7 @@ describe('Queue Processor', () => {
393406
const consumerClosed = sinon.stub();
394407
const close = sinon.stub();
395408
qp._consumer = { close: cb => { consumerClosed(); cb(); } };
396-
qp.sourceClientManagers['http://site:8000::read-role'] = { close };
409+
qp.destClientManagers['site:8000::write-role'] = { close };
397410

398411
qp.stop(() => {
399412
assert(close.calledOnce);
@@ -402,14 +415,18 @@ describe('Queue Processor', () => {
402415
});
403416
});
404417

405-
it('should empty the client manager cache in place', done => {
406-
// the tasks hold a reference to that same object
407-
const cache = qp.sourceClientManagers;
408-
cache['http://site:8000::read-role'] = { close: () => {} };
418+
it('should empty the client manager caches in place', done => {
419+
// the tasks hold a reference to those same objects
420+
const sourceCache = qp.sourceClientManagers;
421+
const destCache = qp.destClientManagers;
422+
sourceCache['http://site:8000::read-role'] = { close: () => {} };
423+
destCache['site:8000::write-role'] = { close: () => {} };
409424

410425
qp.stop(() => {
411-
assert.strictEqual(qp.sourceClientManagers, cache);
412-
assert.deepStrictEqual(cache, {});
426+
assert.strictEqual(qp.sourceClientManagers, sourceCache);
427+
assert.strictEqual(qp.destClientManagers, destCache);
428+
assert.deepStrictEqual(sourceCache, {});
429+
assert.deepStrictEqual(destCache, {});
413430
done();
414431
});
415432
});

tests/unit/replication/ReplicateObject.spec.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ describe('ReplicateObject', () => {
7373
port: 80,
7474
}),
7575
},
76+
destClientManagers: {},
7677
logger: fakeLogger,
7778
}),
7879
});
@@ -1031,6 +1032,51 @@ describe('ReplicateObject', () => {
10311032
});
10321033
});
10331034

1035+
it('should reuse the client manager across entries for the same host and role', () => {
1036+
sinon.stub(ClientManager.prototype, 'initCredentialsManager').returns(null);
1037+
sinon.stub(ClientManager.prototype, 'getBackbeatClient').returns(null);
1038+
const destClientManagers = task.destClientManagers;
1039+
const otherEntry = new ReplicateObject({
1040+
getStateVars: () => ({ ...task, destClientManagers }),
1041+
});
1042+
1043+
task._setupDestClients('arn:aws:iam::123456789012:role/crr-role', fakeLogger);
1044+
otherEntry._setupDestClients('arn:aws:iam::123456789012:role/crr-role', fakeLogger);
1045+
1046+
assert.strictEqual(otherEntry.clientManager, task.clientManager);
1047+
assert.strictEqual(Object.keys(destClientManagers).length, 1);
1048+
});
1049+
1050+
it('should use a separate client manager per role', () => {
1051+
sinon.stub(ClientManager.prototype, 'initCredentialsManager').returns(null);
1052+
sinon.stub(ClientManager.prototype, 'getBackbeatClient').returns(null);
1053+
1054+
task._setupDestClients('arn:aws:iam::123456789012:role/crr-role', fakeLogger);
1055+
const first = task.clientManager;
1056+
task._setupDestClients('arn:aws:iam::210987654321:role/other-role', fakeLogger);
1057+
1058+
assert.notStrictEqual(task.clientManager, first);
1059+
assert.strictEqual(task.clientManager._id, '210987654321');
1060+
assert.strictEqual(Object.keys(task.destClientManagers).length, 2);
1061+
});
1062+
1063+
it('should not hand back the manager of the failed host when retrying', () => {
1064+
sinon.stub(ClientManager.prototype, 'initCredentialsManager').returns(null);
1065+
sinon.stub(ClientManager.prototype, 'getBackbeatClient').returns(null);
1066+
const role = 'arn:aws:iam::123456789012:role/crr-role';
1067+
1068+
task._setupDestClients(role, fakeLogger);
1069+
const failedHostManager = task.clientManager;
1070+
1071+
// what the retry hooks do: rotate the host, then set the clients up again
1072+
task.destHosts.pickHost = () => ({ host: 's3-2.zenko.local', port: 80 });
1073+
task._setupDestClients(role, fakeLogger);
1074+
1075+
assert.notStrictEqual(task.clientManager, failedHostManager);
1076+
assert.deepStrictEqual(task.clientManager._s3Config,
1077+
{ host: 's3-2.zenko.local', port: 80 });
1078+
});
1079+
10341080
it('should setup destination BackbeatClient with proper creds when not in assumeRole', async () => {
10351081
task.destConfig.auth = {
10361082
type: 'service',

0 commit comments

Comments
 (0)