mongo/jstests/replsets/internal_sessions_reaping_r...

953 lines
36 KiB
JavaScript

/*
* Test that the logical session cache reaper reaps transaction sessions that correspond to the same
* retryable write atomically.
*
* @tags: [requires_fcv_60, uses_transactions]
*/
import {configureFailPoint} from "jstests/libs/fail_point_util.js";
import {Thread} from "jstests/libs/parallelTester.js";
import {ReplSetTest} from "jstests/libs/replsettest.js";
import {extractUUIDFromObject} from "jstests/libs/uuid_util.js";
import {makeCommitTransactionCmdObj} from "jstests/sharding/libs/sharded_transactions_helpers.js";
// This test runs the reapLogicalSessionCacheNow command. That can lead to direct writes to the
// config.transactions collection, which cannot be performed on a session.
TestData.disableImplicitSessions = true;
const rst = new ReplSetTest({
nodes: 2,
nodeOptions: {
setParameter: {
TransactionRecordMinimumLifetimeMinutes: 0,
},
},
});
rst.startSet();
rst.initiate();
const primary = rst.getPrimary();
const kConfigSessionsNs = "config.system.sessions";
const kConfigTxnsNs = "config.transactions";
const kConfigImageNs = "config.image_collection";
const sessionsColl = primary.getCollection(kConfigSessionsNs);
const transactionsColl = primary.getCollection(kConfigTxnsNs);
const imageColl = primary.getCollection(kConfigImageNs);
const kDbName = "testDb";
const kCollName = "testColl";
const kNs = kDbName + "." + kCollName;
const testDB = primary.getDB(kDbName);
const testColl = testDB.getCollection(kCollName);
assert.commandWorked(testDB.createCollection(kCollName));
function makeSessionOptsForTest() {
const sessionUUID = UUID();
const parentLsid = {id: sessionUUID};
const parentTxnNumber = NumberLong(35);
const childLsidForRetryableWrite = {
id: sessionUUID,
txnNumber: parentTxnNumber,
txnUUID: UUID(),
};
const childLsidForPrevRetryableWrite = {
id: sessionUUID,
txnNumber: NumberLong(parentTxnNumber.valueOf() - 1),
txnUUID: UUID(),
};
const childLsidForNonRetryableWrite = {id: sessionUUID, txnUUID: UUID()};
const childTxnNumber = NumberLong(0);
return {
sessionUUID,
parentLsid,
parentTxnNumber,
childLsidForRetryableWrite,
childLsidForPrevRetryableWrite,
childLsidForNonRetryableWrite,
childTxnNumber,
};
}
function assertNumEntries(sessionOpts, {numSessionsCollEntries, numTransactionsCollEntries, numImageCollEntries}) {
const filter = {"_id.id": sessionOpts.parentLsid.id};
const sessionsCollEntries = sessionsColl.find(filter).toArray();
assert.eq(numSessionsCollEntries, sessionsCollEntries.length, sessionsCollEntries);
const transactionsCollEntries = transactionsColl.find(filter).toArray();
assert.eq(numTransactionsCollEntries, transactionsCollEntries.length, transactionsCollEntries);
const imageCollEntries = imageColl.find(filter).toArray();
assert.eq(numImageCollEntries, imageCollEntries.length, imageCollEntries);
}
// Test reaping when neither the external session nor the internal sessions are checked out.
{
jsTest.log("Test reaping when there is an in-progress retryable-write internal transaction");
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 1, numImageCollEntries: 1});
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.childLsidForRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(1),
}),
);
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the retryable write in the external session do not get
// reaped since there is an in-progress internal transaction for that retryable write.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 1, numImageCollEntries: 1});
// Retry the write statement executed in the external session.
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
lsid: sessionOpts.childLsidForRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForRetryableWrite, sessionOpts.childTxnNumber),
),
);
// Verify that the retried write statement did not re-execute.
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
{
jsTest.log("Test reaping when there is an in-progress and a committed retryable-write " + "internal transaction");
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.childLsidForRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(1),
}),
);
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 2, numImageCollEntries: 1});
const runInternalTxn = async (primaryHost, parentLsidUUIDString, parentTxnNumber, dbName, collName) => {
const {makeCommitTransactionCmdObj} = await import("jstests/sharding/libs/sharded_transactions_helpers.js");
const primary = new Mongo(primaryHost);
const testDB = primary.getDB(dbName);
const childLsid = {
id: UUID(parentLsidUUIDString),
txnNumber: NumberLong(parentTxnNumber),
txnUUID: UUID(),
};
const childTxnNumber = NumberLong(0);
assert.commandWorked(
testDB.runCommand({
insert: collName,
documents: [{x: 2}],
lsid: childLsid,
txnNumber: childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(2),
}),
);
// Retry the write statement executed in the external session.
assert.commandWorked(
testDB.runCommand({
findAndModify: collName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: childLsid,
txnNumber: childTxnNumber,
autocommit: false,
stmtId: NumberInt(0),
}),
);
// Retry the write statement executed in the committed internal transaction.
assert.commandWorked(
testDB.runCommand({
insert: collName,
documents: [{x: 1}],
lsid: childLsid,
txnNumber: childTxnNumber,
autocommit: false,
stmtId: NumberInt(1),
}),
);
assert.commandWorked(primary.adminCommand(makeCommitTransactionCmdObj(childLsid, childTxnNumber)));
};
// Start another internal transaction in a separate thread, and make it hang right after it
// finishes executing the first statement.
const fp = configureFailPoint(primary, "waitAfterCommandFinishesExecution", {ns: kNs});
const internalTxnThread = new Thread(
runInternalTxn,
primary.host,
extractUUIDFromObject(sessionOpts.sessionUUID),
sessionOpts.parentTxnNumber.valueOf(),
kDbName,
kCollName,
);
internalTxnThread.start();
fp.wait();
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the retryable write in the external session and the
// config.transactions for the committed internal transaction for that retryable write do not
// get reaped since there is an in-progress internal transaction for the same retryable write.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 2, numImageCollEntries: 1});
fp.off();
internalTxnThread.join();
// Verify that the retried write statements did not re-execute.
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.eq(testColl.find({x: 2}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
{
jsTest.log(
"Test reaping when there is an in-progress internal transaction for the current retryable" +
" write and a committed internal transaction for a previous retryable write",
);
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.childLsidForPrevRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForPrevRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(1),
}),
);
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 2}],
lsid: sessionOpts.childLsidForRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(2),
}),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 2, numImageCollEntries: 1});
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the previous write do get reaped.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 1, numImageCollEntries: 0});
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(1),
}),
);
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.eq(testColl.find({x: 2}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
{
jsTest.log(
"Test reaping there is an in-progress transaction in the external session and a " +
"committed internal transaction for a previous retryable write",
);
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.childLsidForPrevRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForPrevRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
startTransaction: true,
autocommit: false,
}),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 1, numImageCollEntries: 1});
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the previous write do get reaped.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(
primary.adminCommand(makeCommitTransactionCmdObj(sessionOpts.parentLsid, sessionOpts.parentTxnNumber)),
);
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
{
jsTest.log(
"Test reaping when there is an in-progress non retryable-write internal transaction " +
"and a committed retryable-write internal transaction",
);
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.childLsidForPrevRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForPrevRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 1, numImageCollEntries: 1});
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.childLsidForNonRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(1),
}),
);
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the retryable write in external session do not get reaped
// since there has not been a retryble write or transaction with a higher txnNumber in the
// logical session.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 1, numImageCollEntries: 1});
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForNonRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
{
jsTest.log(
"Test reaping when there is an in-progress transaction in the external session " +
"and a committed non retryable-write internal transaction",
);
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.childLsidForNonRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForNonRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
startTransaction: true,
autocommit: false,
}),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 1, numImageCollEntries: 0});
// Force the logical session cache to reap, and verify that the config.transactions entry for
// the committed non retryable-write internal transaction does get reaped since it is unrelated
// to the in-progress transaction in the external session.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(
primary.adminCommand(makeCommitTransactionCmdObj(sessionOpts.parentLsid, sessionOpts.parentTxnNumber)),
);
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
// Test reaping when there is a checked out internal session.
{
jsTest.log(
"Test reaping when there is a checked out retryable-write internal session with " +
"an in-progress transaction",
);
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 1, numImageCollEntries: 1});
const runInternalTxn = async (primaryHost, parentLsidUUIDString, parentTxnNumber, dbName, collName) => {
const {makeCommitTransactionCmdObj} = await import("jstests/sharding/libs/sharded_transactions_helpers.js");
const primary = new Mongo(primaryHost);
const testDB = primary.getDB(dbName);
const childLsid = {
id: UUID(parentLsidUUIDString),
txnNumber: NumberLong(parentTxnNumber),
txnUUID: UUID(),
};
const childTxnNumber = NumberLong(0);
assert.commandWorked(
testDB.runCommand({
insert: collName,
documents: [{x: 1}],
lsid: childLsid,
txnNumber: childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(1),
}),
);
// Retry the write statement executed in the external session.
assert.commandWorked(
testDB.runCommand({
findAndModify: collName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: childLsid,
txnNumber: childTxnNumber,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(primary.adminCommand(makeCommitTransactionCmdObj(childLsid, childTxnNumber)));
};
const fp = configureFailPoint(primary, "hangAfterSessionCheckOut", {}, {skip: 1});
const internalTxnThread = new Thread(
runInternalTxn,
primary.host,
extractUUIDFromObject(sessionOpts.sessionUUID),
sessionOpts.parentTxnNumber.valueOf(),
kDbName,
kCollName,
);
internalTxnThread.start();
fp.wait();
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the retryable write in the external session do not get
// reaped.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 1, numImageCollEntries: 1});
fp.off();
internalTxnThread.join();
// Verify that the retried write statement did not re-execute.
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
{
jsTest.log(
"Test reaping when there is a checked out retryable-write internal session " +
"without an in-progress transaction",
);
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 1, numImageCollEntries: 1});
const runInternalTxn = async (primaryHost, parentLsidUUIDString, parentTxnNumber, dbName, collName) => {
const {makeCommitTransactionCmdObj} = await import("jstests/sharding/libs/sharded_transactions_helpers.js");
const primary = new Mongo(primaryHost);
const testDB = primary.getDB(dbName);
const childLsid = {
id: UUID(parentLsidUUIDString),
txnNumber: NumberLong(parentTxnNumber),
txnUUID: UUID(),
};
const childTxnNumber = NumberLong(0);
assert.commandWorked(
testDB.runCommand({
findAndModify: collName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: childLsid,
txnNumber: childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(primary.adminCommand(makeCommitTransactionCmdObj(childLsid, childTxnNumber)));
};
const fp = configureFailPoint(primary, "hangAfterSessionCheckOut");
const internalTxnThread = new Thread(
runInternalTxn,
primary.host,
extractUUIDFromObject(sessionOpts.sessionUUID),
sessionOpts.parentTxnNumber.valueOf(),
kDbName,
kCollName,
);
internalTxnThread.start();
fp.wait();
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the retryable write in the external session do not get
// reaped.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 1, numImageCollEntries: 1});
fp.off();
internalTxnThread.join();
// Verify that the retried write statement did not re-execute.
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
{
jsTest.log(
"Test reaping when there are a checked out retryable-write internal session with " +
"an in-progress transaction and an unchecked out retryable-write internal " +
"session for the same retryable write with a committed transaction",
);
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(
testDB.runCommand({
insert: kCollName,
documents: [{x: 1}],
lsid: sessionOpts.childLsidForRetryableWrite,
txnNumber: sessionOpts.childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(1),
}),
);
assert.commandWorked(
primary.adminCommand(
makeCommitTransactionCmdObj(sessionOpts.childLsidForRetryableWrite, sessionOpts.childTxnNumber),
),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 2, numImageCollEntries: 1});
const runInternalTxn = async (primaryHost, parentLsidUUIDString, parentTxnNumber, dbName, collName) => {
const {makeCommitTransactionCmdObj} = await import("jstests/sharding/libs/sharded_transactions_helpers.js");
const primary = new Mongo(primaryHost);
const testDB = primary.getDB(dbName);
const childLsid = {
id: UUID(parentLsidUUIDString),
txnNumber: NumberLong(parentTxnNumber),
txnUUID: UUID(),
};
const childTxnNumber = NumberLong(0);
assert.commandWorked(
testDB.runCommand({
insert: collName,
documents: [{x: 2}],
lsid: childLsid,
txnNumber: childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(2),
}),
);
// Retry the write statement executed in the external session.
assert.commandWorked(
testDB.runCommand({
findAndModify: collName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: childLsid,
txnNumber: childTxnNumber,
autocommit: false,
stmtId: NumberInt(0),
}),
);
// Retry the write statement executed in the committed internal transaction.
assert.commandWorked(
testDB.runCommand({
insert: collName,
documents: [{x: 1}],
lsid: childLsid,
txnNumber: childTxnNumber,
autocommit: false,
stmtId: NumberInt(1),
}),
);
assert.commandWorked(primary.adminCommand(makeCommitTransactionCmdObj(childLsid, childTxnNumber)));
};
// Start another internal transaction in a separate thread, and make it hang right after it
// finishes executing the first statement.
const fp = configureFailPoint(primary, "hangInsertBeforeWrite", {ns: kNs});
const internalTxnThread = new Thread(
runInternalTxn,
primary.host,
extractUUIDFromObject(sessionOpts.sessionUUID),
sessionOpts.parentTxnNumber.valueOf(),
kDbName,
kCollName,
);
internalTxnThread.start();
fp.wait();
// Force the logical session cache to reap, and verify that the config.transactions and
// config.image_collection entry for the retryable write in the external session and for the
// committed internal transaction for that retryable write do not get reaped since there is an
// in-progress internal transaction for the same retryable write.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 2, numImageCollEntries: 1});
fp.off();
internalTxnThread.join();
// Verify that the retried write statements did not re-execute.
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.eq(testColl.find({x: 1}).itcount(), 1);
assert.eq(testColl.find({x: 2}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
// Test reaping when an internal session is about to be checked out.
{
jsTest.log("Test reaping when a retryable-write internal session is about to be checked out");
const sessionOpts = makeSessionOptsForTest();
assert.commandWorked(testColl.insert([{x: 0, y: 0}]));
assert.commandWorked(
testDB.runCommand({
findAndModify: kCollName,
query: {x: 0},
update: {$inc: {y: 1}},
new: true,
lsid: sessionOpts.parentLsid,
txnNumber: sessionOpts.parentTxnNumber,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(primary.adminCommand({refreshLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 1, numTransactionsCollEntries: 1, numImageCollEntries: 1});
const runInternalTxn = async (primaryHost, parentLsidUUIDString, parentTxnNumber, dbName, collName) => {
const {makeCommitTransactionCmdObj} = await import("jstests/sharding/libs/sharded_transactions_helpers.js");
const primary = new Mongo(primaryHost);
const testDB = primary.getDB(dbName);
const childLsid = {
id: UUID(parentLsidUUIDString),
txnNumber: NumberLong(parentTxnNumber),
txnUUID: UUID(),
};
const childTxnNumber = NumberLong(0);
// Retry the statement executed in the external session.
assert.commandWorked(
testDB.runCommand({
insert: collName,
documents: [{y: 0}],
lsid: childLsid,
txnNumber: childTxnNumber,
startTransaction: true,
autocommit: false,
stmtId: NumberInt(0),
}),
);
assert.commandWorked(testDB.adminCommand(makeCommitTransactionCmdObj(childLsid, childTxnNumber)));
};
const fp = configureFailPoint(primary, "hangBeforeSessionCheckOut");
const internalTxnThread = new Thread(
runInternalTxn,
primary.host,
extractUUIDFromObject(sessionOpts.sessionUUID),
sessionOpts.parentTxnNumber.valueOf(),
kDbName,
kCollName,
);
internalTxnThread.start();
fp.wait();
// Force the logical session cache to reap, and verify that the config.transactions entry and
// config.image_collection entry for the retryable write in the external session do get reaped.
assert.commandWorked(sessionsColl.remove({"_id.id": sessionOpts.sessionUUID}));
rst.awaitReplication();
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
// Verify that the internal transaction did not get interrupted but that the retried write
// statement re-execute, i.e. retryablity is violated because the retry occurs after the session
// got reaped.
fp.off();
internalTxnThread.join();
assert.eq(testColl.find({x: 0, y: 1}).itcount(), 1);
assert.commandWorked(primary.adminCommand({reapLogicalSessionCacheNow: 1}));
assertNumEntries(sessionOpts, {numSessionsCollEntries: 0, numTransactionsCollEntries: 0, numImageCollEntries: 0});
assert.commandWorked(testColl.remove({}));
}
rst.stopSet();