Skip to content

Commit 375d4e8

Browse files
authored
fix(mongodb-runner): make close operation more resilient (#571)
Store identifying information about a server in a database, then only actually run `process.kill()` on servers where this identifying information matches what we expect it to match. Also, do not kill processes to which we cannot connect, and instead assume they have already been closed before. Fixes: #483
1 parent 160c1ca commit 375d4e8

File tree

4 files changed

+120
-30
lines changed

4 files changed

+120
-30
lines changed

packages/mongodb-runner/src/mongocluster.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,10 @@ describe('MongoCluster', function () {
258258
secondaries: 0,
259259
});
260260
cluster = await MongoCluster.deserialize(cluster.serialize());
261-
const { ok } = await cluster.withClient(async (client) => {
262-
return await client.db('admin').command({ ping: 1 });
261+
const doc = await cluster.withClient(async (client) => {
262+
return await client.db('config').collection('mongodbrunner').findOne();
263263
});
264-
expect(ok).to.equal(1);
264+
expect(doc?._id).to.be.a('string');
265265
await cluster.close();
266266
});
267267
});

packages/mongodb-runner/src/mongoserver.ts

Lines changed: 99 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import {
99
isFailureToSetupListener,
1010
} from './mongologreader';
1111
import { Readable } from 'stream';
12-
import type { Document } from 'mongodb';
12+
import type { Document, MongoClientOptions } from 'mongodb';
1313
import { MongoClient } from 'mongodb';
1414
import path from 'path';
1515
import { once } from 'events';
16-
import { uuid, debug } from './util';
16+
import { uuid, debug, pick } from './util';
1717

