Skip to content

Commit e91cd29

Browse files
authored
feat(shell-api): add shardedDataDistribution to sh.status() MONGOSH-1326 (#2214)
1 parent 4ced968 commit e91cd29

File tree

4 files changed

+234
-35
lines changed

4 files changed

+234
-35
lines changed

packages/shell-api/src/helpers.spec.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ShardedDataDistribution } from './helpers';
12
import {
23
assertArgsDefinedType,
34
coerceToJSNumber,
@@ -19,6 +20,7 @@ import sinon from 'ts-sinon';
1920
import chai, { expect } from 'chai';
2021
import { EventEmitter } from 'events';
2122
import sinonChai from 'sinon-chai';
23+
import { stub } from 'sinon';
2224
chai.use(sinonChai);
2325

2426
const fakeConfigDb = makeFakeConfigDatabase(
@@ -133,6 +135,21 @@ describe('getPrintableShardStatus', function () {
133135
let serviceProvider: ServiceProvider;
134136
let inBalancerRound = false;
135137

138+
const mockedShardedDataDistribution: ShardedDataDistribution = [
139+
{
140+
ns: 'test.ns',
141+
shards: [
142+
{
143+
shardName: 'test',
144+
numOrphanedDocs: 1,
145+
numOwnedDocuments: 5,
146+
orphanedSizeBytes: 20,
147+
ownedSizeBytes: 80,
148+
},
149+
],
150+
},
151+
];
152+
136153
beforeEach(async function () {
137154
serviceProvider = await NodeDriverServiceProvider.connect(
138155
await testServer.connectionString(),
@@ -186,6 +203,20 @@ describe('getPrintableShardStatus', function () {
186203
});
187204

188205
it('returns an object with sharding information', async function () {
206+
const mockedAdminDb = {
207+
aggregate: stub()
208+
.withArgs([{ $shardedDataDistribution: {} }])
209+
.resolves({
210+
toArray: stub().resolves(mockedShardedDataDistribution),
211+
}),
212+
};
213+
const getSiblingDB = stub();
214+
getSiblingDB.withArgs('admin').returns(mockedAdminDb);
215+
getSiblingDB.withArgs('config').returns(configDatabase);
216+
217+
configDatabase.getSiblingDB = getSiblingDB;
218+
configDatabase._maybeCachedHello = stub().returns({ msg: 'isdbgrid' });
219+
189220
const status = await getPrintableShardStatus(configDatabase, false);
190221
expect(status.shardingVersion.clusterId).to.be.instanceOf(bson.ObjectId);
191222
expect(status.shards.map(({ host }: { host: string }) => host)).to.include(
@@ -202,6 +233,10 @@ describe('getPrintableShardStatus', function () {
202233
);
203234
expect(status.databases).to.have.lengthOf(1);
204235
expect(status.databases[0].database._id).to.equal('config');
236+
237+
expect(status.shardedDataDistribution).to.equal(
238+
mockedShardedDataDistribution
239+
);
205240
});
206241

207242
describe('hides all internal deprecated fields in shardingVersion', function () {
@@ -214,7 +249,9 @@ describe('getPrintableShardStatus', function () {
214249
]) {
215250
it(`does not show ${hiddenField} in shardingVersion`, async function () {
216251
const status = await getPrintableShardStatus(configDatabase, false);
217-
expect(status.shardingVersion[hiddenField]).to.equal(undefined);
252+
expect((status.shardingVersion as any)[hiddenField]).to.equal(
253+
undefined
254+
);
218255
});
219256
}
220257
});
@@ -235,8 +272,10 @@ describe('getPrintableShardStatus', function () {
235272

236273
it('returns an object with verbose sharding information if requested', async function () {
237274
const status = await getPrintableShardStatus(configDatabase, true);
238-
expect(status['most recently active mongoses'][0].up).to.be.a('number');
239-
expect(status['most recently active mongoses'][0].waiting).to.be.a(
275+
expect((status['most recently active mongoses'][0] as any).up).to.be.a(
276+
'number'
277+
);
278+
expect((status['most recently active mongoses'][0] as any).waiting).to.be.a(
240279
'boolean'
241280
);
242281
});
@@ -281,7 +320,7 @@ describe('getPrintableShardStatus', function () {
281320
status.balancer['Collections with active migrations']
282321
).to.have.lengthOf(1);
283322
expect(
284-
status.balancer['Collections with active migrations'].join('')
323+
status.balancer['Collections with active migrations']?.join('')
285324
).to.include('asdf');
286325
});
287326

packages/shell-api/src/helpers.ts

Lines changed: 115 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type {
2626
bson,
2727
} from '@mongosh/service-provider-core';
2828
import type { ClientSideFieldLevelEncryptionOptions } from './field-level-encryption';
29-
import { type AutoEncryptionOptions } from 'mongodb';
29+
import type { AutoEncryptionOptions, Long, ObjectId, Timestamp } from 'mongodb';
3030
import { shellApiType } from './enums';
3131
import type { AbstractCursor } from './abstract-cursor';
3232
import type ChangeStreamCursor from './change-stream-cursor';
@@ -226,8 +226,8 @@ export function processDigestPassword(
226226
export async function getPrintableShardStatus(
227227
configDB: Database,
228228
verbose: boolean
229-
): Promise<Document> {
230-
const result = {} as any;
229+
): Promise<ShardingStatusResult> {
230+
const result = {} as ShardingStatusResult;
231231

232232
// configDB is a DB object that contains the sharding metadata of interest.
233233
const mongosColl = configDB.getCollection('mongos');
@@ -259,9 +259,12 @@ export async function getPrintableShardStatus(
259259
);
260260
}
261261

262-
result.shardingVersion = version;
262+
result.shardingVersion = version as {
263+
_id: number;
264+
clusterId: ObjectId;
265+
};
263266

264-
result.shards = shards;
267+
result.shards = shards as ShardingStatusResult['shards'];
265268

266269
// (most recently) active mongoses
267270
const mongosActiveThresholdMs = 60000;
@@ -280,9 +283,8 @@ export async function getPrintableShardStatus(
280283
}
281284
}
282285

283-
mongosAdjective = `${mongosAdjective} mongoses`;
284286
if (mostRecentMongosTime === null) {
285-
result[mongosAdjective] = 'none';
287+
result[`${mongosAdjective} mongoses`] = 'none';
286288
} else {
287289
const recentMongosQuery = {
288290
ping: {
@@ -295,25 +297,27 @@ export async function getPrintableShardStatus(
295297
};
296298

297299
if (verbose) {
298-
result[mongosAdjective] = await (await mongosColl.find(recentMongosQuery))
300+
result[`${mongosAdjective} mongoses`] = await (
301+
await mongosColl.find(recentMongosQuery)
302+
)
299303
.sort({ ping: -1 })
300304
.toArray();
301305
} else {
302-
result[mongosAdjective] = (
306+
result[`${mongosAdjective} mongoses`] = (
303307
(await (
304308
await mongosColl.aggregate([
305309
{ $match: recentMongosQuery },
306310
{ $group: { _id: '$mongoVersion', num: { $sum: 1 } } },
307311
{ $sort: { num: -1 } },
308312
])
309-
).toArray()) as any[]
313+
).toArray()) as { _id: string; num: number }[]
310314
).map((z: { _id: string; num: number }) => {
311315
return { [z._id]: z.num };
312316
});
313317
}
314318
}
315319

316-
const balancerRes: Record<string, any> = {};
320+
const balancerRes = {} as ShardingStatusResult['balancer'];
317321
await Promise.all([
318322
(async (): Promise<void> => {
319323
// Is autosplit currently enabled
@@ -331,13 +335,13 @@ export async function getPrintableShardStatus(
331335
})(),
332336
(async (): Promise<void> => {
333337
// Is the balancer currently active
334-
let balancerRunning = 'unknown';
338+
let balancerRunning: 'yes' | 'no' | 'unknown' = 'unknown';
335339
try {
336340
const balancerStatus = await configDB.adminCommand({
337341
balancerStatus: 1,
338342
});
339343
balancerRunning = balancerStatus.inBalancerRound ? 'yes' : 'no';
340-
} catch (err: any) {
344+
} catch {
341345
// pass, ignore all error messages
342346
}
343347
balancerRes['Currently running'] = balancerRunning;
@@ -364,7 +368,7 @@ export async function getPrintableShardStatus(
364368
if (activeLocks?.length > 0) {
365369
balancerRes['Collections with active migrations'] = activeLocks.map(
366370
(lock) => {
367-
return `${lock._id} started at ${lock.when}`;
371+
return `${lock._id} started at ${lock.when}` as const;
368372
}
369373
);
370374
}
@@ -418,8 +422,23 @@ export async function getPrintableShardStatus(
418422
const yesterday = new Date();
419423
yesterday.setDate(yesterday.getDate() - 1);
420424

425+
type MigrationResult =
426+
| {
427+
_id: 'Success';
428+
count: number;
429+
from: never;
430+
to: never;
431+
}
432+
// Failed migration
433+
| {
434+
_id: string;
435+
count: number;
436+
from: string;
437+
to: string;
438+
};
439+
421440
// Successful migrations.
422-
let migrations = await (
441+
let migrations = (await (
423442
await changelogColl.aggregate([
424443
{
425444
$match: {
@@ -437,11 +456,11 @@ export async function getPrintableShardStatus(
437456
},
438457
},
439458
])
440-
).toArray();
459+
).toArray()) as MigrationResult[];
441460

442461
// Failed migrations.
443462
migrations = migrations.concat(
444-
await (
463+
(await (
445464
await changelogColl.aggregate([
446465
{
447466
$match: {
@@ -472,11 +491,12 @@ export async function getPrintableShardStatus(
472491
},
473492
},
474493
])
475-
).toArray()
494+
).toArray()) as MigrationResult[]
476495
);
477496

478-
const migrationsRes: Record<number, string> = {};
479-
migrations.forEach((x: any) => {
497+
const migrationsRes: ShardingStatusResult['balancer']['Migration Results for the last 24 hours'] =
498+
{};
499+
migrations.forEach((x) => {
480500
if (x._id === 'Success') {
481501
migrationsRes[x.count] = x._id;
482502
} else {
@@ -500,7 +520,7 @@ export async function getPrintableShardStatus(
500520
// All databases in config.databases + those implicitly referenced
501521
// by a sharded collection in config.collections
502522
// (could become a single pipeline using $unionWith when we drop 4.2 server support)
503-
const [databases, collections] = await Promise.all([
523+
const [databases, collections, shardedDataDistribution] = await Promise.all([
504524
(async () =>
505525
await (await configDB.getCollection('databases').find())
506526
.sort({ _id: 1 })
@@ -513,7 +533,22 @@ export async function getPrintableShardStatus(
513533
)
514534
.sort({ _id: 1 })
515535
.toArray())(),
536+
(async () => {
537+
try {
538+
// $shardedDataDistribution is available since >= 6.0.3
539+
const adminDB = configDB.getSiblingDB('admin');
540+
return (await (
541+
await adminDB.aggregate([{ $shardedDataDistribution: {} }])
542+
).toArray()) as ShardedDataDistribution;
543+
} catch {
544+
// Pass, most likely an older version.
545+
return undefined;
546+
}
547+
})(),
516548
]);
549+
550+
result.shardedDataDistribution = shardedDataDistribution;
551+
517552
// Special case the config db, since it doesn't have a record in config.databases.
518553
databases.push({ _id: 'config', primary: 'config', partitioned: true });
519554

@@ -648,6 +683,65 @@ export async function getPrintableShardStatus(
648683
return result;
649684
}
650685

686+
export type ShardingStatusResult = {
687+
shardingVersion: {
688+
_id: number;
689+
clusterId: ObjectId;
690+
/** This gets deleted when it is returned from getPrintableShardStatus */
691+
currentVersion?: number;
692+
};
693+
shards: {
694+
_id: string;
695+
host: string;
696+
state: number;
697+
tags: string[];
698+
topologyTime: Timestamp;
699+
replSetConfigVersion: Long;
700+
}[];
701+
[mongoses: `${string} mongoses`]:
702+
| 'none'
703+
| {
704+
[version: string]:
705+
| number
706+
| {
707+
up: number;
708+
waiting: boolean;
709+
};
710+
}[];
711+
autosplit: {
712+
'Currently enabled': 'yes' | 'no';
713+
};
714+
balancer: {
715+
'Currently enabled': 'yes' | 'no';
716+
'Currently running': 'yes' | 'no' | 'unknown';
717+
'Failed balancer rounds in last 5 attempts': number;
718+
'Migration Results for the last 24 hours':
719+
| 'No recent migrations'
720+
| {
721+
[count: number]:
722+
| 'Success'
723+
| `Failed with error '${string}', from ${string} to ${string}`;
724+
};
725+
'Balancer active window is set between'?: `${string} and ${string} server local time`;
726+
'Last reported error'?: string;
727+
'Time of Reported error'?: string;
728+
'Collections with active migrations'?: `${string} started at ${string}`[];
729+
};
730+
shardedDataDistribution?: ShardedDataDistribution;
731+
databases: { database: Document; collections: Document }[];
732+
};
733+
734+
export type ShardedDataDistribution = {
735+
ns: string;
736+
shards: {
737+
shardName: string;
738+
numOrphanedDocs: number;
739+
numOwnedDocuments: number;
740+
orphanedSizeBytes: number;
741+
ownedSizeBytes: number;
742+
}[];
743+
}[];
744+
651745
export async function getConfigDB(db: Database): Promise<Database> {
652746
const helloResult = await db._maybeCachedHello();
653747
if (helloResult.msg !== 'isdbgrid') {

0 commit comments

Comments
 (0)