summaryrefslogtreecommitdiff
path: root/jstests/replsets/recover_prepared_transaction_state.js
blob: 7abb3ae6ed1709bb3468cdfd067ff2ee9d411d29 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/**
 * 1. Test that rollback can successfully recover an aborted prepared transaction that was
 * rolled-back but was in prepare between the stable timestamp and the common point.
 * 2. Test that rollback can successfully recover a committed prepared transaction that was
 * rolled-back but was in prepare before the stable timestamp.
 *
 * This test holds back the stable timestamp and starts two prepared transactions
 * before transitioning to rollback operations, where the branches of history on
 * the rollback node and sync source will diverge. This ensures that we prepare
 * the transactions in between the stable timestamp and the common point.
 *
 * After a rollback of commit/abort, we should correctly reconstruct the two prepared transactions
 * and be able to commit/abort them again.
 *
 * @tags: [
 *   uses_prepare_transaction,
 *   uses_transactions,
 * ]
 */
(function() {
"use strict";

load("jstests/core/txns/libs/prepare_helpers.js");
load("jstests/replsets/libs/rollback_test.js");

const dbName = "test";
const collName = "recover_prepared_transaction_state_after_rollback";

const rollbackTest = new RollbackTest(dbName);
let primary = rollbackTest.getPrimary();

// Create collection we're using beforehand.
const testDB = primary.getDB(dbName);
const testColl = testDB.getCollection(collName);

assert.commandWorked(testDB.runCommand({create: collName}));

// Start two different sessions on the primary.
let session1 = primary.startSession({causalConsistency: false});
let session2 = primary.startSession({causalConsistency: false});

// Save both session IDs so we can later start sessions with the same IDs and commit or
// abort a prepared transaction on them.
const sessionID1 = session1.getSessionId();
const sessionID2 = session2.getSessionId();

let sessionDB1 = session1.getDatabase(dbName);
const sessionColl1 = sessionDB1.getCollection(collName);

let sessionDB2 = session2.getDatabase(dbName);
const sessionColl2 = sessionDB2.getCollection(collName);

assert.commandWorked(sessionColl1.insert({_id: 1}));
assert.commandWorked(sessionColl1.insert({_id: 2}));

rollbackTest.awaitLastOpCommitted();

// Prepare a transaction on the first session whose commit will be rolled-back.
session1.startTransaction();
assert.commandWorked(sessionColl1.insert({_id: 3}));
assert.commandWorked(sessionColl1.update({_id: 1}, {$set: {a: 1}}));
const prepareTimestamp = PrepareHelpers.prepareTransaction(session1);

// Prevent the stable timestamp from moving beyond the following prepared transactions so
// that when we replay the oplog from the stable timestamp, we correctly recover them.
assert.commandWorked(
    primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'alwaysOn'}));

// The following transactions will be prepared before the common point, so they must be in
// prepare after rollback recovery.

// Prepare another transaction on the second session whose abort will be rolled-back.
session2.startTransaction();
assert.commandWorked(sessionColl2.insert({_id: 4}));
assert.commandWorked(sessionColl2.update({_id: 2}, {$set: {b: 2}}));
const prepareTimestamp2 = PrepareHelpers.prepareTransaction(session2, {w: 1});

// Check that we have two transactions in the transactions table.
assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2);

// This characterizes the current behavior of fastcount, which is that the two open transaction
// count toward the value.
assert.eq(testColl.count(), 4);

// The following commit and abort will be rolled back.
rollbackTest.transitionToRollbackOperations();
PrepareHelpers.commitTransaction(session1, prepareTimestamp);
assert.commandWorked(session2.abortTransaction_forTesting());

// The fastcount should be accurate because there are no open transactions.
assert.eq(testColl.count(), 3);

rollbackTest.transitionToSyncSourceOperationsBeforeRollback();
rollbackTest.transitionToSyncSourceOperationsDuringRollback();
try {
    rollbackTest.transitionToSteadyStateOperations({skipDataConsistencyChecks: true});
} finally {
    assert.commandWorked(
        primary.adminCommand({configureFailPoint: 'disableSnapshotting', mode: 'off'}));
}

// Make sure there are two transactions in the transactions table after rollback recovery.
assert.eq(primary.getDB('config')['transactions'].find().itcount(), 2);

