Files
mongo/jstests/replsets/dbcheck/dbcheck_extra_keys_rate_limits.js
Zac 591928c619 SERVER-108478 JS formatted by prettier and remove clang-format (#39656)
GitOrigin-RevId: 6c8f6aded47f260aa4f7c231b17dae3302cb1e04
2025-08-21 17:27:09 +00:00

435 lines
18 KiB
JavaScript

/**
* Tests that the dbCheck command's extra index keys check follows the rate and overall limits.
*
* @tags: [
* requires_fcv_80
* ]
*/
import {configureFailPoint} from "jstests/libs/fail_point_util.js";
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {checkHealthLog, clearHealthLog, resetAndInsert, runDbCheck} from "jstests/replsets/libs/dbcheck_utils.js";
(function () {
"use strict";
// This test injects inconsistencies between replica set members; do not fail because of expected
// dbHash differences.
TestData.skipCheckDBHashes = true;
const dbName = "dbCheckExtraIndexKeys";
const collName = "dbCheckExtraIndexKeysColl";
const allErrorsOrWarningsQuery = {
$or: [{"severity": "warning"}, {"severity": "error"}],
};
const recordNotFoundQuery = {
"severity": "error",
"msg": "found extra index key entry without corresponding document",
"data.context.indexSpec": {$exists: true},
};
const recordDoesNotMatchQuery = {
"severity": "error",
"msg": "found index key entry with corresponding document/keystring set that does not contain the expected key string",
"data.context.indexSpec": {$exists: true},
};
const infoBatchQuery = {
"severity": "info",
"operation": "dbCheckBatch",
};
const replSet = new ReplSetTest({
name: jsTestName(),
nodes: 2,
nodeOptions: {
setParameter: {logComponentVerbosity: tojson({command: 3}), dbCheckHealthLogEveryNBatches: 1},
},
});
replSet.startSet();
replSet.initiate();
const primary = replSet.getPrimary();
const secondary = replSet.getSecondary();
const primaryHealthLog = primary.getDB("local").system.healthlog;
const secondaryHealthLog = secondary.getDB("local").system.healthlog;
const primaryDB = primary.getDB(dbName);
const secondaryDB = secondary.getDB(dbName);
assert.commandWorked(primaryDB.createCollection(collName));
const defaultSnapshotSize = 1000;
const writeConcern = {
w: "majority",
};
const debugBuild = primaryDB.adminCommand("buildInfo").debug;
function checkNumBatchesAndSnapshots(healthLog, nDocs, batchSize, snapshotSize, inconsistentBatch = false) {
const expectedNumBatches = Math.ceil(nDocs / batchSize);
let query = infoBatchQuery;
if (inconsistentBatch) {
query = {"severity": "error", "msg": "dbCheck batch inconsistent"};
}
checkHealthLog(healthLog, query, expectedNumBatches);
if (debugBuild) {
let expectedNumSnapshots = expectedNumBatches;
if (snapshotSize < batchSize) {
const snapshotsPerBatch = Math.ceil(batchSize / snapshotSize);
const lastBatchSize = nDocs % batchSize == 0 ? batchSize : nDocs % batchSize;
const lastBatchSnapshots = Math.ceil(lastBatchSize / snapshotSize);
expectedNumSnapshots = (expectedNumBatches - 1) * snapshotsPerBatch + lastBatchSnapshots;
}
const actualNumSnapshots =
rawMongoProgramOutput("Catalog snapshot for reverse lookup check ending").split(/7844808/).length - 1;
assert.eq(
actualNumSnapshots,
expectedNumSnapshots,
"expected " +
expectedNumSnapshots +
" catalog snapshots during extra index keys check, found " +
actualNumSnapshots,
);
}
}
function exceedMaxCount(nDocs, batchSize, maxCount, docSuffix) {
clearRawMongoProgramOutput();
jsTestLog(
"Testing that dbcheck terminates after exceeding " +
maxCount +
" number of health log entries: nDocs: " +
nDocs +
", batchSize: " +
batchSize +
", docSuffix: " +
docSuffix,
);
resetAndInsert(replSet, primaryDB, collName, nDocs, docSuffix);
const primaryColl = primaryDB.getCollection(collName);
assert.commandWorked(
primaryDB.runCommand({
createIndexes: collName,
indexes: [{key: {a: 1}, name: "a_1"}],
}),
);
replSet.awaitReplication();
assert.eq(primaryColl.find({}).count(), nDocs);
const skipUpdatingIndexDocumentPrimary = configureFailPoint(primaryDB, "skipUpdatingIndexDocument", {
indexName: "a_1",
});
const skipUpdatingIndexDocumentSecondary = configureFailPoint(secondaryDB, "skipUpdatingIndexDocument", {
indexName: "a_1",
});
jsTestLog("Updating docs to remove index key field");
assert.commandWorked(primaryColl.updateMany({}, {$unset: {"a": ""}}));
replSet.awaitReplication();
assert.eq(primaryColl.find({a: {$exists: true}}).count(), 0);
let dbCheckParameters = {
validateMode: "extraIndexKeysCheck",
secondaryIndex: "a_1",
maxDocsPerBatch: batchSize,
batchWriteConcern: writeConcern,
maxCount: maxCount,
};
runDbCheck(replSet, primaryDB, collName, dbCheckParameters, true /*awaitCompletion*/);
// dbcheck will check Math.ceil(maxCount / batchSize) * batchSize documents because we wait
// until the end of a batch to check if we have reached the overall limits of dbcheck
// (maxCount/maxSize), so the number of documents checked will always be a multiple of batchSize
// unless there are fewer documents than maxCount.
const nDocsChecked = Math.min(nDocs, Math.ceil(maxCount / batchSize) * batchSize);
jsTestLog("Checking primary for record does not match error");
checkHealthLog(primaryHealthLog, recordDoesNotMatchQuery, nDocsChecked);
// No other errors on primary.
checkHealthLog(primaryHealthLog, allErrorsOrWarningsQuery, nDocsChecked);
jsTestLog(
"Checking secondary for record does not match error, should have 0 since secondary skips reverse lookup",
);
checkHealthLog(secondaryHealthLog, allErrorsOrWarningsQuery, 0);
jsTestLog("Checking for correct number of batches on primary");
checkNumBatchesAndSnapshots(primaryHealthLog, nDocsChecked, batchSize, defaultSnapshotSize);
jsTestLog("Checking for correct number of batches on secondary");
checkNumBatchesAndSnapshots(secondaryHealthLog, nDocsChecked, batchSize, defaultSnapshotSize);
skipUpdatingIndexDocumentPrimary.off();
skipUpdatingIndexDocumentSecondary.off();
}
function exceedMaxSize(nDocs, batchSize, maxSize, docSuffix) {
clearRawMongoProgramOutput();
// For an index that has `a` as key, the keystring looks like {"": 10}. The size of the encoded
// keystring is 4 or 5 (the first keystring has size 4 and the rest have size 5), which is used
// to keep track of bytesSeen.
// maximum number of documents that should be checked:
const maxCount = Math.ceil((maxSize - 4) / 5 + 1);
jsTestLog(
"Testing that dbcheck terminates after seeing more than " +
maxSize +
" bytes: nDocs: " +
nDocs +
", batchSize: " +
batchSize +
", docSuffix: " +
docSuffix,
);
resetAndInsert(replSet, primaryDB, collName, nDocs, docSuffix);
const primaryColl = primaryDB.getCollection(collName);
assert.commandWorked(
primaryDB.runCommand({
createIndexes: collName,
indexes: [{key: {a: 1}, name: "a_1"}],
}),
);
replSet.awaitReplication();
assert.eq(primaryColl.find({}).count(), nDocs);
const skipUpdatingIndexDocumentPrimary = configureFailPoint(primaryDB, "skipUpdatingIndexDocument", {
indexName: "a_1",
});
const skipUpdatingIndexDocumentSecondary = configureFailPoint(secondaryDB, "skipUpdatingIndexDocument", {
indexName: "a_1",
});
jsTestLog("Updating docs to remove index key field");
assert.commandWorked(primaryColl.updateMany({}, {$unset: {"a": ""}}));
replSet.awaitReplication();
assert.eq(primaryColl.find({a: {$exists: true}}).count(), 0);
let dbCheckParameters = {
validateMode: "extraIndexKeysCheck",
secondaryIndex: "a_1",
maxDocsPerBatch: batchSize,
batchWriteConcern: writeConcern,
maxSize: maxSize,
};
runDbCheck(replSet, primaryDB, collName, dbCheckParameters, true /*awaitCompletion*/);
// dbcheck will check Math.ceil(maxCount / batchSize) * batchSize documents because we wait
// until the end of a batch to check if we have reached the overall limits of dbcheck
// (maxCount/maxSize), so the number of documents checked will always be a multiple of batchSize
// unless there are fewer documents than maxCount.
const nDocsChecked = Math.min(nDocs, Math.ceil(maxCount / batchSize) * batchSize);
jsTestLog("Checking primary for record does not match error");
checkHealthLog(primaryHealthLog, recordDoesNotMatchQuery, nDocsChecked);
// No other errors on primary.
checkHealthLog(primaryHealthLog, allErrorsOrWarningsQuery, nDocsChecked);
jsTestLog(
"Checking secondary for record does not match error, should have 0 since secondary skips reverse lookup",
);
checkHealthLog(secondaryHealthLog, allErrorsOrWarningsQuery, 0);
jsTestLog("Checking for correct number of batches on primary");
checkNumBatchesAndSnapshots(primaryHealthLog, nDocsChecked, batchSize, defaultSnapshotSize);
jsTestLog("Checking for correct number of batches on secondary");
checkNumBatchesAndSnapshots(secondaryHealthLog, nDocsChecked, batchSize, defaultSnapshotSize);
skipUpdatingIndexDocumentPrimary.off();
skipUpdatingIndexDocumentSecondary.off();
}
function exceedMaxDbCheckMBPerSec() {
clearRawMongoProgramOutput();
const nDocs = 5;
const batchSize = 10;
jsTestLog(
"Testing that dbcheck will not read more than maxDbCheckMBperSec (1 MB) per second: nDocs: " +
5 +
", batchSize: " +
batchSize,
);
const primaryColl = primaryDB.getCollection(collName);
primaryDB[collName].drop();
clearHealthLog(replSet);
// Insert nDocs, each slightly larger than the maxDbCheckMBperSec value (1MB), which is the
// default value, while maxBatchTimeMillis is 1 second. Consequently, we will have only 1MB
// per batch.
const chars = ["a", "b", "c", "d", "e"];
primaryColl.insertMany(
[...Array(nDocs).keys()].map((x) => ({a: chars[x].repeat(1024 * 1024 * 2)})),
{ordered: false},
);
assert.commandWorked(
primaryDB.runCommand({
createIndexes: collName,
indexes: [{key: {a: 1}, name: "a_1"}],
}),
);
replSet.awaitReplication();
assert.eq(primaryColl.find({}).count(), nDocs);
// Set up inconsistency.
const skipUnindexingDocumentWhenDeleted = configureFailPoint(primaryDB, "skipUnindexingDocumentWhenDeleted", {
indexName: "a_1",
});
assert.commandWorked(primaryColl.deleteMany({}));
replSet.awaitReplication();
assert.eq(primaryColl.find({}).count(), 0);
assert.eq(secondaryDB.getCollection(collName).find({}).count(), 0);
let dbCheckParameters = {
validateMode: "extraIndexKeysCheck",
secondaryIndex: "a_1",
maxDocsPerBatch: batchSize,
batchWriteConcern: writeConcern,
maxBatchTimeMillis: 1000,
};
runDbCheck(replSet, primaryDB, collName, dbCheckParameters, true /*awaitCompletion*/);
// DbCheck logs (nDocs) batches to account for each batch hitting the time deadline after
// processing only one document.
jsTestLog("Checking primary for record not found error");
checkHealthLog(primaryHealthLog, recordNotFoundQuery, nDocs);
jsTestLog("Checking secondary for record not found error, should have 0");
checkHealthLog(secondaryHealthLog, recordNotFoundQuery, 0);
// No other errors on primary.
checkHealthLog(primaryHealthLog, allErrorsOrWarningsQuery, nDocs);
jsTestLog("Checking for correct number of batches on primary");
checkNumBatchesAndSnapshots(primaryHealthLog, nDocs, 1 /* batchSize */, defaultSnapshotSize);
jsTestLog("Checking for correct number of inconsistent batches on secondary");
checkNumBatchesAndSnapshots(
secondaryHealthLog,
nDocs,
1 /* batchSize */,
defaultSnapshotSize,
true /* inconsistentBatch */,
);
skipUnindexingDocumentWhenDeleted.off();
}
function exceedMaxBatchTimeMillis() {
clearRawMongoProgramOutput();
const maxBatchTimeMillis = 10;
const nDocs = 10;
const batchSize = 20;
jsTestLog(
"Testing that dbcheck will not spend more than maxBatchTimeMillis: " +
maxBatchTimeMillis +
" ms per batch: nDocs: " +
nDocs +
", batchSize: " +
batchSize,
);
const primaryColl = primaryDB.getCollection(collName);
primaryDB[collName].drop();
clearHealthLog(replSet);
// Insert nDocs that are almost 1 MB while maxDbCheckMBperSec is set to the default value of 1
// MB and maxBatchTimeMillis is set to 10 ms. Consequently, we will have 1 doc per batch.
const chars = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"];
primaryColl.insertMany(
[...Array(nDocs).keys()].map((x) => ({a: chars[x].repeat(1024 * 800)})),
{ordered: false},
);
assert.commandWorked(
primaryDB.runCommand({
createIndexes: collName,
indexes: [{key: {a: 1}, name: "a_1"}],
}),
);
replSet.awaitReplication();
assert.eq(primaryColl.find({}).count(), nDocs);
// Set up inconsistency.
const skipUnindexingDocumentWhenDeleted = configureFailPoint(primaryDB, "skipUnindexingDocumentWhenDeleted", {
indexName: "a_1",
});
jsTestLog("Deleting docs");
assert.commandWorked(primaryColl.deleteMany({}));
replSet.awaitReplication();
assert.eq(primaryColl.find({}).count(), 0);
assert.eq(secondaryDB.getCollection(collName).find({}).count(), 0);
let dbCheckParameters = {
validateMode: "extraIndexKeysCheck",
secondaryIndex: "a_1",
maxDocsPerBatch: batchSize,
batchWriteConcern: writeConcern,
maxBatchTimeMillis: maxBatchTimeMillis,
};
runDbCheck(replSet, primaryDB, collName, dbCheckParameters, true /*awaitCompletion*/);
// DbCheck logs (nDocs) batches to account for each batch hitting the time deadline after
// processing only one document.
jsTestLog("Checking primary for record not found error");
checkHealthLog(primaryHealthLog, recordNotFoundQuery, nDocs);
jsTestLog("Checking secondary for record not found error, should have 0");
checkHealthLog(secondaryHealthLog, recordNotFoundQuery, 0);
// No other errors on primary.
checkHealthLog(primaryHealthLog, allErrorsOrWarningsQuery, nDocs);
jsTestLog("Checking for correct number of batches on primary");
checkNumBatchesAndSnapshots(primaryHealthLog, nDocs, 1 /* batchSize */, defaultSnapshotSize);
jsTestLog("Checking for correct number of inconsistent batches on secondary");
checkNumBatchesAndSnapshots(
secondaryHealthLog,
nDocs,
1 /* batchSize */,
defaultSnapshotSize,
true /* inconsistentBatch */,
);
skipUnindexingDocumentWhenDeleted.off();
}
exceedMaxDbCheckMBPerSec();
exceedMaxBatchTimeMillis();
// dbcheck will stop when it reads at least maxSize bytes. Each batch would be around batchSize * 5
// bytes and dbcheck will stop after the last batch that reaches maxSize.
// Test with maxSize = 1 batch
exceedMaxSize(100 /* nDocs */, 10 /* batchSize */, 49 /* maxSize */, null /* docSuffix */);
// Test with maxSize = 2 batches
exceedMaxSize(100 /* nDocs */, 5 /* batchSize */, 49 /* maxSize */, null /* docSuffix */);
// Test with maxSize that does not fit into full batches, so we will check over maxSize at the next
// batch boundary
exceedMaxSize(100 /* nDocs */, 7 /* batchSize */, 49 /* maxSize */, null /* docSuffix */);
// Test with maxSize > size for nDocs
exceedMaxSize(100 /* nDocs */, 3 /* batchSize */, 528 /* maxSize */, null /* docSuffix */);
// Test with integer index entries (1, 2, 3, etc.), single character string entries ("1",
// "2", "3", etc.), and long string entries ("1aaaaaaaaaa")
[null, "", "aaaaaaaaaa"].forEach((docSuffix) => {
// Test with maxCount = 1 batch
exceedMaxCount(100 /* nDocs */, 10 /* batchSize */, 10 /* maxCount */, docSuffix);
// Test with maxCount = 2 batches
exceedMaxCount(100 /* nDocs */, 20 /* batchSize */, 10 /* maxCount */, docSuffix);
// Test with maxCount that is not a multiple of batchSize, so we will check over maxCount at the
// next batch boundary
exceedMaxCount(100 /* nDocs */, 8 /* batchSize */, 10 /* maxCount */, docSuffix);
// Test with maxCount > nDocs
exceedMaxCount(100 /* nDocs */, 50 /* batchSize */, 200 /* maxCount */, docSuffix);
});
// TODO SERVER-79846:
// * Test progress meter/stats are correct
replSet.stopSet(undefined /* signal */, false /* forRestart */, {skipCheckDBHashes: true, skipValidation: true});
})();