Skip to content

Commit b4f5252

Browse files
authored
chore(atlas-service): clean up unused listeners (#4686)
1 parent 4cdc85b commit b4f5252

File tree

2 files changed

+77
-27
lines changed

2 files changed

+77
-27
lines changed

packages/atlas-service/src/main.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@ import Sinon from 'sinon';
22
import { expect } from 'chai';
33
import { AtlasService, throwIfNotOk } from './main';
44
import { promisify } from 'util';
5+
import type { EventEmitter } from 'events';
56
import { once } from 'events';
67

78
const wait = promisify(setTimeout);
89

10+
function getListenerCount(emitter: EventEmitter) {
11+
return emitter.eventNames().reduce((acc, name) => {
12+
return acc + emitter.listenerCount(name);
13+
}, 0);
14+
}
15+
916
describe('AtlasServiceMain', function () {
1017
const sandbox = Sinon.createSandbox();
1118

@@ -446,6 +453,19 @@ describe('AtlasServiceMain', function () {
446453
})(),
447454
]);
448455
});
456+
457+
it('should clean up listeners', async function () {
458+
const logger = AtlasService['oidcPluginLogger'] as EventEmitter;
459+
const initialCount = getListenerCount(logger);
460+
AtlasService['oidcPluginSyncedFromLoggerState'] = 'expired';
461+
const promise = AtlasService['maybeWaitForToken']();
462+
const inflightCount = getListenerCount(logger);
463+
// Sanity check, we added a bunch of listeners
464+
expect(inflightCount > initialCount).to.eq(true);
465+
logger.emit('atlas-service-token-refreshed');
466+
await promise;
467+
expect(getListenerCount(logger)).to.eq(initialCount);
468+
});
449469
});
450470
});
451471

@@ -471,6 +491,9 @@ describe('AtlasServiceMain', function () {
471491
});
472492

473493
it('should refresh token in atlas service state on `mongodb-oidc-plugin:refresh-succeeded` event', async function () {
494+
const initialListenerCount = getListenerCount(
495+
mockOidcPlugin.logger as EventEmitter
496+
);
474497
// Checking that multiple events while we are refreshing don't cause
475498
// multiple calls to REFRESH_TOKEN_CALLBACK
476499
mockOidcPlugin.logger.emit('mongodb-oidc-plugin:refresh-succeeded');
@@ -498,6 +521,10 @@ describe('AtlasServiceMain', function () {
498521
expect(AtlasService)
499522
.to.have.property('token')
500523
.deep.eq({ accessToken: '4321' });
524+
// Checking that we cleaned up all listeners
525+
expect(getListenerCount(mockOidcPlugin.logger as EventEmitter)).to.eq(
526+
initialListenerCount
527+
);
501528
});
502529
});
503530
});

packages/atlas-service/src/main.ts

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -181,24 +181,33 @@ export class AtlasService {
181181
}
182182
this.refreshing = true;
183183
try {
184-
await Promise.race([
185-
// When oidc-plugin logged that token was refreshed, the token is not
186-
// actually refreshed yet in the plugin state and so calling `REFRESH_TOKEN_CALLBACK`
187-
// causes weird behavior that actually opens the browser again, to work
188-
// around that we wait for the state update event in addition. We can't
189-
// guarantee that this event will be emitted for our particular state as
190-
// this is not something oidc-plugin exposes, but we can ignore this for
191-
// now as only one auth state is created in this instance of oidc-plugin
192-
once(this.oidcPluginLogger, 'mongodb-oidc-plugin:state-updated'),
193-
// At the same time refresh can still fail at this stage, so to avoid
194-
// refresh being stuck, we also wait for refresh-failed event and throw
195-
// if it happens to avoid calling `REFRESH_TOKEN_CALLBACK`
196-
once(this.oidcPluginLogger, 'mongodb-oidc-plugin:refresh-failed').then(
197-
() => {
184+
// We expect only one promise below to resolve, to clean up listeners that
185+
// never fired we are setting up an abort controller
186+
const listenerController = new AbortController();
187+
try {
188+
await Promise.race([
189+
// When oidc-plugin logged that token was refreshed, the token is not
190+
// actually refreshed yet in the plugin state and so calling `REFRESH_TOKEN_CALLBACK`
191+
// causes weird behavior that actually opens the browser again, to work
192+
// around that we wait for the state update event in addition. We can't
193+
// guarantee that this event will be emitted for our particular state as
194+
// this is not something oidc-plugin exposes, but we can ignore this for
195+
// now as only one auth state is created in this instance of oidc-plugin
196+
once(this.oidcPluginLogger, 'mongodb-oidc-plugin:state-updated', {
197+
signal: listenerController.signal,
198+
}),
199+
// At the same time refresh can still fail at this stage, so to avoid
200+
// refresh being stuck, we also wait for refresh-failed event and throw
201+
// if it happens to avoid calling `REFRESH_TOKEN_CALLBACK`
202+
once(this.oidcPluginLogger, 'mongodb-oidc-plugin:refresh-failed', {
203+
signal: listenerController.signal,
204+
}).then(() => {
198205
throw new Error('Refresh failed');
199-
}
200-
),
201-
]);
206+
}),
207+
]);
208+
} finally {
209+
listenerController.abort();
210+
}
202211
try {
203212
const token =
204213
await this.plugin.mongoClientOptions.authMechanismProperties
@@ -240,16 +249,30 @@ export class AtlasService {
240249
// is trying to refresh the token automatically, we can wait for this process
241250
// to finish before proceeding with a request
242251
if (this.oidcPluginSyncedFromLoggerState === 'expired') {
243-
await Promise.race([
244-
// We are using our own events here and not oidc plugin ones because
245-
// after plugin logged that token was refreshed, we still need to run
246-
// REFRESH_TOKEN_CALLBACK to get the actual token value in the state
247-
once(this.oidcPluginLogger, 'atlas-service-token-refreshed'),
248-
once(this.oidcPluginLogger, 'atlas-service-token-refresh-failed'),
249-
new Promise((resolve) => {
250-
signal?.addEventListener('abort', resolve, { once: true });
251-
}),
252-
]);
252+
// We expect only one promise below to resolve, to clean up listeners that
253+
// never fired we are setting up an abort controller
254+
const listenerController = new AbortController();
255+
try {
256+
await Promise.race([
257+
// We are using our own events here and not oidc plugin ones because
258+
// after plugin logged that token was refreshed, we still need to run
259+
// REFRESH_TOKEN_CALLBACK to get the actual token value in the state
260+
once(this.oidcPluginLogger, 'atlas-service-token-refreshed', {
261+
signal: listenerController.signal,
262+
}),
263+
once(this.oidcPluginLogger, 'atlas-service-token-refresh-failed', {
264+
signal: listenerController.signal,
265+
}),
266+
signal
267+
? once(signal, 'abort', { signal: listenerController.signal })
268+
: new Promise(() => {
269+
// This should just never resolve if no signal was passed to
270+
// this method
271+
}),
272+
]);
273+
} finally {
274+
listenerController.abort();
275+
}
253276
}
254277
}
255278

0 commit comments

Comments
 (0)