Skip to content

Commit

Permalink
feat: add support for "Explicit Resource Management"
Browse files Browse the repository at this point in the history
closes #794
  • Loading branch information
hasezoey committed Jun 5, 2024
1 parent c9f45c9 commit ab7a73c
Show file tree
Hide file tree
Showing 13 changed files with 275 additions and 1 deletion.
20 changes: 20 additions & 0 deletions docs/api/interfaces/mongo-dispose-opts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
id: mongo-memory-dispose-opts
title: 'DisposeOptions'
---

API Documentation of `DisposeOptions`-Interface

## Values for `DisposeOptions`

### enabled

Typings: `enabled?: boolean`

Set whether to stop the manager on `[Symbol.asyncDispose]` calls.

### cleanup

Typings: `cleanup?: Cleanup`

Set custom cleanup options to be used for disposal, see [`cleanup` function](../classes/mongo-memory-server.md#cleanup) (the same options apply for the replset).
6 changes: 6 additions & 0 deletions docs/api/interfaces/mongo-memory-server-opts.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ Set custom spawn options for spawning processes, uses [`SpawnOptions`](https://n
Typings: `auth?: AutomaticAuth`

Set custom Authentication options for the instance, uses [`AutomaticAuth`](./mongo-memory-server-automaticauth.md).

### dispose

Typings: `dispose?: DisposeOptions`

Set custom behavior for when `[Symbol.asyncDispose]` is called, uses [`DisposeOptions`](./mongo-dispose-opts.md).
6 changes: 6 additions & 0 deletions docs/api/interfaces/replset-opts.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ Set how many ReplSet members to spawn, this number will be deducted from length
:::tip
It is recommended to set this number to a **odd** number, and try to never have it be **even**, see [MongoDB Deploy an Odd Number of Members](https://www.mongodb.com/docs/v5.2/core/replica-set-architectures/#deploy-an-odd-number-of-members).
:::

### dispose

Typings: `dispose?: DisposeOptions`

Set custom behavior for when `[Symbol.asyncDispose]` is called, uses [`DisposeOptions`](./mongo-dispose-opts.md).
11 changes: 11 additions & 0 deletions docs/guides/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,14 @@ Since `8.4.0` objects can also be used instead of just booleans for parameter in

For Example `.stop({ doCleanup: false })` can be used instead of `.stop(false)`.
:::

### Does this package support Explicit Resource Management?

Yes, `[Symbol.asyncDispose]` is implemented for all manager classes, behavior can be configured via `dispose` options:

- [`MongoMemoryServerOpts.dispose`](../api/interfaces/mongo-memory-server-opts.md#dispose)
- [`ReplSetOpts.dispose`](../api/interfaces/replset-opts.md#dispose)

:::note
Note that when using `await using server =` that `[Symbol.asyncDispose]` is called at the end of the scope even if the value is reassigned to something out of the current scope.
:::
19 changes: 18 additions & 1 deletion packages/mongodb-memory-server-core/src/MongoMemoryReplSet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { EventEmitter } from 'events';
import { MongoMemoryServer, AutomaticAuth, MongoMemoryServerOpts } from './MongoMemoryServer';
import {
MongoMemoryServer,
AutomaticAuth,
MongoMemoryServerOpts,
DisposeOptions,
} from './MongoMemoryServer';
import {
assertion,
authDefault,
Expand Down Expand Up @@ -92,6 +97,10 @@ export interface ReplSetOpts {
* @default {}
*/
configSettings?: MongoMemoryReplSetConfigSettings;
/**
* Options for automatic dispose for "Explicit Resource Management"
*/
dispose?: DisposeOptions;
}

/**
Expand Down Expand Up @@ -264,6 +273,7 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced
spawn: {},
storageEngine,
configSettings: {},
dispose: {},
};
// force overwrite "storageEngine" because it is transformed already
this._replSetOpts = { ...defaults, ...val, storageEngine };
Expand Down Expand Up @@ -799,6 +809,13 @@ export class MongoMemoryReplSet extends EventEmitter implements ManagerAdvanced

log('_waitForPrimary: detected one primary instance ');
}

// Symbol for "Explicit Resource Management"
async [Symbol.asyncDispose]() {
if (this.replSetOpts.dispose?.enabled ?? true) {
await this.stop(this.replSetOpts.dispose?.cleanup);
}
}
}

export default MongoMemoryReplSet;
Expand Down
28 changes: 28 additions & 0 deletions packages/mongodb-memory-server-core/src/MongoMemoryServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,27 @@ export interface MongoMemoryServerOpts {
* Defining this enables automatic user creation
*/
auth?: AutomaticAuth;
/**
* Options for automatic dispose for "Explicit Resource Management"
*/
dispose?: DisposeOptions;
}

/**
* Options to configure `Symbol.asyncDispose` behavior
*/
export interface DisposeOptions {
/**
* Set whether to run the dispose hook or not.
* Note that this only applies when `Symbol.asyncDispose` is actually called
* @default true
*/
enabled?: boolean;
/**
* Pass custom options for cleanup
* @default { doCleanup: true, force: false }
*/
cleanup?: Cleanup;
}

export interface AutomaticAuth {
Expand Down Expand Up @@ -838,6 +859,13 @@ export class MongoMemoryServer extends EventEmitter implements ManagerAdvanced {
? this.auth.enable
: false; // if "this._replSetOpts.auth.enable" is not defined, default to false
}

// Symbol for "Explicit Resource Management"
async [Symbol.asyncDispose]() {
if (this.opts.dispose?.enabled ?? true) {
await this.stop(this.opts.dispose?.cleanup);
}
}
}

export default MongoMemoryServer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ describe('MongoMemoryReplSet', () => {
spawn: {},
storageEngine: 'wiredTiger',
configSettings: {},
dispose: {},
});
replSet.replSetOpts = { auth: { enable: true } };
// @ts-expect-error because "_replSetOpts" is protected
Expand All @@ -457,6 +458,7 @@ describe('MongoMemoryReplSet', () => {
spawn: {},
storageEngine: 'wiredTiger',
configSettings: {},
dispose: {},
});
});

Expand Down Expand Up @@ -802,4 +804,81 @@ describe('MongoMemoryReplSet', () => {
await server.stop();
});
});

describe('asyncDispose', () => {
it('should work by default', async () => {
jest.spyOn(MongoMemoryReplSet.prototype, 'start');
jest.spyOn(MongoMemoryReplSet.prototype, 'stop');
let outer;
// would like to test this, but jest seemingly does not support spying on symbols
// jest.spyOn(MongoMemoryReplSet.prototype, Symbol.asyncDispose);
{
await using server = await MongoMemoryReplSet.create();
// use the value and test that it actually runs, as "getUri" will throw is not in "running" state
server.getUri();
// reassignment still calls dispose at the *current* scope
outer = server;
}
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped);
expect(outer.servers.length).toStrictEqual(0);
expect(MongoMemoryReplSet.prototype.start).toHaveBeenCalledTimes(1);
expect(MongoMemoryReplSet.prototype.stop).toHaveBeenCalledTimes(1);
// expect(MongoMemoryReplSet.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1);
});

it('should be able to be disabled', async () => {
jest.spyOn(MongoMemoryReplSet.prototype, 'start');
jest.spyOn(MongoMemoryReplSet.prototype, 'stop');
let outer;
// would like to test this, but jest seemingly does not support spying on symbols
// jest.spyOn(MongoMemoryReplSet.prototype, Symbol.asyncDispose);
{
await using server = await MongoMemoryReplSet.create({
replSet: { dispose: { enabled: false } },
});
// use the value and test that it actually runs, as "getUri" will throw is not in "running" state
server.getUri();
// reassignment still calls dispose at the *current* scope
outer = server;
}
expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.running);
expect(outer.servers.length).toStrictEqual(1);
expect(MongoMemoryReplSet.prototype.start).toHaveBeenCalledTimes(1);
expect(MongoMemoryReplSet.prototype.stop).toHaveBeenCalledTimes(0);
// expect(MongoMemoryReplSet.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1);
await outer.stop();
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped);
});

