mongo/jstests/replsets/version_context/version_context_oplog_entri...

158 lines
6.3 KiB
JavaScript

/**
* Tests the propagation of Operation FCV in the oplog's `versionContext` field for:
* - Non-applyOps entries from operations outside transactions.
* - applyOps entries for batched writes.
* - applyOps entries for unprepared transactions.
* - applyOps entries for prepared transactions.
*
* @tags: [
* required_fcv_83,
* requires_replication,
* uses_transactions,
* uses_prepare_transaction
* ]
*/
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {PrepareHelpers} from "jstests/core/txns/libs/prepare_helpers.js";
let rst = new ReplSetTest({nodes: 2});
rst.startSet();
rst.initiate();
const conn = rst.getPrimary();
const db = conn.getDB("test");
const currentFCV = assert.commandWorked(db.adminCommand({getParameter: 1, featureCompatibilityVersion: 1}))
.featureCompatibilityVersion.version;
// Force operations to use an Operation FCV, so they emit the `versionContext` field in their oplog
// entries. This is required since not all operations use an OFCV yet.
const originalMongoRunCommand = Mongo.prototype.runCommand;
Mongo.prototype.runCommand = function runCommandSpy(dbName, cmdObj, options) {
cmdObj.versionContext = {OFCV: currentFCV};
return originalMongoRunCommand.apply(this, arguments);
};
function findOneInOplog(filter) {
const oplogEntry = db.getSiblingDB("local").oplog.rs.findOne(filter);
assert(oplogEntry, "Could not find an oplog entry for filter: " + tojson(filter));
return oplogEntry;
}
// Verifies that oplog entry format for a standalone entry (not a transaction nor a batched write).
// We expect those entries to have the versionContext field, except for no-op entries.
function verifyStandaloneOplogEntry(oplogEntry) {
assert(
!(oplogEntry.op === "c" && oplogEntry.o.applyOps),
"Unexpected format for standalone oplog entry: " + tojson(oplogEntry),
);
if (oplogEntry.op === "n") {
assert(!oplogEntry.versionContext, "Unexpected versionContext in no-op entry: " + tojson(oplogEntry));
} else {
assert.eq({OFCV: currentFCV}, oplogEntry.versionContext, tojson(oplogEntry));
}
}
(function testStandaloneOperations() {
const coll = db.no_txn;
const insertResult = assert.commandWorked(db.runCommand({insert: coll.getName(), documents: [{x: 1}]}));
verifyStandaloneOplogEntry(findOneInOplog({ts: insertResult.operationTime}));
const updateResult = assert.commandWorked(
db.runCommand({update: coll.getName(), updates: [{q: {x: 1}, u: {$set: {x: 2}}}]}),
);
verifyStandaloneOplogEntry(findOneInOplog({ts: updateResult.operationTime}));
const dropResult = assert.commandWorked(db.runCommand({drop: coll.getName()}));
verifyStandaloneOplogEntry(findOneInOplog({ts: dropResult.operationTime}));
const noopResult = assert.commandWorked(
db.adminCommand({
appendOplogNote: 1,
data: {msg: "test oplog note"},
}),
);
verifyStandaloneOplogEntry(findOneInOplog({ts: noopResult.operationTime}));
})();
// Verifies that oplog entry format for a batched write.
// We expect it to specify versionContext on the oplog entry, but *not* for each sub-operation,
// since batched writes are generated by a single operation (with its single OFCV).
function verifyBatchedWritesEntry(oplogEntry) {
assert(
oplogEntry.op === "c" && oplogEntry.o.applyOps && !oplogEntry.txnNumber,
"Unexpected format for batched writes oplog entry:" + tojson(oplogEntry),
);
assert.eq({OFCV: currentFCV}, oplogEntry.versionContext, tojson(oplogEntry));
for (const nestedOp of oplogEntry.o.applyOps) {
assert(!nestedOp.versionContext, tojson(oplogEntry));
}
}
(function testBatchedWrites() {
const coll = db.getCollection("batched_writes");
const insertResult = assert.commandWorked(db.runCommand({insert: coll.getName(), documents: [{x: 1}, {x: 2}]}));
verifyBatchedWritesEntry(findOneInOplog({ts: insertResult.operationTime}));
const deleteResult = assert.commandWorked(
db.runCommand({
delete: coll.getName(),
deletes: [{q: {x: {$gt: 0}}, limit: 0}],
}),
);
verifyBatchedWritesEntry(findOneInOplog({ts: deleteResult.operationTime}));
})();
// Verifies that oplog entry format for a (prepared or unprepared) transaction.
// We expect it to not specify versionContext for each sub-operation, but not on the oplog entry,
// since transactions can be generated by multiple operations (each with its OFCV).
function verifyTransactionOplogEntry(oplogEntry) {
assert(
oplogEntry.op === "c" && oplogEntry.o.applyOps && oplogEntry.txnNumber,
"Unexpected format for transaction oplog entry:" + tojson(oplogEntry),
);
assert(!oplogEntry.versionContext, tojson(oplogEntry));
for (const nestedOp of oplogEntry.o.applyOps) {
assert.eq({OFCV: currentFCV}, nestedOp.versionContext, tojson(oplogEntry));
}
}
(function testUnpreparedTransaction() {
const session = conn.startSession();
session.startTransaction();
const sessionDB = session.getDatabase(db.getName());
const coll = sessionDB.getCollection("unprepared_txn");
assert.commandWorked(coll.createIndex({x: 1}));
assert.commandWorked(coll.insertOne({x: 123}));
assert.commandWorked(coll.updateOne({x: 123}, {$set: {x: 321}}));
const commitResult = assert.commandWorked(session.commitTransaction_forTesting());
verifyTransactionOplogEntry(findOneInOplog({ts: commitResult.operationTime}));
})();
(function testPreparedTransaction() {
const session = conn.startSession();
session.startTransaction();
const sessionDB = session.getDatabase(db.getName());
const coll = sessionDB.getCollection("prepared_txn");
// Collections can't be created in a prepared transaction, so do it outside
assert.commandWorked(db.createCollection(coll.getName()));
assert.commandWorked(coll.insertOne({x: 1}));
assert.commandWorked(coll.updateOne({x: 1}, {$set: {x: 2}}));
const prepareTimestamp = PrepareHelpers.prepareTransaction(session);
const commitResult = assert.commandWorked(PrepareHelpers.commitTransaction(session, prepareTimestamp));
verifyTransactionOplogEntry(findOneInOplog({ts: prepareTimestamp}));
verifyStandaloneOplogEntry(findOneInOplog({ts: commitResult.operationTime}));
})();
Mongo.prototype.runCommand = originalMongoRunCommand;
rst.stopSet();