UNPKG

ipsambeatae

Version:

Shared dependencies of Compass, the MongoDB extension for VSCode and MongoSH

241 lines (216 loc) 6.92 kB
import type { MongoServerOptions } from './mongoserver'; import { MongoServer } from './mongoserver'; import type { DownloadOptions } from '@mongodb-js/mongodb-downloader'; import { downloadMongoDb } from '@mongodb-js/mongodb-downloader'; import { MongoClient } from 'mongodb'; import { sleep, range, uuid, debug } from './util'; export interface MongoClusterOptions extends Pick< MongoServerOptions, 'logDir' | 'tmpDir' | 'args' | 'binDir' | 'docker' > { topology: 'standalone' | 'replset' | 'sharded'; arbiters?: number; secondaries?: number; shards?: number; version?: string; downloadOptions?: DownloadOptions; } export class MongoCluster { private topology: MongoClusterOptions['topology'] = 'standalone'; private replSetName?: string; private servers: MongoServer[] = []; // mongod/mongos private shards: MongoCluster[] = []; // replsets private constructor() { /* see .start() */ } serialize(): unknown /* JSON-serializable */ { return { topology: this.topology, replSetName: this.replSetName, servers: this.servers.map((srv) => srv.serialize()), shards: this.shards.map((shard) => shard.serialize()), }; } isClosed(): boolean { return this.servers.length === 0 && this.shards.length === 0; } static async deserialize(serialized: any): Promise<MongoCluster> { const cluster = new MongoCluster(); cluster.topology = serialized.topology; cluster.replSetName = serialized.replSetName; cluster.servers = await Promise.all( serialized.servers.map((srv: any) => MongoServer.deserialize(srv)) ); cluster.shards = await Promise.all( serialized.shards.map((shard: any) => MongoCluster.deserialize(shard)) ); return cluster; } get hostport(): string { return this.servers.map((srv) => srv.hostport).join(','); } get connectionString(): string { return `mongodb://${this.hostport}/${ this.replSetName ? `?replicaSet=${this.replSetName}` : '' }`; } get serverVersion(): string { return this.servers[0].serverVersion; } get serverVariant(): 'enterprise' | 'community' { return this.servers[0].serverVariant; } static async start({ ...options }: MongoClusterOptions): Promise<MongoCluster> { const cluster = new MongoCluster(); cluster.topology = options.topology; if (!options.binDir) { options.binDir = await downloadMongoDb( options.tmpDir, options.version, options.downloadOptions ); } if (options.topology === 'standalone') { cluster.servers.push( await MongoServer.start({ ...options, binary: 'mongod', }) ); } else if (options.topology === 'replset') { const { secondaries = 2, arbiters = 0 } = options; const args = [...(options.args ?? [])]; let replSetName: string; if (!args.includes('--replSet')) { replSetName = `replSet-${uuid()}`; args.push('--replSet', replSetName); } else { replSetName = args[args.indexOf('--replSet') + 1]; } const primaryArgs = [...args]; debug('Starting primary', primaryArgs); const primary = await MongoServer.start({ ...options, args: primaryArgs, binary: 'mongod', }); cluster.servers.push(primary); if (args.includes('--port')) { args.splice(args.indexOf('--port') + 1, 1, '0'); } debug('Starting secondaries and arbiters', { secondaries, arbiters, args, }); cluster.servers.push( ...(await Promise.all( range(secondaries + arbiters).map(() => MongoServer.start({ ...options, args, binary: 'mongod', }) ) )) ); await primary.withClient(async (client) => { debug('Running rs.initiate'); const rsConf = { _id: replSetName, configsvr: args.includes('--configsvr'), members: cluster.servers.map((srv, i) => { return { _id: i, host: srv.hostport, arbiterOnly: i > 1 + secondaries, priority: i === 0 ? 1 : 0, }; }), }; await client.db('admin').command({ replSetInitiate: rsConf, }); for (let i = 0; i < 60; i++) { const status = await client.db('admin').command({ replSetGetStatus: 1, }); if ( status.members.some((member: any) => member.stateStr === 'PRIMARY') ) { debug('rs.status indicated primary for replset', status.set); cluster.replSetName = status.set; break; } debug('rs.status did not include primary, waiting...'); await sleep(1000); } }); } else if (options.topology === 'sharded') { const { shards = 3 } = options; const shardArgs = [...(options.args ?? [])]; if (shardArgs.includes('--port')) { shardArgs.splice(shardArgs.indexOf('--port') + 1, 1, '0'); } debug('starting config server and shard servers', shardArgs); const [configsvr, ...shardsvrs] = await Promise.all( range(shards + 1).map((i) => MongoCluster.start({ ...options, args: [...shardArgs, i > 0 ? '--shardsvr' : '--configsvr'], topology: 'replset', }) ) ); cluster.shards.push(configsvr, ...shardsvrs); debug('starting mongos'); const mongos = await MongoServer.start({ ...options, binary: 'mongos', args: [ ...(options.args ?? []), '--configdb', `${configsvr.replSetName!}/${configsvr.hostport}`, ], }); cluster.servers.push(mongos); await mongos.withClient(async (client) => { for (const shard of shardsvrs) { const shardSpec = `${shard.replSetName!}/${shard.hostport}`; debug('adding shard', shardSpec); await client.db('admin').command({ addShard: shardSpec, }); } debug('added shards'); }); } return cluster; } async close(): Promise<void> { await Promise.all( [...this.servers, ...this.shards].map((closable) => closable.close()) ); this.servers = []; this.shards = []; } async withClient<Fn extends (client: MongoClient) => any>( fn: Fn ): Promise<ReturnType<Fn>> { const client = await MongoClient.connect(this.connectionString); try { return await fn(client); } finally { await client.close(true); } } ref(): void { for (const child of [...this.servers, ...this.shards]) child.ref(); } unref(): void { for (const child of [...this.servers, ...this.shards]) child.unref(); } }