1818
export interface MongoServerOptions {
1919
binDir?: string;
@@ -24,32 +24,52 @@ export interface MongoServerOptions {
2424
docker?: string | string[]; // Image or docker options
2525
}
2626

27+
interface SerializedServerProperties {
28+
_id: string;
29+
pid?: number;
30+
port?: number;
31+
dbPath?: string;
32+
startTime: string;
33+
hasInsertedMetadataCollEntry: boolean;
34+
}
35+
2736
export class MongoServer {
37+
private uuid: string = uuid();
2838
private buildInfo?: Document;
2939
private childProcess?: ChildProcess;
3040
private pid?: number;
3141
private port?: number;
3242
private dbPath?: string;
3343
private closing = false;
44+
private startTime = new Date().toISOString();
45+
private hasInsertedMetadataCollEntry = false;
3446

3547
private constructor() {
3648
/* see .start() */
3749
}
3850

39-
serialize(): unknown /* JSON-serializable */ {
51+
serialize(): SerializedServerProperties {
4052
return {
53+
_id: this.uuid,
4154
pid: this.pid,
4255
port: this.port,
4356
dbPath: this.dbPath,
57+
startTime: this.startTime,
58+
hasInsertedMetadataCollEntry: this.hasInsertedMetadataCollEntry,
4459
};
4560
}
4661

47-
static async deserialize(serialized: any): Promise<MongoServer> {
62+
static async deserialize(
63+
serialized: SerializedServerProperties,
64+
): Promise<MongoServer> {
4865
const srv = new MongoServer();
49-
srv.pid = serialized.pid;
66+
srv.uuid = serialized._id;
5067
srv.port = serialized.port;
51-
srv.dbPath = serialized.dbPath;
52-
await srv._populateBuildInfo();
68+
srv.closing = !!(await srv._populateBuildInfo('restore-check'));
69+
if (!srv.closing) {
70+
srv.pid = serialized.pid;
71+
srv.dbPath = serialized.dbPath;
72+
}
5373
return srv;
5474
}
5575

@@ -116,7 +136,7 @@ export class MongoServer {
116136
const srv = new MongoServer();
117137

118138
if (!options.docker) {
119-
const dbPath = path.join(options.tmpDir, `db-${uuid()}`);
139+
const dbPath = path.join(options.tmpDir, `db-${srv.uuid}`);
120140
await fs.mkdir(dbPath, { recursive: true });
121141
srv.dbPath = dbPath;
122142
}
@@ -234,7 +254,10 @@ export class MongoServer {
234254
logEntryStream.resume();
235255

236256
srv.port = port;
237-
await srv._populateBuildInfo();
257+
const buildInfoError = await srv._populateBuildInfo('insert-new');
258+
if (buildInfoError) {
259+
debug('failed to get buildInfo', buildInfoError);
260+
}
238261
} catch (err) {
239262
await srv.close();
240263
throw err;
@@ -270,24 +293,83 @@ export class MongoServer {
270293
this.dbPath = undefined;
271294
}
272295

273-
private async _populateBuildInfo(): Promise<void> {
274-
if (this.buildInfo?.version) return;
275-
this.buildInfo = await this.withClient(
276-
async (client) => await client.db('admin').command({ buildInfo: 1 }),
277-
);
296+
private async _ensureMatchingMetadataColl(
297+
client: MongoClient,
298+
mode: 'insert-new' | 'restore-check',
299+
): Promise<void> {
300+
const hello = await client.db('admin').command({ hello: 1 });
301+
const isMongoS = hello.msg === 'isdbgrid';
302+
const insertedInfo = pick(this.serialize(), [
303+
'_id',
304+
'pid',
305+
'port',
306+
'dbPath',
307+
'startTime',
308+
]);
309+
const runnerColl = client
310+
.db(isMongoS ? 'config' : 'local')
311+
.collection<
312+
Omit<SerializedServerProperties, 'hasInsertedMetadataCollEntry'>
313+
>('mongodbrunner');
314+
debug('ensuring metadata collection entry', insertedInfo, { isMongoS });
315+
if (mode === 'insert-new') {
316+
await runnerColl.insertOne(insertedInfo);
317+
debug('inserted metadata collection entry', insertedInfo);
318+
this.hasInsertedMetadataCollEntry = true;
319+
} else {
320+
if (!this.hasInsertedMetadataCollEntry) {
321+
debug(
322+
'skipping metadata collection match check as we never inserted metadata',
323+
);
324+
return;
325+
}
326+
const match = await runnerColl.findOne();
327+
debug('read metadata collection entry', insertedInfo, match);
328+
if (!match) {
329+
throw new Error(
330+
'Cannot find mongodbrunner entry, assuming that this instance was not started by mongodb-runner',
331+
);
332+
}
333+
if (match._id !== insertedInfo._id) {
334+
throw new Error(
335+
`Mismatched mongodbrunner entry: ${JSON.stringify(match)} !== ${JSON.stringify(insertedInfo)}`,
336+
);
337+
}
338+
}
339+
}
340+
341+
private async _populateBuildInfo(
342+
mode: 'insert-new' | 'restore-check',
343+
): Promise<Error | null> {
344+
try {
345+
// directConnection + retryWrites let us write to `local` db on secondaries
346+
const clientOpts = { retryWrites: false };
347+
this.buildInfo = await this.withClient(async (client) => {
348+
// Insert the metadata entry, except if we're a freshly started mongos
349+
// (which does not have its own storage to persist)
350+
await this._ensureMatchingMetadataColl(client, mode);
351+
return await client.db('admin').command({ buildInfo: 1 });
352+
}, clientOpts);
353+
} catch (err) {
354+
debug('failed to get buildInfo, treating as closed server', err);
355+
return err as Error;
356+
}
278357
debug(
279358
'got server build info through client',
280359
this.serverVersion,
281360
this.serverVariant,
282361
);
362+
return null;
283363
}
284364

285365
async withClient<Fn extends (client: MongoClient) => any>(
286366
fn: Fn,
367+
clientOptions: MongoClientOptions = {},
287368
): Promise<ReturnType<Fn>> {
288-
const client = await MongoClient.connect(
289-
`mongodb://${this.hostport}/?directConnection=true`,
290-
);
369+
const client = await MongoClient.connect(`mongodb://${this.hostport}/`, {
370+
directConnection: true,
371+
...clientOptions,
372+
});
291373
try {
292374
return await fn(client);
293375
} finally {

packages/mongodb-runner/src/runner-helpers.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,9 @@ export async function stop(argv: {
8484
id?: string;
8585
all?: boolean;
8686
}) {
87-
const toStop: Array<StoredInstance> = [];
88-
for await (const instance of instances(argv)) {
89-
if (instance.id === argv.id || argv.all) toStop.push(instance);
90-
}
91-
await Promise.all(
92-
toStop.map(async ({ filepath, serialized }) => {
93-
await (await MongoCluster.deserialize(serialized)).close();
94-
await fs.rm(filepath);
95-
}),
96-
);
87+
await parallelForEach(instances(argv), async (instance: StoredInstance) => {
88+
if (instance.id !== argv.id && !argv.all) return;
89+
await (await MongoCluster.deserialize(instance.serialized)).close();
90+
await fs.rm(instance.filepath);
91+
});
9792
}

packages/mongodb-runner/src/util.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,16 @@ export async function parallelForEach<T>(
2828

2929
return await Promise.allSettled(result);
3030
}
31+
32+
export function pick<T extends object, K extends keyof T>(
33+
obj: T,
34+
keys: K[],
35+
): Pick<T, K> {
36+
const ret: Partial<Pick<T, K>> = {};
37+
for (const key of Object.keys(obj) as K[]) {
38+
if (keys.includes(key)) {
39+
ret[key] = obj[key];
40+
}
41+
}
42+
return ret as Pick<T, K>;
43+
}

0 commit comments

Comments
 (0)