Skip to content

Commit

Permalink
fix(MongoInstance::stop): handle "stop" being called multiple times w…
Browse files Browse the repository at this point in the history
…hile running

fixes #802
  • Loading branch information
hasezoey committed Oct 5, 2023
1 parent 4515fdd commit 80e6cd8
Showing 1 changed file with 77 additions and 57 deletions.
134 changes: 77 additions & 57 deletions packages/mongodb-memory-server-core/src/util/MongoInstance.ts
Expand Up @@ -244,6 +244,14 @@ export class MongoInstance extends EventEmitter implements ManagerBase {
*/
isReplSet: boolean = false;

/**
* Extra promise to avoid multiple calls of `.stop` at the same time
*
* @see https://github.com/nodkz/mongodb-memory-server/issues/802
*/
// NOTE: i am not sure how to properly test this
stopPromise?: Promise<boolean>;

constructor(opts: Partial<MongodOpts>) {
super();
this.instanceOpts = { ...opts.instance };
Expand Down Expand Up @@ -406,69 +414,81 @@ export class MongoInstance extends EventEmitter implements ManagerBase {
return false;
}

if (!isNullOrUndefined(this.mongodProcess)) {
// try to run "shutdown" before running "killProcess" (gracefull "SIGINT")
// using this, otherwise on windows nodejs will handle "SIGINT" & "SIGTERM" & "SIGKILL" the same (instant exit)
if (this.isReplSet) {
let con: MongoClient | undefined;
try {
this.debug('stop: trying shutdownServer');
const port = this.instanceOpts.port;
const ip = this.instanceOpts.ip;
assertion(
!isNullOrUndefined(port),
new Error('Cannot shutdown replset gracefully, no "port" is provided')
);
assertion(
!isNullOrUndefined(ip),
new Error('Cannot shutdown replset gracefully, no "ip" is provided')
);

con = await MongoClient.connect(uriTemplate(ip, port, 'admin'), {
...this.extraConnectionOptions,
directConnection: true,
});

const admin = con.db('admin'); // just to ensure it is actually the "admin" database
// "timeoutSecs" is set to "1" otherwise it will take at least "10" seconds to stop (very long tests)
await admin.command({ shutdown: 1, force: true, timeoutSecs: 1 });
this.debug('stop: after admin shutdown command');
} catch (err) {
// Quote from MongoDB Documentation (https://docs.mongodb.com/manual/reference/command/replSetStepDown/#client-connections):
// > Starting in MongoDB 4.2, replSetStepDown command no longer closes all client connections.
// > In MongoDB 4.0 and earlier, replSetStepDown command closes all client connections during the step down.
// so error "MongoNetworkError: connection 1 to 127.0.0.1:41485 closed" will get thrown below 4.2
if (
!(
err instanceof MongoNetworkError &&
/^connection \d+ to [\d.]+:\d+ closed$/i.test(err.message)
)
) {
console.warn(err);
}
} finally {
if (!isNullOrUndefined(con)) {
// even if it errors out, somehow the connection stays open
await con.close();
if (!isNullOrUndefined(this.stopPromise)) {
this.debug('stop: stopPromise is already set, using that');

return this.stopPromise;
}

// wrap the actual stop in a promise, so it can be awaited in multiple calls
// for example a instanceError while stop is already running would cause another stop
this.stopPromise = (async () => {
if (!isNullOrUndefined(this.mongodProcess)) {
// try to run "shutdown" before running "killProcess" (gracefull "SIGINT")
// using this, otherwise on windows nodejs will handle "SIGINT" & "SIGTERM" & "SIGKILL" the same (instant exit)
if (this.isReplSet) {
let con: MongoClient | undefined;
try {
this.debug('stop: trying shutdownServer');
const port = this.instanceOpts.port;
const ip = this.instanceOpts.ip;
assertion(
!isNullOrUndefined(port),
new Error('Cannot shutdown replset gracefully, no "port" is provided')
);
assertion(
!isNullOrUndefined(ip),
new Error('Cannot shutdown replset gracefully, no "ip" is provided')
);

con = await MongoClient.connect(uriTemplate(ip, port, 'admin'), {
...this.extraConnectionOptions,
directConnection: true,
});

const admin = con.db('admin'); // just to ensure it is actually the "admin" database
// "timeoutSecs" is set to "1" otherwise it will take at least "10" seconds to stop (very long tests)
await admin.command({ shutdown: 1, force: true, timeoutSecs: 1 });
this.debug('stop: after admin shutdown command');
} catch (err) {
// Quote from MongoDB Documentation (https://docs.mongodb.com/manual/reference/command/replSetStepDown/#client-connections):
// > Starting in MongoDB 4.2, replSetStepDown command no longer closes all client connections.
// > In MongoDB 4.0 and earlier, replSetStepDown command closes all client connections during the step down.
// so error "MongoNetworkError: connection 1 to 127.0.0.1:41485 closed" will get thrown below 4.2
if (
!(
err instanceof MongoNetworkError &&
/^connection \d+ to [\d.]+:\d+ closed$/i.test(err.message)
)
) {
console.warn(err);
}
} finally {
if (!isNullOrUndefined(con)) {
// even if it errors out, somehow the connection stays open
await con.close();
}
}
}

await killProcess(this.mongodProcess, 'mongodProcess', this.instanceOpts.port);
this.mongodProcess = undefined; // reset reference to the childProcess for "mongod"
} else {
this.debug('stop: mongodProcess: nothing to shutdown, skipping');
}
if (!isNullOrUndefined(this.killerProcess)) {
await killProcess(this.killerProcess, 'killerProcess', this.instanceOpts.port);
this.killerProcess = undefined; // reset reference to the childProcess for "mongo_killer"
} else {
this.debug('stop: killerProcess: nothing to shutdown, skipping');
}

await killProcess(this.mongodProcess, 'mongodProcess', this.instanceOpts.port);
this.mongodProcess = undefined; // reset reference to the childProcess for "mongod"
} else {
this.debug('stop: mongodProcess: nothing to shutdown, skipping');
}
if (!isNullOrUndefined(this.killerProcess)) {
await killProcess(this.killerProcess, 'killerProcess', this.instanceOpts.port);
this.killerProcess = undefined; // reset reference to the childProcess for "mongo_killer"
} else {
this.debug('stop: killerProcess: nothing to shutdown, skipping');
}
this.debug('stop: Instance Finished Shutdown');

this.debug('stop: Instance Finished Shutdown');
return true;
})().finally(() => (this.stopPromise = undefined));

return true;
return this.stopPromise;
}

/**
Expand Down

0 comments on commit 80e6cd8

Please sign in to comment.