it('should be able to set custom cleanup', async () => {
jest.spyOn(MongoMemoryReplSet.prototype, 'start');
jest.spyOn(MongoMemoryReplSet.prototype, 'stop');
let outer;
// would like to test this, but jest seemingly does not support spying on symbols
// jest.spyOn(MongoMemoryReplSet.prototype, Symbol.asyncDispose);
{
await using server = await MongoMemoryReplSet.create({
replSet: {
dispose: { cleanup: { doCleanup: false } },
},
});
// use the value and test that it actually runs, as "getUri" will throw is not in "running" state
server.getUri();
// reassignment still calls dispose at the *current* scope
outer = server;
}
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped);
expect(outer.servers.length).toStrictEqual(1);
expect(MongoMemoryReplSet.prototype.start).toHaveBeenCalledTimes(1);
expect(MongoMemoryReplSet.prototype.stop).toHaveBeenCalledTimes(1);
// expect(MongoMemoryReplSet.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1);
await outer.cleanup({ doCleanup: true });
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryReplSetStates.stopped);
expect(outer.servers.length).toStrictEqual(0);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1188,4 +1188,73 @@ describe('MongoMemoryServer', () => {
await server.stop();
});
});

describe('asyncDispose', () => {
it('should work by default', async () => {
jest.spyOn(MongoMemoryServer.prototype, 'start');
jest.spyOn(MongoMemoryServer.prototype, 'stop');
let outer;
// would like to test this, but jest seemingly does not support spying on symbols
// jest.spyOn(MongoMemoryServer.prototype, Symbol.asyncDispose);
{
await using server = await MongoMemoryServer.create();
// use the value and test that it actually runs, as "getUri" will throw is not in "running" state
server.getUri();
// reassignment still calls dispose at the *current* scope
outer = server;
}
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryServerStates.new);
expect(MongoMemoryServer.prototype.start).toHaveBeenCalledTimes(1);
expect(MongoMemoryServer.prototype.stop).toHaveBeenCalledTimes(1);
// expect(MongoMemoryServer.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1);
});

it('should be able to be disabled', async () => {
jest.spyOn(MongoMemoryServer.prototype, 'start');
jest.spyOn(MongoMemoryServer.prototype, 'stop');
let outer;
// would like to test this, but jest seemingly does not support spying on symbols
// jest.spyOn(MongoMemoryServer.prototype, Symbol.asyncDispose);
{
await using server = await MongoMemoryServer.create({ dispose: { enabled: false } });
// use the value and test that it actually runs, as "getUri" will throw is not in "running" state
server.getUri();
// reassignment still calls dispose at the *current* scope
outer = server;
}
expect(outer.state).toStrictEqual(MongoMemoryServerStates.running);
expect(MongoMemoryServer.prototype.start).toHaveBeenCalledTimes(1);
expect(MongoMemoryServer.prototype.stop).toHaveBeenCalledTimes(0);
// expect(MongoMemoryServer.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1);
await outer.stop();
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryServerStates.new);
});

