@@ -9,11 +9,11 @@ import {
99 isFailureToSetupListener ,
1010} from './mongologreader' ;
1111import { Readable } from 'stream' ;
12- import type { Document } from 'mongodb' ;
12+ import type { Document , MongoClientOptions } from 'mongodb' ;
1313import { MongoClient } from 'mongodb' ;
1414import path from 'path' ;
1515import { once } from 'events' ;
16- import { uuid , debug } from './util' ;
16+ import { uuid , debug , pick } from './util' ;
1717
1818export 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+
2736export 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 {
0 commit comments