195 |
281 |
|
} |
196 |
282 |
|
|
197 |
283 |
|
/** |
198 |
|
- |
* Returns a mongodb: URI to connect to a given database. |
|
284 |
+ |
* Returns an mongodb URI that is setup with all replSet servers |
199 |
285 |
|
* @param otherDb use a different database than what was set on creation? |
|
286 |
+ |
* @throws if state is not "running" |
|
287 |
+ |
* @throws if an server doesnt have "instanceInfo.port" defined |
200 |
288 |
|
*/ |
201 |
|
- |
async getUri(otherDb?: string | boolean): Promise<string> { |
202 |
|
- |
if (this._state === 'init') { |
203 |
|
- |
await this._waitForPrimary(); |
204 |
|
- |
} |
205 |
|
- |
if (this._state !== 'running') { |
206 |
|
- |
throw new Error('Replica Set is not running. Use debug for more info.'); |
207 |
|
- |
} |
208 |
|
- |
let dbName: string; |
209 |
|
- |
if (otherDb) { |
210 |
|
- |
dbName = typeof otherDb === 'string' ? otherDb : generateDbName(); |
211 |
|
- |
} else { |
212 |
|
- |
dbName = this.opts.replSet.dbName; |
|
289 |
+ |
getUri(otherDb?: string | boolean): string { |
|
290 |
+ |
log('getUri:', this._state); |
|
291 |
+ |
switch (this._state) { |
|
292 |
+ |
case MongoMemoryReplSetStates.running: |
|
293 |
+ |
case MongoMemoryReplSetStates.init: |
|
294 |
+ |
break; |
|
295 |
+ |
case MongoMemoryReplSetStates.stopped: |
|
296 |
+ |
default: |
|
297 |
+ |
throw new Error('Replica Set is not running. Use debug for more info.'); |
213 |
298 |
|
} |
|
299 |
+ |
|
|
300 |
+ |
const dbName: string = isNullOrUndefined(otherDb) |
|
301 |
+ |
? this._replSetOpts.dbName |
|
302 |
+ |
: typeof otherDb === 'string' |
|
303 |
+ |
? otherDb |
|
304 |
+ |
: generateDbName(); |
214 |
305 |
|
const ports = this.servers.map((s) => { |
215 |
306 |
|
const port = s.instanceInfo?.port; |
216 |
307 |
|
assertion(!isNullOrUndefined(port), new Error('Instance Port is undefined!')); |
217 |
308 |
|
return port; |
218 |
309 |
|
}); |
219 |
310 |
|
const hosts = ports.map((port) => `127.0.0.1:${port}`).join(','); |
220 |
|
- |
return `mongodb://${hosts}/${dbName}?replicaSet=${this.opts.replSet.name}`; |
|
311 |
+ |
return `mongodb://${hosts}/${dbName}?replicaSet=${this._replSetOpts.name}`; |
221 |
312 |
|
} |
222 |
313 |
|
|
223 |
314 |
|
/** |
224 |
315 |
|
* Start underlying `mongod` instances. |
|
316 |
+ |
* @throws if state is already "running" |
225 |
317 |
|
*/ |
226 |
318 |
|
async start(): Promise<void> { |
227 |
319 |
|
log('start'); |
228 |
|
- |
if (this._state !== 'stopped') { |
229 |
|
- |
throw new Error(`Already in 'init' or 'running' state. Use debug for more info.`); |
|
320 |
+ |
switch (this._state) { |
|
321 |
+ |
// case MongoMemoryReplSetStateEnum.init: |
|
322 |
+ |
// return this.waitUntilRunning(); |
|
323 |
+ |
case MongoMemoryReplSetStates.stopped: |
|
324 |
+ |
break; |
|
325 |
+ |
case MongoMemoryReplSetStates.running: |
|
326 |
+ |
default: |
|
327 |
+ |
throw new Error('Already in "init" or "running" state. Use debug for more info.'); |
230 |
328 |
|
} |
231 |
|
- |
this.emit((this._state = 'init')); |
|
329 |
+ |
this.stateChange(MongoMemoryReplSetStates.init); // this needs to be executed before "setImmediate" |
|
330 |
+ |
await ensureAsync(); |
232 |
331 |
|
log('init'); |
233 |
|
- |
// Any servers defined within `opts.instanceOpts` should be started first as |
|
332 |
+ |
await this.initAllServers(); |
|
333 |
+ |
await this._initReplSet(); |
|
334 |
+ |
process.once('beforeExit', this.stop); |
|
335 |
+ |
} |
|
336 |
+ |
|
|
337 |
+ |
protected async initAllServers(): Promise<void> { |
|
338 |
+ |
this.stateChange(MongoMemoryReplSetStates.init); |
|
339 |
+ |
|
|
340 |
+ |
if (this.servers.length > 0) { |
|
341 |
+ |
log('initAllServers: lenght of "servers" is higher than 0, starting existing servers'); |
|
342 |
+ |
await Promise.all(this.servers.map((s) => s.start(true))); |
|
343 |
+ |
|
|
344 |
+ |
return; |
|
345 |
+ |
} |
|
346 |
+ |
|
|
347 |
+ |
// Any servers defined within `_instanceOpts` should be started first as |
234 |
348 |
|
// the user could have specified a `dbPath` in which case we would want to perform |
235 |
349 |
|
// the `replSetInitiate` command against that server. |
236 |
|
- |
const servers = this.opts.instanceOpts.map((opts) => { |
237 |
|
- |
log(' starting server from instanceOpts:', opts, '...'); |
238 |
|
- |
return this._initServer(this.getInstanceOpts(opts)); |
|
350 |
+ |
this._instanceOpts.forEach((opts) => { |
|
351 |
+ |
log(` starting server from instanceOpts (count: ${this.servers.length + 1}):`, opts); |
|
352 |
+ |
this.servers.push(this._initServer(this.getInstanceOpts(opts))); |
239 |
353 |
|
}); |
240 |
|
- |
const cnt = this.opts.replSet.count || 1; |
241 |
|
- |
while (servers.length < cnt) { |
242 |
|
- |
log(` starting server ${servers.length + 1} of ${cnt}...`); |
243 |
|
- |
const server = this._initServer(this.getInstanceOpts({})); |
244 |
|
- |
servers.push(server); |
|
354 |
+ |
while (this.servers.length < this._replSetOpts.count) { |
|
355 |
+ |
log(` starting server ${this.servers.length + 1} of ${this._replSetOpts.count}`); |
|
356 |
+ |
this.servers.push(this._initServer(this.getInstanceOpts())); |
245 |
357 |
|
} |
|
358 |
+ |
|
246 |
359 |
|
// ensures all servers are listening for connection |
247 |
|
- |
await Promise.all(servers.map((s) => s.start())); |
248 |
|
- |
this.servers = servers; |
249 |
|
- |
await this._initReplSet(); |
|
360 |
+ |
await Promise.all(this.servers.map((s) => s.start())); |
250 |
361 |
|
} |
251 |
362 |
|
|
252 |
363 |
|
/** |
253 |
364 |
|
* Stop the underlying `mongod` instance(s). |
254 |
365 |
|
*/ |
255 |
366 |
|
async stop(): Promise<boolean> { |
256 |
|
- |
if (this._state === 'stopped') { |
|
367 |
+ |
log('stop' + isNullOrUndefined(process.exitCode) ? '' : ': called by process-event'); |
|
368 |
+ |
process.removeListener('beforeExit', this.stop); // many accumulate inside tests |
|
369 |
+ |
if (this._state === MongoMemoryReplSetStates.stopped) { |
257 |
370 |
|
return false; |
258 |
371 |
|
} |
259 |
|
- |
process.removeListener('beforeExit', this.stop); // many accumulate inside tests |
260 |
|
- |
return Promise.all(this.servers.map((s) => s.stop())) |
|
372 |
+ |
return Promise.all(this.servers.map((s) => s.stop(false))) |
261 |
373 |
|
.then(() => { |
262 |
|
- |
this.servers = []; |
263 |
|
- |
this.emit((this._state = 'stopped')); |
|
374 |
+ |
this.stateChange(MongoMemoryReplSetStates.stopped); |
264 |
375 |
|
return true; |
265 |
376 |
|
}) |
266 |
377 |
|
.catch((err) => { |
267 |
|
- |
this.servers = []; |
268 |
378 |
|
log(err); |
269 |
|
- |
this.emit((this._state = 'stopped'), err); |
|
379 |
+ |
this.stateChange(MongoMemoryReplSetStates.stopped, err); |
270 |
380 |
|
return false; |
271 |
381 |
|
}); |
272 |
382 |
|
} |
273 |
383 |
|
|
|
384 |
+ |
/** |
|
385 |
+ |
* Remove the defined dbPath's |
|
386 |
+ |
* This function gets automatically called on process event "beforeExit" (with force being "false") |
|
387 |
+ |
* @param force Remove the dbPath even if it is no "tmpDir" (and re-check if tmpDir actually removed it) |
|
388 |
+ |
* @throws If "state" is not "stopped" |
|
389 |
+ |
* @throws If "instanceInfo" is not defined |
|
390 |
+ |
* @throws If an fs error occured |
|
391 |
+ |
*/ |
|
392 |
+ |
async cleanup(force: boolean = false): Promise<void> { |
|
393 |
+ |
assertion( |
|
394 |
+ |
this._state === MongoMemoryReplSetStates.stopped, |
|
395 |
+ |
new Error('Cannot run cleanup when state is not "stopped"') |
|
396 |
+ |
); |
|
397 |
+ |
log(`cleanup for "${this.servers.length}" servers`); |
|
398 |
+ |
await Promise.all(this.servers.map((s) => s.cleanup(force))); |
|
399 |
+ |
|
|
400 |
+ |
this.servers = []; |
|
401 |
+ |
|
|
402 |
+ |
return; |
|
403 |
+ |
} |
|
404 |
+ |
|
274 |
405 |
|
/** |
275 |
406 |
|
* Wait until all instances are running |
|
407 |
+ |
* @throws if state is "stopped" (cannot wait on something that dosnt start) |
276 |
408 |
|
*/ |
277 |
409 |
|
async waitUntilRunning(): Promise<void> { |
278 |
410 |
|
// TODO: this seems like it dosnt catch if an instance fails, and runs forever |
279 |
|
- |
if (this._state === 'running') { |
280 |
|
- |
return; |
|
411 |
+ |
await ensureAsync(); |
|
412 |
+ |
log('waitUntilRunning:', this._state); |
|
413 |
+ |
switch (this._state) { |
|
414 |
+ |
case MongoMemoryReplSetStates.running: |
|
415 |
+ |
// just return immediatly if the replSet is already running |
|
416 |
+ |
return; |
|
417 |
+ |
case MongoMemoryReplSetStates.init: |
|
418 |
+ |
// wait for event "running" |
|
419 |
+ |
await new Promise((res) => { |
|
420 |
+ |
// the use of "this" here can be done because "on" either binds "this" or uses an arrow function |
|
421 |
+ |
function waitRunning(this: MongoMemoryReplSet, state: MongoMemoryReplSetStates) { |
|
422 |
+ |
// this is because other states can be emitted multiple times (like stopped & init for auth creation) |
|
423 |
+ |
if (state === MongoMemoryReplSetStates.running) { |
|
424 |
+ |
this.removeListener(MongoMemoryReplSetEvents.stateChange, waitRunning); |
|
425 |
+ |
res(); |
|
426 |
+ |
} |
|
427 |
+ |
} |
|
428 |
+ |
this.on(MongoMemoryReplSetEvents.stateChange, waitRunning); |
|
429 |
+ |
}); |
|
430 |
+ |
return; |
|
431 |
+ |
case MongoMemoryReplSetStates.stopped: |
|
432 |
+ |
default: |
|
433 |
+ |
// throw an error if not "running" or "init" |
|
434 |
+ |
throw new Error( |
|
435 |
+ |
'State is not "running" or "init" - cannot wait on something that dosnt start' |
|
436 |
+ |
); |
281 |
437 |
|
} |
282 |
|
- |
await new Promise((resolve) => this.once('running', () => resolve())); |
283 |
438 |
|
} |
284 |
439 |
|
|
285 |
440 |
|
/** |
286 |
441 |
|
* Connects to the first server from the list of servers and issues the `replSetInitiate` |
287 |
442 |
|
* command passing in a new replica set configuration object. |
|
443 |
+ |
* @throws if state is not "init" |
|
444 |
+ |
* @throws if "servers.length" is not 1 or above |
|
445 |
+ |
* @throws if package "mongodb" is not installed |
288 |
446 |
|
*/ |
289 |
|
- |
async _initReplSet(): Promise<void> { |
290 |
|
- |
if (this._state !== 'init') { |
291 |
|
- |
throw new Error('Not in init phase.'); |
292 |
|
- |
} |
293 |
|
- |
log('Initializing replica set.'); |
294 |
|
- |
if (!this.servers.length) { |
295 |
|
- |
throw new Error('One or more servers are required.'); |
296 |
|
- |
} |
|
447 |
+ |
protected async _initReplSet(): Promise<void> { |
|
448 |
+ |
log('_initReplSet'); |
|
449 |
+ |
assertion(this._state === MongoMemoryReplSetStates.init, new Error('Not in init phase.')); |
|
450 |
+ |
assertion(this.servers.length > 0, new Error('One or more servers are required.')); |
297 |
451 |
|
const uris = this.servers.map((server) => server.getUri()); |
298 |
452 |
|
|
299 |
|
- |
let MongoClient: typeof mongodb.MongoClient; |
300 |
|
- |
try { |
301 |
|
- |
MongoClient = (await import('mongodb')).MongoClient; |
302 |
|
- |
} catch (e) { |
303 |
|
- |
throw new Error( |
304 |
|
- |
`You need to install "mongodb" package. It's required for checking ReplicaSet state.` |
305 |
|
- |
); |
306 |
|
- |
} |
307 |
|
- |
|
308 |
|
- |
const conn: mongodb.MongoClient = await MongoClient.connect(uris[0], { |
|
453 |
+ |
let con: MongoClient = await MongoClient.connect(uris[0], { |
309 |
454 |
|
useNewUrlParser: true, |
310 |
455 |
|
useUnifiedTopology: true, |
311 |
456 |
|
}); |
312 |
457 |
|
|
313 |
458 |
|
try { |
314 |
|
- |
const db = await conn.db(this.opts.replSet.dbName); |
|
459 |
+ |
let adminDb = con.db('admin'); |
315 |
460 |
|
|
316 |
|
- |
// MongoClient HACK which helps to avoid the following error: |
317 |
|
- |
// "RangeError: Maximum call stack size exceeded" |
318 |
|
- |
// (db as any).topology.shouldCheckForSessionSupport = () => false; // TODO: remove after 1.1.2021 if no issues arise |
319 |
|
- |
|
320 |
|
- |
/** reference to "db.admin()" */ |
321 |
|
- |
const admin = db.admin(); |
322 |
461 |
|
const members = uris.map((uri, idx) => ({ _id: idx, host: getHost(uri) })); |
323 |
462 |
|
const rsConfig = { |
324 |
|
- |
_id: this.opts.replSet.name, |
|
463 |
+ |
_id: this._replSetOpts.name, |
325 |
464 |
|
members, |
326 |
465 |
|
settings: { |
327 |
466 |
|
electionTimeoutMillis: 500, |
328 |
|
- |
...(this.opts.replSet.configSettings || {}), |
|
467 |
+ |
...this._replSetOpts.configSettings, |
329 |
468 |
|
}, |
330 |
469 |
|
}; |
331 |
470 |
|
try { |
332 |
|
- |
await admin.command({ replSetInitiate: rsConfig }); |
|
471 |
+ |
await adminDb.command({ replSetInitiate: rsConfig }); |
|
472 |
+ |
|
|
473 |
+ |
if (typeof this._replSetOpts.auth === 'object') { |
|
474 |
+ |
log('_initReplSet: "this._replSetOpts.auth" is an object'); |
|
475 |
+ |
|
|
476 |
+ |
await this._waitForPrimary(); |
|
477 |
+ |
|
|
478 |
+ |
const primary = this.servers.find( |
|
479 |
+ |
(server) => server.instanceInfo?.instance.isInstancePrimary |
|
480 |
+ |
); |
|
481 |
+ |
assertion(!isNullOrUndefined(primary), new Error('No Primary found')); |
|
482 |
+ |
assertion( |
|
483 |
+ |
!isNullOrUndefined(primary.instanceInfo), |
|
484 |
+ |
new Error('Primary dosnt have an "instanceInfo" defined') |
|
485 |
+ |
); |
|
486 |
+ |
|
|
487 |
+ |
await primary.createAuth(primary.instanceInfo); |
|
488 |
+ |
this._ranCreateAuth = true; |
|
489 |
+ |
|
|
490 |
+ |
if (primary.opts.instance?.storageEngine !== 'ephemeralForTest') { |
|
491 |
+ |
log('_initReplSet: closing connection for restart'); |
|
492 |
+ |
await con.close(); // close connection in preparation for "stop" |
|
493 |
+ |
await this.stop(); // stop all servers for enabling auth |
|
494 |
+ |
log('_initReplSet: starting all server again with auth'); |
|
495 |
+ |
await this.initAllServers(); // start all servers again with "auth" enabled |
|
496 |
+ |
|
|
497 |
+ |
con = await MongoClient.connect(this.getUri('admin'), { |
|
498 |
+ |
useNewUrlParser: true, |
|
499 |
+ |
useUnifiedTopology: true, |
|
500 |
+ |
authSource: 'admin', |
|
501 |
+ |
authMechanism: 'SCRAM-SHA-256', |
|
502 |
+ |
auth: { |
|
503 |
+ |
user: this._replSetOpts.auth.customRootName as string, // cast because these are existing |
|
504 |
+ |
password: this._replSetOpts.auth.customRootPwd as string, |
|
505 |
+ |
}, |
|
506 |
+ |
}); |
|
507 |
+ |
adminDb = con.db('admin'); |
|
508 |
+ |
log('_initReplSet: auth restart finished'); |
|
509 |
+ |
} else { |
|
510 |
+ |
console.warn( |
|
511 |
+ |
'Not Restarting ReplSet for Auth\n' + |
|
512 |
+ |
'Storage engine of current PRIMARY is ephemeralForTest, which does not write data on shutdown, and mongodb does not allow changeing "auth" runtime' |
|
513 |
+ |
); |
|
514 |
+ |
} |
|
515 |
+ |
} |
333 |
516 |
|
} catch (e) { |
334 |
517 |
|
if (e instanceof MongoError && e.errmsg == 'already initialized') { |
335 |
518 |
|
log(`${e.errmsg}: trying to set old config`); |
336 |
|
- |
const { config: oldConfig } = await admin.command({ replSetGetConfig: 1 }); |
|
519 |
+ |
const { config: oldConfig } = await adminDb.command({ replSetGetConfig: 1 }); |
337 |
520 |
|
log('got old config:\n', oldConfig); |
338 |
|
- |
await admin.command({ |
|
521 |
+ |
await adminDb.command({ |
339 |
522 |
|
replSetReconfig: oldConfig, |
340 |
523 |
|
force: true, |
341 |
524 |
|
}); |
342 |
525 |
|
} else { |
343 |
526 |
|
throw e; |
344 |
527 |
|
} |
345 |
528 |
|
} |
346 |
|
- |
log('Waiting for replica set to have a PRIMARY member.'); |
|
529 |
+ |
log('_initReplSet: ReplSet-reConfig finished'); |
347 |
530 |
|
await this._waitForPrimary(); |
348 |
|
- |
this.emit((this._state = 'running')); |
349 |
|
- |
log('running'); |
|
531 |
+ |
this.stateChange(MongoMemoryReplSetStates.running); |
|
532 |
+ |
log('_initReplSet: running'); |
350 |
533 |
|
} finally { |
351 |
|
- |
await conn.close(); |
|
534 |
+ |
await con.close(); |
352 |
535 |
|
} |
353 |
536 |
|
} |
354 |
537 |
|
|
355 |
538 |
|
/** |
356 |
539 |
|
* Create the one Instance (without starting them) |
357 |
540 |
|
* @param instanceOpts Instance Options to use for this instance |
358 |
541 |
|
*/ |
359 |
|
- |
_initServer(instanceOpts: MongoMemoryInstancePropT): MongoMemoryServer { |
360 |
|
- |
const serverOpts: MongoMemoryServerOptsT = { |
361 |
|
- |
binary: this.opts.binary, |
|
542 |
+ |
protected _initServer(instanceOpts: MongoMemoryInstanceProp): MongoMemoryServer { |
|
543 |
+ |
const serverOpts: MongoMemoryServerOpts = { |
|
544 |
+ |
binary: this._binaryOpts, |
362 |
545 |
|
instance: instanceOpts, |
363 |
|
- |
spawn: this.opts.replSet.spawn, |
|
546 |
+ |
spawn: this._replSetOpts.spawn, |
|
547 |
+ |
auth: typeof this.replSetOpts.auth === 'object' ? this.replSetOpts.auth : undefined, |
364 |
548 |
|
}; |
365 |
549 |
|
const server = new MongoMemoryServer(serverOpts); |
366 |
550 |
|
return server; |
04b0ee9
f6ca651
61685e0
9fc358b
7970fbb
c19d216
139d3fd
251c7ed
bfd5441
c02e21d
644a335
718aed7
6279619
3c7d102
301b0f2
66dc223
fede066
1e2f2fc
7a0d5a5
3d64705
561c883
846f969
19ae688
28bcf5b
b3ebac2
8f65696
18c77e2
188d333
e2ae879
e5b906d
4fd1ede
f057ea7
a828506
4214aa5
662a69b
96c30de
6c118a9
d5bf77a
58b55b6
d05f529
fdf2d09
7ee646c
f3df9c8
5e320ca
2bad87c
23cdc65
8dffa64
78a78cd
74d30a0
deb0098
5e377fa
c888b95
52c9dcc
d41f042
dc88399
bccac07
#391
5a2c397
7859d6c
d29c2db
89c8af6
7107c59
e575e0d
2559117
6710a7f
d78b538
8833a93
5166d9a
280d579
60646eb
f394b7e
2482404
bd038e8
0fc27d6
1ad5bdc
179bdbb
8e6e312
a335500
3de371e
e12396e
2b14552
017239c
fb958ae
335780e
3ba31e2
c2311cb
6b60a71
150494e
13f3f1d
18b9a58
d0d62e2
4c32f45
f46d113
e954806
07937e2
c701f09
6ebafbd
65135a8
415fc8f
138e21d
90ed578
6dcb12a
3c3d6fb
dbe844e
d3dff26
f8d3803
019b118
127102e
8b8a609
eced979
4ab4aca
385e7e2
236c7c3
c686475
55f3d0f
412e615
9add2bc
a3a911f
68c78d6
df1af0c
765b5b1
18428d5
0202e8f
27f6215
971b02d
e17762d
2775b4f
9b17925
e3d4678
3d80724
aca8262
2a9aab7
23e6eaa
8943ced
55fc2ff
adcbdcf
2e9faec
de41060
a1c9264
cc5af05
cc053c7
869d697