Skip to content

Commit

Permalink
fix(MongoInstance): add a timeout for the "launch" promise
Browse files Browse the repository at this point in the history
re #710
  • Loading branch information
hasezoey committed Oct 28, 2022
1 parent 1c60f61 commit f822856
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 2 deletions.
27 changes: 26 additions & 1 deletion packages/mongodb-memory-server-core/src/util/MongoInstance.ts
Expand Up @@ -14,6 +14,7 @@ import { lt } from 'semver';
import { EventEmitter } from 'events';
import { MongoClient, MongoClientOptions, MongoNetworkError } from 'mongodb';
import {
GenericMMSError,
KeyFileMissingError,
StartBinaryFailedError,
StdoutInstanceError,
Expand Down Expand Up @@ -165,6 +166,12 @@ export interface MongoMemoryInstanceOpts extends MongoMemoryInstanceOptsBase {
* @default undefined
*/
keyfileLocation?: string;
/**
* Define a custom timeout for when out of some reason the binary cannot get started correctly
* Time in MS
* @default 10000 10 seconds
*/
launchTimeout?: number;
}

export enum MongoInstanceEvents {
Expand Down Expand Up @@ -336,12 +343,30 @@ export class MongoInstance extends EventEmitter implements ManagerBase {
this.isInstanceReady = false;
this.isReplSet = false;

const launch: Promise<void> = new Promise((res, rej) => {
let timeout: NodeJS.Timeout;

const launch: Promise<void> = new Promise<void>((res, rej) => {
this.once(MongoInstanceEvents.instanceReady, res);
this.once(MongoInstanceEvents.instanceError, rej);
this.once(MongoInstanceEvents.instanceClosed, function launchInstanceClosed() {
rej(new Error('Instance Exited before being ready and without throwing an error!'));
});

// extra conditions just to be sure that the custom defined timeout is valid
const timeoutTime =
!!this.instanceOpts.launchTimeout && this.instanceOpts.launchTimeout >= 1000
? this.instanceOpts.launchTimeout
: 1000 * 10; // default 10 seconds

timeout = setTimeout(() => {
const err = new GenericMMSError(`Instance failed to start within ${timeoutTime}ms`);
this.emit(MongoInstanceEvents.instanceError, err);

rej(err);
}, timeoutTime);
}).finally(() => {
// always clear the timeout after the promise somehow resolves
clearTimeout(timeout);
});

const mongoBin = await MongoBinary.getPath(this.binaryOpts);
Expand Down
Expand Up @@ -4,7 +4,12 @@ import * as dbUtil from '../utils';
import MongodbInstance, { MongoInstanceEvents } from '../MongoInstance';
import resolveConfig, { ResolveConfigVariables } from '../resolveConfig';
import getPort from 'get-port';
import { StartBinaryFailedError, StdoutInstanceError, UnexpectedCloseError } from '../errors';
import {
GenericMMSError,
StartBinaryFailedError,
StdoutInstanceError,
UnexpectedCloseError,
} from '../errors';
import { assertIsError } from '../../__tests__/testUtils/test_utils';

jest.setTimeout(100000); // 10s
Expand All @@ -17,6 +22,7 @@ beforeEach(() => {

afterEach(() => {
tmpDir.removeCallback();
jest.restoreAllMocks();
});

describe('MongodbInstance', () => {
Expand Down Expand Up @@ -597,5 +603,42 @@ describe('MongodbInstance', () => {
expect(event.message).toMatchSnapshot();
});
});

it('"start" should emit a "instanceError" when timeout is reached and throw a error', async () => {
mongod.instanceOpts['launchTimeout'] = 1000;

jest.spyOn(mongod, '_launchMongod').mockImplementation(
// @ts-expect-error The following is not meant to work, but in this test we dont care about that result, only that it never fires any events
() => {
return { pid: 0 }; // required for a direct check afterwards
}
);
jest.spyOn(mongod, '_launchKiller').mockImplementation(
// @ts-expect-error The following is not meant to work, but in this test we dont care about that result, only that it never fires any events
() => undefined
);
jest.spyOn(mongod, 'stop').mockImplementation(
// @ts-expect-error The following is not meant to work, but in this test we dont care about that result, only that it never fires any events
() => undefined
);

try {
await mongod.start();
fail('Expected "start" to throw');
} catch (err) {
// this error could be thrown through "once => instanceError" or from the timeout directly, but it does not matter where it gets thrown from
expect(err).toBeInstanceOf(GenericMMSError);
assertIsError(err);
expect(err.message).toMatchSnapshot();

expect(events.size).toEqual(1);

const event = events.get(MongoInstanceEvents.instanceError)?.[0];
expect(event).toBeInstanceOf(GenericMMSError);
assertIsError(event);
expect(event.message).toStrictEqual(err.message);
expect(err).toBe(event); // reference compare, because these 2 values should be the same
}
});
});
});
Expand Up @@ -17,6 +17,8 @@ exports[`MongodbInstance test events "closeHandler" should emit "instanceError"

exports[`MongodbInstance test events "closeHandler" should emit "instanceError" with non-0 or non-12 code 2`] = `"Instance closed unexpectedly with code \\"null\\" and signal \\"SIG\\""`;

exports[`MongodbInstance test events "start" should emit a "instanceError" when timeout is reached and throw a error 1`] = `"Instance failed to start within 1000ms"`;

exports[`MongodbInstance test events checkErrorInLine() should emit "instanceError" when shared libraries fail to load 1`] = `"Instance failed to start because a library is missing or cannot be opened: \\"libcrypto.so.1.1\\""`;

exports[`MongodbInstance test events stdoutHandler() should emit "instanceError" when "excepetion in initAndListen" is thrown DBException in initAndListen 1`] = `
Expand Down

0 comments on commit f822856

Please sign in to comment.