summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCheahuychou Mao <mao.cheahuychou@gmail.com>2023-02-16 19:11:40 +0000
committerEvergreen Agent <no-reply@evergreen.mongodb.com>2023-02-16 22:45:36 +0000
commitf4050044d6521d3d4f4b5873addd7a9b234df1a7 (patch)
treef29f6214bec7664e0f7dc0f96ba9451af9250c58
parent23b3dd8f1a4346905ff7d8181f824d71c715ab65 (diff)
downloadmongo-f4050044d6521d3d4f4b5873addd7a9b234df1a7.tar.gz
SERVER-73938 Make sure chunk migration can handle a retryable internal transaction whose oplog entries have been truncated
(cherry picked from commit b9f6d6beb02df55ede1276222a56279e7b2d48f2)
-rw-r--r--jstests/sharding/internal_txns/incomplete_transaction_history_during_migration.js89
-rw-r--r--src/mongo/db/s/session_catalog_migration_source.cpp23
2 files changed, 107 insertions, 5 deletions
diff --git a/jstests/sharding/internal_txns/incomplete_transaction_history_during_migration.js b/jstests/sharding/internal_txns/incomplete_transaction_history_during_migration.js
new file mode 100644
index 00000000000..c9b85d2c426
--- /dev/null
+++ b/jstests/sharding/internal_txns/incomplete_transaction_history_during_migration.js
@@ -0,0 +1,89 @@
+/*
+ * Test that chunk migration can migrate a retryable internal transaction whose oplog entries have
+ * been truncated.
+ *
+ * @tags: [requires_fcv_60, uses_transactions, requires_persistence]
+ */
+(function() {
+'use strict';
+
+// This test involves writing directly to the config.transactions collection which is not allowed
+// in a session.
+TestData.disableImplicitSessions = true;
+
+const st = new ShardingTest({shards: 2, rs: {nodes: 1}});
+
+const dbName = 'testDb';
+const collName = 'testColl';
+const ns = dbName + '.' + collName;
+const testDB = st.s.getDB(dbName);
+
+assert.commandWorked(st.s.adminCommand({enableSharding: dbName}));
+st.ensurePrimaryShard(dbName, st.shard0.name);
+
+assert.commandWorked(st.s.getCollection(ns).createIndex({x: 1}));
+assert.commandWorked(st.s.adminCommand({shardCollection: ns, key: {x: 1}}));
+
+assert.commandWorked(st.s.getDB(dbName).runCommand({insert: collName, documents: [{x: 1}]}));
+
+const parentLsid = {
+ id: UUID()
+};
+const parentTxnNumber = NumberLong(35);
+
+const originalChildLsid = {
+ id: parentLsid.id,
+ txnNumber: parentTxnNumber,
+ txnUUID: UUID()
+};
+const childTxnNumber = NumberLong(1);
+
+const updateCmdObj = {
+ update: collName,
+ updates: [{q: {x: 1}, u: {$set: {y: 1}}}],
+ stmtId: NumberInt(0),
+};
+const res0 = assert.commandWorked(testDB.runCommand(Object.assign({}, updateCmdObj, {
+ lsid: originalChildLsid,
+ txnNumber: childTxnNumber,
+ autocommit: false,
+ startTransaction: true
+})));
+assert.eq(res0.nModified, 1, res0);
+assert.commandWorked(st.s.adminCommand({
+ commitTransaction: 1,
+ lsid: originalChildLsid,
+ txnNumber: childTxnNumber,
+ autocommit: false,
+}));
+
+// Manually update the config.transactions document for the retryable internal transaction to point
+// to an invalid op time.
+const shard0ConfigTxnsColl = st.rs0.getPrimary().getCollection("config.transactions");
+const res1 = assert.commandWorked(shard0ConfigTxnsColl.update(
+ {"_id.txnUUID": originalChildLsid.txnUUID},
+ {$set: {lastWriteOpTime: {ts: new Timestamp(100, 1), t: NumberLong(1)}}}));
+assert.eq(res1.nModified, 1, res1);
+
+assert.commandWorked(st.s.adminCommand({split: ns, middle: {x: 0}}));
+assert.commandWorked(st.s.adminCommand({moveChunk: ns, find: {x: 0}, to: st.shard1.shardName}));
+
+assert.commandFailedWithCode(testDB.runCommand(Object.assign(
+ {}, updateCmdObj, {lsid: parentLsid, txnNumber: parentTxnNumber})),
+ ErrorCodes.IncompleteTransactionHistory);
+
+const retryChildLsid = {
+ id: parentLsid.id,
+ txnNumber: parentTxnNumber,
+ txnUUID: UUID()
+};
+assert.commandFailedWithCode(testDB.runCommand(Object.assign({}, updateCmdObj, {
+ lsid: retryChildLsid,
+ txnNumber: childTxnNumber,
+ autocommit: false,
+ startTransaction: true
+})),
+ ErrorCodes.IncompleteTransactionHistory);
+
+st.stop();
+})();
diff --git a/src/mongo/db/s/session_catalog_migration_source.cpp b/src/mongo/db/s/session_catalog_migration_source.cpp
index e6ddaf5151c..704e57ea8c5 100644
--- a/src/mongo/db/s/session_catalog_migration_source.cpp
+++ b/src/mongo/db/s/session_catalog_migration_source.cpp
@@ -50,6 +50,7 @@
#include "mongo/db/transaction_history_iterator.h"
#include "mongo/db/transaction_participant.h"
#include "mongo/db/write_concern.h"
+#include "mongo/logv2/redaction.h"
#include "mongo/platform/random.h"
#include "mongo/s/catalog/type_chunk.h"
#include "mongo/s/shard_key_pattern.h"
@@ -483,11 +484,23 @@ bool SessionCatalogMigrationSource::_handleWriteHistory(WithLock lk, OperationCo
// oplog entries derived from it to the oplog buffer.
if (isInternalSessionForRetryableWrite(*nextOplog->getSessionId())) {
- invariant(nextOplog->getCommandType() == repl::OplogEntry::CommandType::kApplyOps);
- // Derive retryable write oplog entries from this retryable internal transaction
- // applyOps oplog entry, and add them to the oplog buffer.
- _extractOplogEntriesForInternalTransactionForRetryableWrite(
- lk, *nextOplog, &_unprocessedOplogBuffer);
+ if (nextOplog->getCommandType() == repl::OplogEntry::CommandType::kApplyOps) {
+ // Derive retryable write oplog entries from this retryable internal transaction
+ // applyOps oplog entry, and add them to the oplog buffer.
+ _extractOplogEntriesForInternalTransactionForRetryableWrite(
+ lk, *nextOplog, &_unprocessedOplogBuffer);
+ } else {
+ tassert(7393800,
+ str::stream() << "Found an oplog entry for a retrayble internal "
+ "transaction with an unexpected type"
+ << redact(nextOplog->toBSONForLogging()),
+ nextOplog->getOpType() == repl::OpTypeEnum::kNoop);
+ if (!nextOplog->getStatementIds().empty() &&
+ !shouldSkipOplogEntry(nextOplog.value(), _keyPattern, _chunkRange)) {
+ _unprocessedOplogBuffer.emplace_back(*nextOplog);
+ }
+ }
+
continue;
}