@@ -9,11 +9,11 @@ import {
9
9
isFailureToSetupListener ,
10
10
} from './mongologreader' ;
11
11
import { Readable } from 'stream' ;
12
- import type { Document } from 'mongodb' ;
12
+ import type { Document , MongoClientOptions } from 'mongodb' ;
13
13
import { MongoClient } from 'mongodb' ;
14
14
import path from 'path' ;
15
15
import { once } from 'events' ;
16
- import { uuid , debug } from './util' ;
16
+ import { uuid , debug , pick } from './util' ;
17
17
18
18
export interface MongoServerOptions {
19
19
binDir ?: string ;
@@ -24,32 +24,52 @@ export interface MongoServerOptions {
24
24
docker ?: string | string [ ] ; // Image or docker options
25
25
}
26
26
27
+ interface SerializedServerProperties {
28
+ _id : string ;
29
+ pid ?: number ;
30
+ port ?: number ;
31
+ dbPath ?: string ;
32
+ startTime : string ;
33
+ hasInsertedMetadataCollEntry : boolean ;
34
+ }
35
+
27
36
export class MongoServer {
37
+ private uuid : string = uuid ( ) ;
28
38
private buildInfo ?: Document ;
29
39
private childProcess ?: ChildProcess ;
30
40
private pid ?: number ;
31
41
private port ?: number ;
32
42
private dbPath ?: string ;
33
43
private closing = false ;
44
+ private startTime = new Date ( ) . toISOString ( ) ;
45
+ private hasInsertedMetadataCollEntry = false ;
34
46
35
47
private constructor ( ) {
36
48
/* see .start() */
37
49
}
38
50
39
- serialize ( ) : unknown /* JSON-serializable */ {
51
+ serialize ( ) : SerializedServerProperties {
40
52
return {
53
+ _id : this . uuid ,
41
54
pid : this . pid ,
42
55
port : this . port ,
43
56
dbPath : this . dbPath ,
57
+ startTime : this . startTime ,
58
+ hasInsertedMetadataCollEntry : this . hasInsertedMetadataCollEntry ,
44
59
} ;
45
60
}
46
61
47
- static async deserialize ( serialized : any ) : Promise < MongoServer > {
62
+ static async deserialize (
63
+ serialized : SerializedServerProperties ,
64
+ ) : Promise < MongoServer > {
48
65
const srv = new MongoServer ( ) ;
49
- srv . pid = serialized . pid ;
66
+ srv . uuid = serialized . _id ;
50
67
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
+ }
53
73
return srv ;
54
74
}
55
75
@@ -116,7 +136,7 @@ export class MongoServer {
116
136
const srv = new MongoServer ( ) ;
117
137
118
138
if ( ! options . docker ) {
119
- const dbPath = path . join ( options . tmpDir , `db-${ uuid ( ) } ` ) ;
139
+ const dbPath = path . join ( options . tmpDir , `db-${ srv . uuid } ` ) ;
120
140
await fs . mkdir ( dbPath , { recursive : true } ) ;
121
141
srv . dbPath = dbPath ;
122
142
}
@@ -234,7 +254,10 @@ export class MongoServer {
234
254
logEntryStream . resume ( ) ;
235
255
236
256
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
+ }
238
261
} catch ( err ) {
239
262
await srv . close ( ) ;
240
263
throw err ;
@@ -270,24 +293,83 @@ export class MongoServer {
270
293
this . dbPath = undefined ;
271
294
}
272
295
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
+ }
278
357
debug (
279
358
'got server build info through client' ,
280
359
this . serverVersion ,
281
360
this . serverVariant ,
282
361
) ;
362
+ return null ;
283
363
}
284
364
285
365
async withClient < Fn extends ( client : MongoClient ) => any > (
286
366
fn : Fn ,
367
+ clientOptions : MongoClientOptions = { } ,
287
368
) : 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
+ } ) ;
291
373
try {
292
374
return await fn ( client ) ;
293
375
} finally {
0 commit comments