From f822856ca4bb9c53a229a0399f00966fa6023e25 Mon Sep 17 00:00:00 2001 From: hasezoey Date: Fri, 28 Oct 2022 18:28:37 +0200 Subject: [PATCH] fix(MongoInstance): add a timeout for the "launch" promise re #710 --- .../src/util/MongoInstance.ts | 27 ++++++++++- .../src/util/__tests__/MongoInstance.test.ts | 45 ++++++++++++++++++- .../__snapshots__/MongoInstance.test.ts.snap | 2 + 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/mongodb-memory-server-core/src/util/MongoInstance.ts b/packages/mongodb-memory-server-core/src/util/MongoInstance.ts index 2804e46d0..954c7de56 100644 --- a/packages/mongodb-memory-server-core/src/util/MongoInstance.ts +++ b/packages/mongodb-memory-server-core/src/util/MongoInstance.ts @@ -14,6 +14,7 @@ import { lt } from 'semver'; import { EventEmitter } from 'events'; import { MongoClient, MongoClientOptions, MongoNetworkError } from 'mongodb'; import { + GenericMMSError, KeyFileMissingError, StartBinaryFailedError, StdoutInstanceError, @@ -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 { @@ -336,12 +343,30 @@ export class MongoInstance extends EventEmitter implements ManagerBase { this.isInstanceReady = false; this.isReplSet = false; - const launch: Promise = new Promise((res, rej) => { + let timeout: NodeJS.Timeout; + + const launch: Promise = new Promise((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); diff --git a/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts b/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts index 52707a165..2e95b4a57 100644 --- a/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts +++ b/packages/mongodb-memory-server-core/src/util/__tests__/MongoInstance.test.ts @@ -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 @@ -17,6 +22,7 @@ beforeEach(() => { afterEach(() => { tmpDir.removeCallback(); + jest.restoreAllMocks(); }); describe('MongodbInstance', () => { @@ -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 + } + }); }); }); diff --git a/packages/mongodb-memory-server-core/src/util/__tests__/__snapshots__/MongoInstance.test.ts.snap b/packages/mongodb-memory-server-core/src/util/__tests__/__snapshots__/MongoInstance.test.ts.snap index ab2835382..3e2e8af16 100644 --- a/packages/mongodb-memory-server-core/src/util/__tests__/__snapshots__/MongoInstance.test.ts.snap +++ b/packages/mongodb-memory-server-core/src/util/__tests__/__snapshots__/MongoInstance.test.ts.snap @@ -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`] = `