Files
mongo/jstests/libs/replicated_record_ids_utils.js
Moustafa Maher 6849b3ff14 SERVER-96971 Prevent ReplSetTest.waitForState from returning stale data for SECONDARY state (#29585)
GitOrigin-RevId: 9d38b94afbd94ff5dffa63b7ec487ea54fa4a98d
2024-12-05 21:15:14 +00:00

251 lines
11 KiB
JavaScript

// Use an aggregate because showRecordId() will hide any existing user '$recordId' field in the
// documents, otherwise.
import {ReplSetTest} from "jstests/libs/replsettest.js";
function getShowRecordIdsCursor(node, dbName, replicatedCollName) {
return node.getDB(dbName)[replicatedCollName].aggregate(
[{"$project": {"recordId": {"$meta": "recordId"}, "document": "$$ROOT"}}]);
}
// Confirms data returned from a full collection scan on 'replicatedCollName', with the '$recordId'
// field included for each document, yields the same results across all nodes.
export function validateShowRecordIdReplicatesAcrossNodes(nodes, dbName, replicatedCollName) {
assert(nodes.length !== 0, `Method only applies when there is more than 1 node to compare`);
const node0 = nodes[0];
const node0Cursor = getShowRecordIdsCursor(node0, dbName, replicatedCollName);
for (let i = 1; i < nodes.length; i++) {
const curNode = nodes[i];
const curNodeCursor = getShowRecordIdsCursor(curNode, dbName, replicatedCollName);
assert(curNodeCursor.hasNext(), `Expected to validate non-empty results`);
const actualDiff = DataConsistencyChecker.getDiff(node0Cursor, curNodeCursor);
assert.eq({docsWithDifferentContents: [], docsMissingOnFirst: [], docsMissingOnSecond: []},
actualDiff,
`Expected RecordIds to match between node ${node0.host} and node ${
curNode.host}. Got diff ${tojson(actualDiff)}`);
}
}
// Returns the '$recordId' associated with the 'doc' when a find() with 'showRecordId()' is
// performed.
export function getRidForDoc(db, collName, doc) {
assert(db[collName].exists(),
`Collection ${db[collName].getFullName()} not found on ${db.getMongo().host}`);
const docs = db[collName].find(doc).showRecordId().toArray();
assert.eq(docs.length,
1,
`Document ${tojson(doc)} not found in collection ${db[collName].getFullName()} on ${
db.getMongo().host}`);
const res = docs[0];
assert.neq(res["$recordId"], null);
return res["$recordId"];
}
// Generates a map of recordIds keyed by provided field name.
// 'docs' is an array returned by a find query that must contain both '$recordId' and the field
// name of the key.
export function mapFieldToMatchingDocRid(docs, fieldName) {
return docs.reduce((m, doc) => {
assert(doc.hasOwnProperty(fieldName),
`Missing key ${fieldName} in query results: ${tojson(doc)}`);
assert(doc.hasOwnProperty('$recordId'),
`Missing record ID in query results: ${tojson(doc)}`);
const recordId = doc['$recordId'];
m[doc[fieldName]] = recordId;
return m;
}, {});
}
// Tests that recordIds are preserved during initial sync for collections with
// recordIdsReplicated:true. The method of initial sync used is determined by 'initSyncMethod',
// which can be one of "logical" or "fileCopyBased". This test also pauses initial sync before
// cloning and after cloning to test the oplog application phase of initial sync, including its
// idempotency.
// The 'beforeCloningFP' is a failpoint that is used to pause initial sync before
// collections have been cloned. The 'afterCloningFP' is a failpoint that is used to pause initial
// sync after collections have been cloned, but before oplog application.
export function testPreservingRecordIdsDuringInitialSync(
initSyncMethod, beforeCloningFP, afterCloningFP) {
const testName = jsTestName();
const replTest = new ReplSetTest({name: testName, nodes: 2});
replTest.startSet();
replTest.initiate();
const dbName = 'test';
const collName = 'rrid';
const primary = replTest.getPrimary();
const primDB = primary.getDB(dbName);
{
jsTestLog("Beginning case 1.");
// Case 1: Insert documents on the primary where the recordIds start from recordId:2.
// The new node, after initial sync, must also start its recordIds from recordId:2.
// Insert documents where some have a $recordId field within them. The recordId provided
// here is just a field and is separate from the true recordId used when inserting.
assert.commandWorked(primDB.runCommand({create: collName, recordIdsReplicated: true}));
assert.commandWorked(primDB[collName].insertMany([
{_id: 1, a: 1}, // recordId: 1
{$recordId: 12, _id: 2, a: 2}, // recordId: 2
{_id: 3, $recordId: 13, a: 3}, // recordId: 3
{_id: 4, a: 4} // recordId: 4
]));
// Remove recordId: 1 and then insert the doc again, which now gets recordId: 5.
assert.commandWorked(primDB[collName].remove({_id: 1}));
assert.commandWorked(primDB[collName].insert({_id: 1, a: 1}));
jsTestLog("Add a new node and wait for it to become secondary.");
const initialSyncNode = replTest.add({
rsConfig: {priority: 0},
setParameter: {
logComponentVerbosity: tojsononeline({replication: 0, storage: 0}),
initialSyncMethod: initSyncMethod
}
});
replTest.reInitiate();
replTest.awaitSecondaryNodes(null, [initialSyncNode]);
replTest.awaitReplication();
validateShowRecordIdReplicatesAcrossNodes([primary, initialSyncNode], dbName, collName);
assert.sameMembers(primary.getDB(dbName).getCollectionInfos(),
initialSyncNode.getDB(dbName).getCollectionInfos());
replTest.remove(initialSyncNode);
replTest.reInitiate();
}
{
jsTestLog("Beginning case 2.");
// Case 2: Add a new node and while initial sync is ongoing, insert more documents on
// to the primary after collection copying to test oplog application during initial sync.
primDB[collName].drop();
assert.commandWorked(primDB.runCommand({create: collName, recordIdsReplicated: true}));
assert.commandWorked(primDB[collName].insertMany([{_id: 1}, {_id: 2}]));
assert.commandWorked(primDB[collName].remove({_id: 2}));
// Populate an array of documents that we will insert during initial sync.
const moreDocsToInsert = [];
for (let i = 0; i < 100; i++) {
moreDocsToInsert.push({_id: i + 5, a: i + 5});
}
jsTestLog("Add a new node.");
// We add a new node that hangs after it finishes copying databases. Therefore any
// inserts performed during this time will have to be applied during the oplog application
// phase of initial sync.
const initialSyncNode = replTest.add({
rsConfig: {priority: 0},
setParameter: {
logComponentVerbosity: tojsononeline({replication: 5, storage: 5}),
initialSyncMethod: initSyncMethod
}
});
assert.commandWorked(
initialSyncNode.adminCommand({configureFailPoint: afterCloningFP, mode: "alwaysOn"}));
replTest.reInitiate();
jsTestLog("Wait for the initial sync to start and pause right after copying databases.");
assert.commandWorked(initialSyncNode.adminCommand(
{waitForFailPoint: afterCloningFP, timesEntered: 1, maxTimeMS: 60000}));
jsTestLog("Insert documents on the primary.");
assert.commandWorked(primDB[collName].insertMany(moreDocsToInsert));
jsTestLog("Resume initial sync.");
assert.commandWorked(
initialSyncNode.adminCommand({configureFailPoint: afterCloningFP, mode: "off"}));
replTest.awaitSecondaryNodes(null, [initialSyncNode]);
replTest.awaitReplication();
validateShowRecordIdReplicatesAcrossNodes([primary, initialSyncNode], dbName, collName);
replTest.remove(initialSyncNode);
replTest.reInitiate();
}
{
jsTestLog("Beginning case 3.");
// Case 3: Add a new node and while initial sync is ongoing but before collection copying
// starts, insert more documents on to the primary to test oplog application idempotency.
// During collection copy, all the documents will have been copied over already. However
// oplog application will try to re-insert them.
primDB[collName].drop();
assert.commandWorked(primDB.runCommand({create: collName, recordIdsReplicated: true}));
assert.commandWorked(primDB[collName].insertMany([{_id: 1}, {_id: 2}]));
assert.commandWorked(primDB[collName].remove({_id: 2}));
// Populate an array of documents that we will insert during initial sync.
const moreDocsToInsert = [];
for (let i = 0; i < 100; i++) {
moreDocsToInsert.push({_id: i + 5, a: i + 5});
}
jsTestLog("Add a new node.");
// We add a new node that hangs before it starts copying databases. Therefore any
// inserts performed during this time will have to be applied during the oplog application
// phase of initial sync.
const initialSyncNode = replTest.add({
rsConfig: {priority: 0},
setParameter: {
logComponentVerbosity: tojsononeline({replication: 5, storage: 5}),
initialSyncMethod: initSyncMethod
}
});
assert.commandWorked(
initialSyncNode.adminCommand({configureFailPoint: beforeCloningFP, mode: "alwaysOn"}));
replTest.reInitiate();
jsTestLog("Wait for the initial sync to start and pause right before copying databases.");
assert.commandWorked(initialSyncNode.adminCommand(
{waitForFailPoint: beforeCloningFP, timesEntered: 1, maxTimeMS: 60000}));
jsTestLog("Insert documents on the primary.");
assert.commandWorked(primDB[collName].insertMany(moreDocsToInsert));
jsTestLog("Resume initial sync.");
assert.commandWorked(
initialSyncNode.adminCommand({configureFailPoint: beforeCloningFP, mode: "off"}));
replTest.awaitSecondaryNodes(null, [initialSyncNode]);
replTest.awaitReplication();
validateShowRecordIdReplicatesAcrossNodes([primary, initialSyncNode], dbName, collName);
replTest.remove(initialSyncNode);
replTest.reInitiate();
}
{
jsTestLog("Beginning case 4.");
// Case 3: Add a new node that has to collection copy a few 16 MB documents.
primDB[collName].drop();
assert.commandWorked(primDB.runCommand({create: collName, recordIdsReplicated: true}));
// Insert a few 16 MB documents.
for (let i = 0; i < 5; i++) {
assert.commandWorked(
primDB[collName].insert({_id: -100 + i, a: 'a'.repeat(16 * 1024 * 1024 - 26)}));
}
jsTestLog("Add a new node.");
const initialSyncNode = replTest.add({
rsConfig: {priority: 0},
setParameter: {
logComponentVerbosity: tojsononeline({replication: 5, storage: 5}),
initialSyncMethod: initSyncMethod
}
});
replTest.reInitiate();
replTest.awaitSecondaryNodes(null, [initialSyncNode]);
replTest.awaitReplication();
validateShowRecordIdReplicatesAcrossNodes([primary, initialSyncNode], dbName, collName);
replTest.remove(initialSyncNode);
replTest.reInitiate();
}
replTest.stopSet();
}