it('should be able to set custom cleanup', async () => {
jest.spyOn(MongoMemoryServer.prototype, 'start');
jest.spyOn(MongoMemoryServer.prototype, 'stop');
let outer;
// would like to test this, but jest seemingly does not support spying on symbols
// jest.spyOn(MongoMemoryServer.prototype, Symbol.asyncDispose);
{
await using server = await MongoMemoryServer.create({
dispose: { cleanup: { doCleanup: false } },
});
// use the value and test that it actually runs, as "getUri" will throw is not in "running" state
server.getUri();
// reassignment still calls dispose at the *current* scope
outer = server;
}
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryServerStates.stopped);
expect(MongoMemoryServer.prototype.start).toHaveBeenCalledTimes(1);
expect(MongoMemoryServer.prototype.stop).toHaveBeenCalledTimes(1);
// expect(MongoMemoryServer.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1);
await outer.cleanup({ doCleanup: true });
// not "stopped" because of cleanup
expect(outer.state).toStrictEqual(MongoMemoryServerStates.new);
});
});
});
5 changes: 5 additions & 0 deletions packages/mongodb-memory-server-core/src/util/MongoInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,11 @@ export class MongoInstance extends EventEmitter implements ManagerBase {
);
}
}

/// Symbol for "Explicit Resource Management"
async [Symbol.asyncDispose]() {
await this.stop();
}
}

export default MongoInstance;
Original file line number Diff line number Diff line change
Expand Up @@ -780,4 +780,28 @@ describe('MongodbInstance', () => {
await mongod.stop();
}
});

describe('asyncDispose', () => {
it('should work', async () => {
jest.spyOn(MongodbInstance.prototype, 'start');
jest.spyOn(MongodbInstance.prototype, 'stop');
let outer;
// would like to test this, but jest seemingly does not support spying on symbols
// jest.spyOn(MongodbInstance.prototype, Symbol.asyncDispose);
{
const gotPort = await getFreePort(27333);
await using server = await MongodbInstance.create({
instance: { port: gotPort, dbPath: tmpDir },
binary: { version },
});
expect(server.mongodProcess).toBeTruthy();
// reassignment still calls dispose at the *current* scope
outer = server;
}
expect(outer.mongodProcess).not.toBeTruthy();
expect(MongodbInstance.prototype.start).toHaveBeenCalledTimes(1);
expect(MongodbInstance.prototype.stop).toHaveBeenCalledTimes(1);
// expect(MongodbInstance.prototype[Symbol.asyncDispose]).toHaveBeenCalledTimes(1);
});
});
});
6 changes: 6 additions & 0 deletions packages/mongodb-memory-server-core/src/util/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import * as path from 'path';
import { readFileSync } from 'fs';
import { isNullOrUndefined } from './utils';

// polyfills
// @ts-expect-error they are marked "read-only", but are set-able if not implemented by the runtime
Symbol.dispose ??= Symbol('Symbol.dispose');
// @ts-expect-error they are marked "read-only", but are set-able if not implemented by the runtime
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');

const log = debug('MongoMS:ResolveConfig');

/** Enum of all possible config options */
Expand Down
2 changes: 2 additions & 0 deletions packages/mongodb-memory-server-core/src/util/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ export abstract class ManagerBase {
abstract start(forceSamePort: boolean): Promise<void>;
abstract start(): Promise<void>;
abstract stop(cleanup: Cleanup): Promise<boolean>;
// Symbol for "Explicit Resource Management"
abstract [Symbol.asyncDispose](): Promise<void>;
}

/**
Expand Down
1 change: 1 addition & 0 deletions website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
'api/interfaces/mongo-memory-instance-replicamemberconfig',
'api/interfaces/mongo-memory-binary-opts',
'api/interfaces/mongo-memory-replset-opts',
'api/interfaces/mongo-memory-dispose-opts',
'api/interfaces/replset-opts',
],
},
Expand Down

0 comments on commit ab7a73c

Please sign in to comment.