251 lines
11 KiB
JavaScript
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();
|
|
}
|