mirror of https://github.com/mongodb/mongo
410 lines
14 KiB
JavaScript
410 lines
14 KiB
JavaScript
/*
|
|
* Tests when internal transactions overwrite existing transactions.
|
|
*
|
|
* @tags: [requires_fcv_60, uses_transactions]
|
|
*/
|
|
import {withRetryOnTransientTxnError} from "jstests/libs/auto_retry_transaction_in_sharding.js";
|
|
import {configureFailPoint} from "jstests/libs/fail_point_util.js";
|
|
import {Thread} from "jstests/libs/parallelTester.js";
|
|
import {ShardingTest} from "jstests/libs/shardingtest.js";
|
|
import {extractUUIDFromObject} from "jstests/libs/uuid_util.js";
|
|
|
|
// This test requires running transactions directly against the shard.
|
|
TestData.replicaSetEndpointIncompatible = true;
|
|
|
|
const st = new ShardingTest({shards: 1, rs: {nodes: 2}});
|
|
|
|
const kDbName = "testDb";
|
|
const kCollName = "testColl";
|
|
const testDB = st.rs0.getPrimary().getDB(kDbName);
|
|
assert.commandWorked(testDB[kCollName].insert({x: 1})); // Set up the collection.
|
|
|
|
let clientSession;
|
|
let clientTxnNumber;
|
|
let retryableChildSession;
|
|
let nonRetryableChildSession;
|
|
|
|
withRetryOnTransientTxnError(
|
|
() => {
|
|
jsTest.log("Verify in progress child transactions are aborted by higher txnNumbers");
|
|
|
|
clientTxnNumber = 5;
|
|
clientSession = {id: UUID()};
|
|
retryableChildSession = {
|
|
id: clientSession.id,
|
|
txnUUID: UUID(),
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
};
|
|
nonRetryableChildSession = {id: clientSession.id, txnUUID: UUID()};
|
|
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
|
|
// A new child transaction should abort an existing client transaction.
|
|
clientTxnNumber++;
|
|
retryableChildSession.txnNumber = NumberLong(clientTxnNumber);
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
// The client transaction should have been aborted.
|
|
assert.commandFailedWithCode(
|
|
testDB.adminCommand({
|
|
commitTransaction: 1,
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber - 1),
|
|
autocommit: false,
|
|
}),
|
|
ErrorCodes.TransactionTooOld,
|
|
);
|
|
|
|
// A non-retryable child transaction shouldn't affect retryable operations.
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: nonRetryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
// The retryable child transaction should still be open.
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
find: kCollName,
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
|
|
// A new child transaction should abort a lower child transaction.
|
|
clientTxnNumber++;
|
|
let retryableChildSessionCopy = Object.merge({}, retryableChildSession);
|
|
retryableChildSession.txnNumber = NumberLong(clientTxnNumber);
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
// The child transaction should have been aborted.
|
|
assert.commandFailedWithCode(
|
|
testDB.adminCommand({
|
|
commitTransaction: 1,
|
|
lsid: retryableChildSessionCopy,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
ErrorCodes.TransactionTooOld,
|
|
);
|
|
|
|
// A new client transaction should abort a lower child transaction.
|
|
clientTxnNumber++;
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
// The client transaction should have been aborted.
|
|
assert.commandFailedWithCode(
|
|
testDB.adminCommand({
|
|
commitTransaction: 1,
|
|
lsid: retryableChildSessionCopy,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
ErrorCodes.TransactionTooOld,
|
|
);
|
|
|
|
// A new retryable write should abort a lower child transaction.
|
|
clientTxnNumber++;
|
|
retryableChildSession.txnNumber = NumberLong(clientTxnNumber);
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
clientTxnNumber++;
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
}),
|
|
);
|
|
// The child transaction should have been aborted.
|
|
assert.commandFailedWithCode(
|
|
testDB.adminCommand({
|
|
commitTransaction: 1,
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
ErrorCodes.TransactionTooOld,
|
|
);
|
|
|
|
// The non-retryable child transaction should still be open.
|
|
assert.commandWorked(
|
|
testDB.adminCommand({
|
|
commitTransaction: 1,
|
|
lsid: nonRetryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
},
|
|
() => {
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
autocommit: false,
|
|
});
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
});
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: nonRetryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
withRetryOnTransientTxnError(
|
|
() => {
|
|
jsTest.log("Verify prepared child transactions are not aborted by higher txnNumbers");
|
|
|
|
clientSession = {id: UUID()};
|
|
clientTxnNumber = 5;
|
|
|
|
retryableChildSession = {
|
|
id: clientSession.id,
|
|
txnUUID: UUID(),
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
};
|
|
nonRetryableChildSession = {id: clientSession.id, txnUUID: UUID()};
|
|
|
|
// Prepare a retryable and non-retryable child transaction.
|
|
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: nonRetryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
assert.commandWorked(
|
|
testDB.adminCommand({
|
|
prepareTransaction: 1,
|
|
lsid: nonRetryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
assert.commandWorked(
|
|
testDB.adminCommand({
|
|
prepareTransaction: 1,
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
|
|
// Verify a higher txnNumber cannot be accepted until the retryable transaction exits
|
|
// prepare. Test all three sources of a higher txnNumber: client retryable write, client
|
|
// transaction, and a retryable child session transaction.
|
|
clientTxnNumber++;
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
maxTimeMS: 1000,
|
|
}),
|
|
ErrorCodes.MaxTimeMSExpired,
|
|
);
|
|
|
|
clientTxnNumber++;
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
maxTimeMS: 1000,
|
|
}),
|
|
ErrorCodes.MaxTimeMSExpired,
|
|
);
|
|
|
|
clientTxnNumber++;
|
|
assert.commandFailedWithCode(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: {id: clientSession.id, txnUUID: UUID(), txnNumber: NumberLong(clientTxnNumber)},
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
maxTimeMS: 1000,
|
|
}),
|
|
ErrorCodes.MaxTimeMSExpired,
|
|
);
|
|
|
|
// Verify a transaction blocked on a prepared child transaction can become unstuck and
|
|
// succeed once the child transaction exits prepare.
|
|
const fp = configureFailPoint(
|
|
st.rs0.getPrimary(),
|
|
"waitAfterNewStatementBlocksBehindOpenInternalTransactionForRetryableWrite",
|
|
);
|
|
const newTxnThread = new Thread(
|
|
(host, lsidUUID, txnNumber) => {
|
|
try {
|
|
const lsid = {id: UUID(lsidUUID)};
|
|
|
|
const conn = new Mongo(host);
|
|
assert.commandWorked(
|
|
conn.getDB("foo").runCommand({
|
|
insert: "test",
|
|
documents: [{x: 1}],
|
|
lsid: lsid,
|
|
txnNumber: NumberLong(txnNumber),
|
|
startTransaction: true,
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
assert.commandWorked(
|
|
conn.adminCommand({
|
|
commitTransaction: 1,
|
|
lsid: lsid,
|
|
txnNumber: NumberLong(txnNumber),
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
return {ok: 1};
|
|
} catch (e) {
|
|
return {exception: e};
|
|
}
|
|
},
|
|
st.s.host,
|
|
extractUUIDFromObject(clientSession.id),
|
|
clientTxnNumber,
|
|
);
|
|
newTxnThread.start();
|
|
|
|
// Wait for the side transaction to hit a PreparedTransactionInProgress error, then resolve
|
|
// the prepared transaction and verify the side transaction can successfully complete.
|
|
fp.wait();
|
|
fp.off();
|
|
|
|
assert.commandWorked(
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
|
|
newTxnThread.join();
|
|
const res = newTxnThread.returnData();
|
|
if (res.hasOwnProperty("exception")) {
|
|
throw res.exception;
|
|
}
|
|
|
|
// A higher txnNumber is accepted despite the prepared non-retryable child transaction.
|
|
clientTxnNumber++;
|
|
assert.commandWorked(
|
|
testDB.runCommand({
|
|
insert: kCollName,
|
|
documents: [{x: 1}],
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
}),
|
|
);
|
|
|
|
assert.commandWorked(
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: nonRetryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
}),
|
|
);
|
|
},
|
|
() => {
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: clientSession,
|
|
txnNumber: NumberLong(clientTxnNumber),
|
|
autocommit: false,
|
|
});
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: retryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
});
|
|
testDB.adminCommand({
|
|
abortTransaction: 1,
|
|
lsid: nonRetryableChildSession,
|
|
txnNumber: NumberLong(0),
|
|
autocommit: false,
|
|
});
|
|
},
|
|
);
|
|
|
|
st.stop();
|