From e91cd2909b796bd3255dbb15beece36ce20fd893 Mon Sep 17 00:00:00 2001 From: Gagik Amaryan Date: Fri, 25 Oct 2024 09:01:17 +0200 Subject: [PATCH] feat(shell-api): add shardedDataDistribution to sh.status() MONGOSH-1326 (#2214) --- packages/shell-api/src/helpers.spec.ts | 47 ++++++++- packages/shell-api/src/helpers.ts | 136 +++++++++++++++++++++---- packages/shell-api/src/shard.spec.ts | 83 +++++++++++++-- packages/shell-api/src/shard.ts | 3 +- 4 files changed, 234 insertions(+), 35 deletions(-) diff --git a/packages/shell-api/src/helpers.spec.ts b/packages/shell-api/src/helpers.spec.ts index 381c97b03..f39227fe0 100644 --- a/packages/shell-api/src/helpers.spec.ts +++ b/packages/shell-api/src/helpers.spec.ts @@ -1,3 +1,4 @@ +import type { ShardedDataDistribution } from './helpers'; import { assertArgsDefinedType, coerceToJSNumber, @@ -19,6 +20,7 @@ import sinon from 'ts-sinon'; import chai, { expect } from 'chai'; import { EventEmitter } from 'events'; import sinonChai from 'sinon-chai'; +import { stub } from 'sinon'; chai.use(sinonChai); const fakeConfigDb = makeFakeConfigDatabase( @@ -133,6 +135,21 @@ describe('getPrintableShardStatus', function () { let serviceProvider: ServiceProvider; let inBalancerRound = false; + const mockedShardedDataDistribution: ShardedDataDistribution = [ + { + ns: 'test.ns', + shards: [ + { + shardName: 'test', + numOrphanedDocs: 1, + numOwnedDocuments: 5, + orphanedSizeBytes: 20, + ownedSizeBytes: 80, + }, + ], + }, + ]; + beforeEach(async function () { serviceProvider = await NodeDriverServiceProvider.connect( await testServer.connectionString(), @@ -186,6 +203,20 @@ describe('getPrintableShardStatus', function () { }); it('returns an object with sharding information', async function () { + const mockedAdminDb = { + aggregate: stub() + .withArgs([{ $shardedDataDistribution: {} }]) + .resolves({ + toArray: stub().resolves(mockedShardedDataDistribution), + }), + }; + const getSiblingDB = stub(); + getSiblingDB.withArgs('admin').returns(mockedAdminDb); + getSiblingDB.withArgs('config').returns(configDatabase); + + configDatabase.getSiblingDB = getSiblingDB; + configDatabase._maybeCachedHello = stub().returns({ msg: 'isdbgrid' }); + const status = await getPrintableShardStatus(configDatabase, false); expect(status.shardingVersion.clusterId).to.be.instanceOf(bson.ObjectId); expect(status.shards.map(({ host }: { host: string }) => host)).to.include( @@ -202,6 +233,10 @@ describe('getPrintableShardStatus', function () { ); expect(status.databases).to.have.lengthOf(1); expect(status.databases[0].database._id).to.equal('config'); + + expect(status.shardedDataDistribution).to.equal( + mockedShardedDataDistribution + ); }); describe('hides all internal deprecated fields in shardingVersion', function () { @@ -214,7 +249,9 @@ describe('getPrintableShardStatus', function () { ]) { it(`does not show ${hiddenField} in shardingVersion`, async function () { const status = await getPrintableShardStatus(configDatabase, false); - expect(status.shardingVersion[hiddenField]).to.equal(undefined); + expect((status.shardingVersion as any)[hiddenField]).to.equal( + undefined + ); }); } }); @@ -235,8 +272,10 @@ describe('getPrintableShardStatus', function () { it('returns an object with verbose sharding information if requested', async function () { const status = await getPrintableShardStatus(configDatabase, true); - expect(status['most recently active mongoses'][0].up).to.be.a('number'); - expect(status['most recently active mongoses'][0].waiting).to.be.a( + expect((status['most recently active mongoses'][0] as any).up).to.be.a( + 'number' + ); + expect((status['most recently active mongoses'][0] as any).waiting).to.be.a( 'boolean' ); }); @@ -281,7 +320,7 @@ describe('getPrintableShardStatus', function () { status.balancer['Collections with active migrations'] ).to.have.lengthOf(1); expect( - status.balancer['Collections with active migrations'].join('') + status.balancer['Collections with active migrations']?.join('') ).to.include('asdf'); }); diff --git a/packages/shell-api/src/helpers.ts b/packages/shell-api/src/helpers.ts index 0b1affb42..180137a60 100644 --- a/packages/shell-api/src/helpers.ts +++ b/packages/shell-api/src/helpers.ts @@ -26,7 +26,7 @@ import type { bson, } from '@mongosh/service-provider-core'; import type { ClientSideFieldLevelEncryptionOptions } from './field-level-encryption'; -import { type AutoEncryptionOptions } from 'mongodb'; +import type { AutoEncryptionOptions, Long, ObjectId, Timestamp } from 'mongodb'; import { shellApiType } from './enums'; import type { AbstractCursor } from './abstract-cursor'; import type ChangeStreamCursor from './change-stream-cursor'; @@ -226,8 +226,8 @@ export function processDigestPassword( export async function getPrintableShardStatus( configDB: Database, verbose: boolean -): Promise { - const result = {} as any; +): Promise { + const result = {} as ShardingStatusResult; // configDB is a DB object that contains the sharding metadata of interest. const mongosColl = configDB.getCollection('mongos'); @@ -259,9 +259,12 @@ export async function getPrintableShardStatus( ); } - result.shardingVersion = version; + result.shardingVersion = version as { + _id: number; + clusterId: ObjectId; + }; - result.shards = shards; + result.shards = shards as ShardingStatusResult['shards']; // (most recently) active mongoses const mongosActiveThresholdMs = 60000; @@ -280,9 +283,8 @@ export async function getPrintableShardStatus( } } - mongosAdjective = `${mongosAdjective} mongoses`; if (mostRecentMongosTime === null) { - result[mongosAdjective] = 'none'; + result[`${mongosAdjective} mongoses`] = 'none'; } else { const recentMongosQuery = { ping: { @@ -295,25 +297,27 @@ export async function getPrintableShardStatus( }; if (verbose) { - result[mongosAdjective] = await (await mongosColl.find(recentMongosQuery)) + result[`${mongosAdjective} mongoses`] = await ( + await mongosColl.find(recentMongosQuery) + ) .sort({ ping: -1 }) .toArray(); } else { - result[mongosAdjective] = ( + result[`${mongosAdjective} mongoses`] = ( (await ( await mongosColl.aggregate([ { $match: recentMongosQuery }, { $group: { _id: '$mongoVersion', num: { $sum: 1 } } }, { $sort: { num: -1 } }, ]) - ).toArray()) as any[] + ).toArray()) as { _id: string; num: number }[] ).map((z: { _id: string; num: number }) => { return { [z._id]: z.num }; }); } } - const balancerRes: Record = {}; + const balancerRes = {} as ShardingStatusResult['balancer']; await Promise.all([ (async (): Promise => { // Is autosplit currently enabled @@ -331,13 +335,13 @@ export async function getPrintableShardStatus( })(), (async (): Promise => { // Is the balancer currently active - let balancerRunning = 'unknown'; + let balancerRunning: 'yes' | 'no' | 'unknown' = 'unknown'; try { const balancerStatus = await configDB.adminCommand({ balancerStatus: 1, }); balancerRunning = balancerStatus.inBalancerRound ? 'yes' : 'no'; - } catch (err: any) { + } catch { // pass, ignore all error messages } balancerRes['Currently running'] = balancerRunning; @@ -364,7 +368,7 @@ export async function getPrintableShardStatus( if (activeLocks?.length > 0) { balancerRes['Collections with active migrations'] = activeLocks.map( (lock) => { - return `${lock._id} started at ${lock.when}`; + return `${lock._id} started at ${lock.when}` as const; } ); } @@ -418,8 +422,23 @@ export async function getPrintableShardStatus( const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); + type MigrationResult = + | { + _id: 'Success'; + count: number; + from: never; + to: never; + } + // Failed migration + | { + _id: string; + count: number; + from: string; + to: string; + }; + // Successful migrations. - let migrations = await ( + let migrations = (await ( await changelogColl.aggregate([ { $match: { @@ -437,11 +456,11 @@ export async function getPrintableShardStatus( }, }, ]) - ).toArray(); + ).toArray()) as MigrationResult[]; // Failed migrations. migrations = migrations.concat( - await ( + (await ( await changelogColl.aggregate([ { $match: { @@ -472,11 +491,12 @@ export async function getPrintableShardStatus( }, }, ]) - ).toArray() + ).toArray()) as MigrationResult[] ); - const migrationsRes: Record = {}; - migrations.forEach((x: any) => { + const migrationsRes: ShardingStatusResult['balancer']['Migration Results for the last 24 hours'] = + {}; + migrations.forEach((x) => { if (x._id === 'Success') { migrationsRes[x.count] = x._id; } else { @@ -500,7 +520,7 @@ export async function getPrintableShardStatus( // All databases in config.databases + those implicitly referenced // by a sharded collection in config.collections // (could become a single pipeline using $unionWith when we drop 4.2 server support) - const [databases, collections] = await Promise.all([ + const [databases, collections, shardedDataDistribution] = await Promise.all([ (async () => await (await configDB.getCollection('databases').find()) .sort({ _id: 1 }) @@ -513,7 +533,22 @@ export async function getPrintableShardStatus( ) .sort({ _id: 1 }) .toArray())(), + (async () => { + try { + // $shardedDataDistribution is available since >= 6.0.3 + const adminDB = configDB.getSiblingDB('admin'); + return (await ( + await adminDB.aggregate([{ $shardedDataDistribution: {} }]) + ).toArray()) as ShardedDataDistribution; + } catch { + // Pass, most likely an older version. + return undefined; + } + })(), ]); + + result.shardedDataDistribution = shardedDataDistribution; + // Special case the config db, since it doesn't have a record in config.databases. databases.push({ _id: 'config', primary: 'config', partitioned: true }); @@ -648,6 +683,65 @@ export async function getPrintableShardStatus( return result; } +export type ShardingStatusResult = { + shardingVersion: { + _id: number; + clusterId: ObjectId; + /** This gets deleted when it is returned from getPrintableShardStatus */ + currentVersion?: number; + }; + shards: { + _id: string; + host: string; + state: number; + tags: string[]; + topologyTime: Timestamp; + replSetConfigVersion: Long; + }[]; + [mongoses: `${string} mongoses`]: + | 'none' + | { + [version: string]: + | number + | { + up: number; + waiting: boolean; + }; + }[]; + autosplit: { + 'Currently enabled': 'yes' | 'no'; + }; + balancer: { + 'Currently enabled': 'yes' | 'no'; + 'Currently running': 'yes' | 'no' | 'unknown'; + 'Failed balancer rounds in last 5 attempts': number; + 'Migration Results for the last 24 hours': + | 'No recent migrations' + | { + [count: number]: + | 'Success' + | `Failed with error '${string}', from ${string} to ${string}`; + }; + 'Balancer active window is set between'?: `${string} and ${string} server local time`; + 'Last reported error'?: string; + 'Time of Reported error'?: string; + 'Collections with active migrations'?: `${string} started at ${string}`[]; + }; + shardedDataDistribution?: ShardedDataDistribution; + databases: { database: Document; collections: Document }[]; +}; + +export type ShardedDataDistribution = { + ns: string; + shards: { + shardName: string; + numOrphanedDocs: number; + numOwnedDocuments: number; + orphanedSizeBytes: number; + ownedSizeBytes: number; + }[]; +}[]; + export async function getConfigDB(db: Database): Promise { const helloResult = await db._maybeCachedHello(); if (helloResult.msg !== 'isdbgrid') { diff --git a/packages/shell-api/src/shard.spec.ts b/packages/shell-api/src/shard.spec.ts index 9910c0496..ad2abce97 100644 --- a/packages/shell-api/src/shard.spec.ts +++ b/packages/shell-api/src/shard.spec.ts @@ -2043,6 +2043,70 @@ describe('Shard', function () { return serviceProvider.close(true); }); + describe('collection.status()', function () { + let db: Database; + + const dbName = 'shard-status-test'; + const ns = `${dbName}.test`; + + beforeEach(async function () { + db = sh._database.getSiblingDB(dbName); + await db.getCollection('test').insertOne({ key: 1 }); + await db.getCollection('test').createIndex({ key: 1 }); + }); + afterEach(async function () { + await db.dropDatabase(); + }); + describe('unsharded collections', function () { + describe('with >= 6.0.3', function () { + skipIfServerVersion(mongos, '< 6.0.3'); + + it('returns shardedDataDistribution as an empty array', async function () { + const status = await sh.status(); + expect(status.value.shardedDataDistribution).deep.equals([]); + }); + }); + + describe('with < 6.0.3', function () { + skipIfServerVersion(mongos, '>= 6.0.3'); + + it('returns shardedDataDistribution as undefined', async function () { + const status = await sh.status(); + expect(status.value.shardedDataDistribution).equals(undefined); + }); + }); + }); + + describe('sharded collections', function () { + beforeEach(async function () { + expect((await sh.enableSharding(dbName)).ok).to.equal(1); + expect( + (await sh.shardCollection(ns, { key: 1 })).collectionsharded + ).to.equal(ns); + }); + + describe('with >= 6.0.3', function () { + skipIfServerVersion(mongos, '< 6.0.3'); + + it('returns correct shardedDataDistribution', async function () { + const status = await sh.status(); + + expect(status.value.shardedDataDistribution?.length).equals(1); + expect(status.value.shardedDataDistribution?.[0].ns).equals(ns); + }); + }); + + describe('with < 6.0.3', function () { + skipIfServerVersion(mongos, '>= 6.0.3'); + + it('returns shardedDataDistribution as undefined', async function () { + const status = await sh.status(); + expect(status.value.shardedDataDistribution).equals(undefined); + }); + }); + }); + }); + describe('sharding info', function () { it('returns the status', async function () { const result = await sh.status(); @@ -2121,7 +2185,7 @@ describe('Shard', function () { expect( (await sh.status()).value.databases.find( (d: Document) => d.database._id === 'test' - ).collections[ns].shardKey + )?.collections[ns].shardKey ).to.deep.equal({ key: 1 }); const db = instanceState.currentDb.getSiblingDB(dbName); @@ -2166,13 +2230,13 @@ describe('Shard', function () { describe('tags', function () { it('creates a zone', async function () { expect((await sh.addShardTag(`${shardId}-1`, 'zone1')).ok).to.equal(1); - expect((await sh.status()).value.shards[1].tags).to.deep.equal([ + expect((await sh.status()).value.shards[1]?.tags).to.deep.equal([ 'zone1', ]); expect((await sh.addShardToZone(`${shardId}-0`, 'zone0')).ok).to.equal( 1 ); - expect((await sh.status()).value.shards[0].tags).to.deep.equal([ + expect((await sh.status()).value.shards[0]?.tags).to.deep.equal([ 'zone0', ]); }); @@ -2241,7 +2305,7 @@ describe('Shard', function () { const tags = (await sh.status()).value.databases.find( (d: Document) => d.database._id === 'test' - ).collections[ns].tags; + )?.collections[ns].tags; expect(tags.length).to.equal(19); }); it('cuts a tag list when there are more than 20 tags', async function () { @@ -2251,7 +2315,7 @@ describe('Shard', function () { const tags = (await sh.status()).value.databases.find( (d: Document) => d.database._id === 'test' - ).collections[ns].tags; + )?.collections[ns].tags; expect(tags.length).to.equal(21); expect( tags.indexOf( @@ -2885,6 +2949,7 @@ describe('Shard', function () { }); }); }); + describe('collection.isCapped', function () { it('returns true for config.changelog', async function () { const ret = await sh._database @@ -2929,7 +2994,7 @@ describe('Shard', function () { (item: Document) => item.database._id === 'db' ); // Cannot get strict guarantees about the value of this field since SERVER-63983 - expect(databasesDbItem.database.partitioned).to.be.oneOf([ + expect(databasesDbItem?.database.partitioned).to.be.oneOf([ false, undefined, ]); @@ -2937,7 +3002,7 @@ describe('Shard', function () { (item: Document) => item.database._id === 'dbSh' ); // Cannot get strict guarantees about the value of this field since SERVER-60926 and SERVER-63983 - expect(databasesDbShItem.database.partitioned).to.be.oneOf([ + expect(databasesDbShItem?.database.partitioned).to.be.oneOf([ true, false, undefined, @@ -3051,7 +3116,7 @@ describe('Shard', function () { } const chunks = (await sh.status()).value.databases.find( (d: Document) => d.database._id === 'test' - ).collections[ns].chunks; + )?.collections[ns].chunks; expect(chunks.length).to.equal(20); }); @@ -3059,7 +3124,7 @@ describe('Shard', function () { await sh.splitAt(ns, { key: 20 }); const chunks = (await sh.status()).value.databases.find( (d: Document) => d.database._id === 'test' - ).collections[ns].chunks; + )?.collections[ns].chunks; expect(chunks.length).to.equal(21); expect( chunks.indexOf( diff --git a/packages/shell-api/src/shard.ts b/packages/shell-api/src/shard.ts index cd2882115..293ca6fca 100644 --- a/packages/shell-api/src/shard.ts +++ b/packages/shell-api/src/shard.ts @@ -12,6 +12,7 @@ import type { Document, CheckMetadataConsistencyOptions, } from '@mongosh/service-provider-core'; +import type { ShardingStatusResult } from './helpers'; import { assertArgsDefinedType, getConfigDB, @@ -205,7 +206,7 @@ export default class Shard extends ShellApiWithMongoClass { async status( verbose = false, configDB?: Database - ): Promise> { + ): Promise> { const result = await getPrintableShardStatus( configDB ?? (await getConfigDB(this._database)), verbose