// Make sure we can only see the first write and cannot see the writes from the prepared
// transactions or the write that was rolled back.
assert.sameMembers(sessionColl1.find().toArray(), [{_id: 1}, {_id: 2}]);
assert.sameMembers(testColl.find().toArray(), [{_id: 1}, {_id: 2}]);

// This check characterizes the current behavior of fastcount after rollback. It will not be
// correct, but reflects the count at the point where both transactions are not yet committed or
// aborted (because the operations were not majority committed). The count will eventually be
// correct once the commit and abort are retried.
assert.eq(sessionColl1.count(), 4);
assert.eq(testColl.count(), 4);

// Get the correct primary after the topology changes.
primary = rollbackTest.getPrimary();
rollbackTest.awaitReplication();

// Make sure we can successfully commit the first rolled back prepared transaction.
session1 = PrepareHelpers.createSessionWithGivenId(primary, sessionID1, {causalConsistency: false});
sessionDB1 = session1.getDatabase(dbName);
// The next transaction on this session should have a txnNumber of 0. We explicitly set this
// since createSessionWithGivenId does not restore the current txnNumber in the shell.
session1.setTxnNumber_forTesting(0);
const txnNumber1 = session1.getTxnNumber_forTesting();

// Make sure we cannot add any operations to a prepared transaction.
assert.commandFailedWithCode(sessionDB1.runCommand({
    insert: collName,
    txnNumber: NumberLong(txnNumber1),
    documents: [{_id: 10}],
    autocommit: false,
}),
                             ErrorCodes.PreparedTransactionInProgress);

// Make sure that writing to a document that was updated in the first prepared transaction
// causes a write conflict.
assert.commandFailedWithCode(
    sessionDB1.runCommand(
        {update: collName, updates: [{q: {_id: 1}, u: {$set: {a: 2}}}], maxTimeMS: 5 * 1000}),
    ErrorCodes.MaxTimeMSExpired);

const commitTimestamp = Timestamp(prepareTimestamp.getTime(), prepareTimestamp.getInc() + 1);
assert.commandWorked(sessionDB1.adminCommand({
    commitTransaction: 1,
    commitTimestamp: commitTimestamp,
    txnNumber: NumberLong(txnNumber1),
    autocommit: false,
}));
// Retry the commitTransaction command after rollback.
assert.commandWorked(sessionDB1.adminCommand({
    commitTransaction: 1,
    commitTimestamp: commitTimestamp,
    txnNumber: NumberLong(txnNumber1),
    autocommit: false,
}));

// Make sure we can successfully abort the second recovered prepared transaction.
session2 = PrepareHelpers.createSessionWithGivenId(primary, sessionID2, {causalConsistency: false});
sessionDB2 = session2.getDatabase(dbName);
// The next transaction on this session should have a txnNumber of 0. We explicitly set this
// since createSessionWithGivenId does not restore the current txnNumber in the shell.
session2.setTxnNumber_forTesting(0);
const txnNumber2 = session2.getTxnNumber_forTesting();

// Make sure we cannot add any operations to a prepared transaction.
assert.commandFailedWithCode(sessionDB2.runCommand({
    insert: collName,
    txnNumber: NumberLong(txnNumber2),
    documents: [{_id: 10}],
    autocommit: false,
}),
                             ErrorCodes.PreparedTransactionInProgress);

// Make sure that writing to a document that was updated in the second prepared transaction
// causes a write conflict.
assert.commandFailedWithCode(
    sessionDB2.runCommand(
        {update: collName, updates: [{q: {_id: 2}, u: {$set: {b: 3}}}], maxTimeMS: 5 * 1000}),
    ErrorCodes.MaxTimeMSExpired);

assert.commandWorked(sessionDB2.adminCommand({
    abortTransaction: 1,
    txnNumber: NumberLong(txnNumber2),
    autocommit: false,
}));

rollbackTest.awaitReplication();

// Make sure we can see the result of the committed prepared transaction and cannot see the
// write from the aborted transaction.
assert.sameMembers(testColl.find().toArray(), [{_id: 1, a: 1}, {_id: 2}, {_id: 3}]);
assert.eq(testColl.count(), 3);

rollbackTest.stop();
}());