Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(shell-api): add shardedDataDistribution to sh.status() MONGOSH-1326 #2214

Merged
merged 26 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d082248
WIP: test if shardDistribution works
gagik Oct 11, 2024
fc93cbd
Fix helper tests
gagik Oct 14, 2024
6fc82eb
Add unit test
gagik Oct 15, 2024
fa4f17f
Merge branch 'main' into gagik/status-6.0.3-clusters
gagik Oct 15, 2024
e7f0eef
Merge branch 'main' into gagik/status-6.0.3-clusters
gagik Oct 15, 2024
7ae0ac7
Merge branch 'main' into gagik/status-6.0.3-clusters
gagik Oct 17, 2024
9fb806d
Remove log
gagik Oct 22, 2024
4842194
Merge branch 'gagik/status-6.0.3-clusters' of github.com:mongodb-js/m…
gagik Oct 22, 2024
f71faa0
Add integration test
gagik Oct 23, 2024
789ba83
Merge branch 'main' of github.com:mongodb-js/mongosh into gagik/statu…
gagik Oct 23, 2024
80ac653
Clear previous shards
gagik Oct 23, 2024
05f6b33
Use a different db name
gagik Oct 23, 2024
2ef07db
and correct ns
gagik Oct 23, 2024
631c92f
Remove unneeded import
gagik Oct 23, 2024
0083086
Move test before others
gagik Oct 24, 2024
7c606db
less strict check of shards
gagik Oct 24, 2024
46adbb2
Apply suggestions from code review
gagik Oct 24, 2024
118574c
Update packages/shell-api/src/helpers.ts
gagik Oct 24, 2024
c6f5f86
Add changes
gagik Oct 24, 2024
f7bc2b5
Merge branch 'main' of github.com:mongodb-js/mongosh into gagik/statu…
gagik Oct 24, 2024
9bf0251
Align with main
gagik Oct 24, 2024
f12bf83
Merge branch 'main' of github.com:mongodb-js/mongosh into gagik/statu…
gagik Oct 24, 2024
f25c329
Fix accidental change
gagik Oct 24, 2024
c4f06b9
Only mock for sharding information
gagik Oct 24, 2024
178b616
Fix lint
gagik Oct 24, 2024
9f26347
Merge branch 'main' into gagik/status-6.0.3-clusters
gagik Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/shell-api/src/change-stream-cursor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,6 @@ describe('ChangeStreamCursor', function () {
it('isExhausted fails', function () {
try {
cursor.isExhausted();
expect.fail('missed exception');
gagik marked this conversation as resolved.
Show resolved Hide resolved
} catch (err: any) {
expect(err.name).to.equal('MongoshInvalidInputError');
}
Expand Down
58 changes: 51 additions & 7 deletions packages/shell-api/src/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ShardedDataDistribution } from './helpers';
import {
assertArgsDefinedType,
coerceToJSNumber,
Expand All @@ -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(
Expand Down Expand Up @@ -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(),
Expand All @@ -150,15 +167,34 @@ describe('getPrintableShardStatus', function () {
configDatabase = new Database(mongo, 'config_test');
expect(configDatabase.getName()).to.equal('config_test');

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 origRunCommandWithCheck = serviceProvider.runCommandWithCheck;
serviceProvider.runCommandWithCheck = async (db, cmd) => {
serviceProvider.runCommandWithCheck = async (configDatabase, cmd) => {
if (cmd.hello) {
return { ok: 1, msg: 'isdbgrid' };
}
if (db === 'admin' && cmd.balancerStatus) {
if (configDatabase === 'admin' && cmd.balancerStatus) {
return { ok: 1, inBalancerRound };
}
return origRunCommandWithCheck.call(serviceProvider, db, cmd, {});
return origRunCommandWithCheck.call(
serviceProvider,
configDatabase,
cmd,
{}
);
};

await Promise.all(
Expand Down Expand Up @@ -202,6 +238,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 () {
Expand All @@ -214,7 +254,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
);
});
}
});
Expand All @@ -235,8 +277,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'
);
});
Expand Down Expand Up @@ -281,7 +325,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');
});

Expand Down
136 changes: 115 additions & 21 deletions packages/shell-api/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -226,8 +226,8 @@ export function processDigestPassword(
export async function getPrintableShardStatus(
configDB: Database,
verbose: boolean
): Promise<Document> {
const result = {} as any;
): Promise<ShardingStatusResult> {
const result = {} as ShardingStatusResult;

// configDB is a DB object that contains the sharding metadata of interest.
const mongosColl = configDB.getCollection('mongos');
Expand Down Expand Up @@ -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;
Expand All @@ -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: {
Expand All @@ -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<string, any> = {};
const balancerRes = {} as ShardingStatusResult['balancer'];
gagik marked this conversation as resolved.
Show resolved Hide resolved
await Promise.all([
(async (): Promise<void> => {
// Is autosplit currently enabled
Expand All @@ -331,13 +335,13 @@ export async function getPrintableShardStatus(
})(),
(async (): Promise<void> => {
// 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;
Expand All @@ -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;
}
);
}
Expand Down Expand Up @@ -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: {
Expand All @@ -437,11 +456,11 @@ export async function getPrintableShardStatus(
},
},
])
).toArray();
).toArray()) as MigrationResult[];

// Failed migrations.
migrations = migrations.concat(
await (
(await (
await changelogColl.aggregate([
{
$match: {
Expand Down Expand Up @@ -472,11 +491,12 @@ export async function getPrintableShardStatus(
},
},
])
).toArray()
).toArray()) as MigrationResult[]
);

const migrationsRes: Record<number, string> = {};
migrations.forEach((x: any) => {
const migrationsRes: ShardingStatusResult['balancer']['Migration Results for the last 24 hours'] =
{};
migrations.forEach((x) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a note for the future, but if you run into existing .forEach() into the future, don't be shy to refactor them to for ... of 🙂

if (x._id === 'Success') {
migrationsRes[x.count] = x._id;
} else {
Expand All @@ -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 })
Expand All @@ -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 });

Expand Down Expand Up @@ -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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am adding it as a separate field at the moment since that's just a simple solution, but we could make it cleaner/nicer for the user. Open to suggestions

databases: { database: Document; collections: Document }[];
};

export type ShardedDataDistribution = {
gagik marked this conversation as resolved.
Show resolved Hide resolved
ns: string;
shards: {
shardName: string;
numOrphanedDocs: number;
numOwnedDocuments: number;
orphanedSizeBytes: number;
ownedSizeBytes: number;
}[];
}[];

export async function getConfigDB(db: Database): Promise<Database> {
const helloResult = await db._maybeCachedHello();
if (helloResult.msg !== 'isdbgrid') {
Expand Down
Loading
Loading