summaryrefslogtreecommitdiff
path: root/jstests/replsets/prepare_conflict_read_concern_behavior.js
blob: 4ad65f75506db9c073e24b13112f74d9f3c2f8d5 (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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
/**
 * Test calling reads with various read concerns on a prepared transaction. Snapshot, linearizable
 * and afterClusterTime reads are the only reads that should block on a prepared transaction. Reads
 * that happen as part of a write should also block on a prepared transaction.
 *
 * Also test that dbHash, which acquires a collection S lock for reads, does not block on
 * a prepared transaction on secondaries. Otherwise, it would cause deadlocks when the prepared
 * transaction reacquires locks (since locks were yielded on secondaries) at commit time. This test
 * makes sure dbHash and mapReduce do not accept a non local read concern or afterClusterTime and so
 * it is safe for the two commands to ignore prepare conflicts for reads. This test also makes sure
 * mapReduce that does writes is not allowed to run on secondaries.
 *
 * Also test that validate, which acquires collection X lock during its execution, does not block on
 * a prepared transaction on secondaries. Otherwise, it would cause deadlocks when the prepared
 * transaction reacquires locks (since locks were yielded on secondaries) at commit time. This test
 * makes sure the validate command does not accept a non local read concern or afterClusterTime and
 * that it is therefore safe to ignore prepare conflicts during its execution.
 *
 * @tags: [
 *   uses_prepare_transaction,
 *   uses_transactions,
 * ]
 */

(function() {
"use strict";
load("jstests/core/txns/libs/prepare_helpers.js");

const replTest = new ReplSetTest({nodes: 2});
replTest.startSet();
replTest.initiate();

const conn = replTest.getPrimary();

const failureTimeout = 1 * 1000;       // 1 second.
const successTimeout = 5 * 60 * 1000;  // 5 minutes.
const dbName = "test";
const collName = "prepare_conflict_read_concern_behavior";
const collName2 = "prepare_conflict_read_concern_behavior2";
const testDB = conn.getDB(dbName);
const testColl = testDB.getCollection(collName);
const testColl2 = testDB.getCollection(collName2);

const secondary = replTest.getSecondary();
const secondaryTestDB = secondary.getDB(dbName);

// Turn off timestamp reaping so that clusterTimeBeforePrepare doesn't get too old.
assert.commandWorked(testDB.adminCommand({
    configureFailPoint: "WTPreserveSnapshotHistoryIndefinitely",
    mode: "alwaysOn",
}));

function runTest() {
    testDB.runCommand({drop: collName, writeConcern: {w: "majority"}});
    assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));

    testDB.runCommand({drop: collName2, writeConcern: {w: "majority"}});
    assert.commandWorked(testDB.runCommand({create: collName2, writeConcern: {w: "majority"}}));

    const session = conn.startSession({causalConsistency: false});
    const sessionDB = session.getDatabase(dbName);
    const sessionColl = sessionDB.getCollection(collName);

    const read = function(read_concern, timeout, db, coll, num_expected) {
        let res = db.runCommand({
            find: coll,
            filter: {in_prepared_txn: false},
            readConcern: read_concern,
            maxTimeMS: timeout,
        });

        if (num_expected) {
            assert(res.cursor, tojson(res));
            assert.eq(res.cursor.firstBatch.length, num_expected, tojson(res));
        }
        return res;
    };

    const dbHash = function(read_concern, db, timeout = successTimeout) {
        let res = db.runCommand({
            dbHash: 1,
            readConcern: read_concern,
            maxTimeMS: timeout,
        });

        return res;
    };

    const mapReduce = function(
        read_concern, db, outOptions = {inline: 1}, timeout = successTimeout) {
        let map = function() {
            emit(this.a, this.a);
        };
        let reduce = function(key, vals) {
            return 1;
        };
        let res = db.runCommand({
            mapReduce: collName,
            map: map,
            reduce: reduce,
            out: outOptions,
            readConcern: read_concern,
            maxTimeMS: timeout,
        });
        return res;
    };

    const validate = function(read_concern, db, timeout = successTimeout) {
        let res = db.runCommand({
            validate: collName,
            readConcern: read_concern,
            maxTimeMS: timeout,
        });

        return res;
    };

    assert.commandWorked(
        testColl.insert({_id: 1, in_prepared_txn: false}, {writeConcern: {w: "majority"}}));
    assert.commandWorked(testColl.insert({_id: 2, in_prepared_txn: false}));
    assert.commandWorked(testColl2.insert({_id: 1, in_prepared_txn: false}));

    session.startTransaction();
    const clusterTimeBeforePrepare =
        assert.commandWorked(sessionColl.runCommand("insert", {documents: [{_id: 3}]}))
            .operationTime;
    assert.commandWorked(sessionColl.update({_id: 2}, {_id: 2, in_prepared_txn: true}));
    const prepareTimestamp = PrepareHelpers.prepareTransaction(session);

    const clusterTimeAfterPrepare =
        assert
            .commandWorked(testColl.runCommand(
                "insert",
                {documents: [{_id: 4, in_prepared_txn: false}], writeConcern: {w: "majority"}}))
            .operationTime;

    jsTestLog("prepareTimestamp: " + prepareTimestamp + " clusterTimeBeforePrepare: " +
              clusterTimeBeforePrepare + " clusterTimeAfterPrepare: " + clusterTimeAfterPrepare);

    assert.gt(prepareTimestamp, clusterTimeBeforePrepare);
    assert.gt(clusterTimeAfterPrepare, prepareTimestamp);

    jsTestLog("Test read with read concern 'majority' doesn't block on a prepared transaction.");
    assert.commandWorked(read({level: 'majority'}, successTimeout, testDB, collName, 3));

    jsTestLog("Test read with read concern 'local' doesn't block on a prepared transaction.");
    assert.commandWorked(read({level: 'local'}, successTimeout, testDB, collName, 3));

    jsTestLog("Test read with read concern 'available' doesn't block on a prepared transaction.");
    assert.commandWorked(read({level: 'available'}, successTimeout, testDB, collName, 3));

    jsTestLog("Test read with read concern 'linearizable' blocks on a prepared transaction.");
    assert.commandFailedWithCode(read({level: 'linearizable'}, failureTimeout, testDB, collName),
                                 ErrorCodes.MaxTimeMSExpired);

    jsTestLog("Test afterClusterTime read after prepareTimestamp blocks on a prepared " +
              "transaction.");
    assert.commandFailedWithCode(read({level: 'local', afterClusterTime: clusterTimeAfterPrepare},
                                      failureTimeout,
                                      testDB,
                                      collName),
                                 ErrorCodes.MaxTimeMSExpired);

    jsTestLog("Test read with afterClusterTime after prepareTimestamp on non-prepared " +
              "documents doesn't block on a prepared transaction.");
    assert.commandWorked(read({level: 'local', afterClusterTime: clusterTimeAfterPrepare},
                              successTimeout,
                              testDB,
                              collName2,
                              1));

    // dbHash does not accept a non local read concern or afterClusterTime and it also sets
    // ignore_prepare=true during its execution. Therefore, dbHash should never get prepare
    // conflicts on secondaries. dbHash acquires collection S lock for reads and it will be
    // blocked by a prepared transaction that writes to the same collection if it is run on
    // primaries.
    jsTestLog("Test dbHash doesn't support afterClusterTime read.");
    assert.commandFailedWithCode(
        dbHash({level: 'local', afterClusterTime: clusterTimeAfterPrepare}, secondaryTestDB),
        ErrorCodes.InvalidOptions);

    jsTestLog("Test dbHash doesn't support read concern other than local.");
    assert.commandWorked(dbHash({level: 'local'}, secondaryTestDB));
    assert.commandFailedWithCode(dbHash({level: 'available'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(dbHash({level: 'majority'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(dbHash({level: 'snapshot'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(dbHash({level: 'linearizable'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);

    jsTestLog("Test dbHash on secondary doesn't block on a prepared transaction.");
    assert.commandWorked(dbHash({}, secondaryTestDB));
    jsTestLog("Test dbHash on primary blocks on collection S lock which conflicts with " +
              "a prepared transaction.");
    assert.commandFailedWithCode(dbHash({}, testDB, failureTimeout), ErrorCodes.MaxTimeMSExpired);

    // mapReduce does not accept a non local read concern or afterClusterTime and it also sets
    // ignore_prepare=true during its read phase. As mapReduce that writes is not allowed to run
    // on secondaries, mapReduce should never get prepare conflicts on secondaries. mapReduce
    // acquires collection S lock for reads and it will be blocked by a prepared transaction
    // that writes to the same collection if it is run on primaries.
    jsTestLog("Test mapReduce doesn't support afterClusterTime read.");
    assert.commandFailedWithCode(
        mapReduce({level: 'local', afterClusterTime: clusterTimeAfterPrepare}, secondaryTestDB),
        ErrorCodes.InvalidOptions);

    jsTestLog("Test mapReduce doesn't support read concern other than local or available.");
    assert.commandWorked(mapReduce({level: 'local'}, secondaryTestDB));
    assert.commandWorked(mapReduce({level: 'available'}, secondaryTestDB));
    assert.commandFailedWithCode(mapReduce({level: 'majority'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(mapReduce({level: 'snapshot'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(mapReduce({level: 'linearizable'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);

    jsTestLog("Test mapReduce on secondary doesn't block on a prepared transaction.");
    assert.commandWorked(mapReduce({}, secondaryTestDB));

    jsTestLog("Test mapReduce on a primary doesn't block on a prepared transaction.");
    assert.commandWorked(mapReduce({}, testDB));

    // validate does not accept a non local read concern or afterClusterTime and it also sets
    // ignore_prepare=true during its execution. Therefore, validate should never get prepare
    // conflicts on secondaries. validate acquires collection X lock during its execution and it
    // will be blocked by a prepared transaction that writes to the same collection if it is run
    // on primaries.
    jsTestLog("Test validate doesn't support afterClusterTime read.");
    assert.commandFailedWithCode(
        validate({level: 'local', afterClusterTime: clusterTimeAfterPrepare}, secondaryTestDB),
        ErrorCodes.InvalidOptions);
    jsTestLog("Test validate doesn't support read concern other than local.");
    assert.commandWorked(validate({level: 'local'}, secondaryTestDB));
    assert.commandFailedWithCode(validate({level: 'available'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(validate({level: 'majority'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(validate({level: 'snapshot'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);
    assert.commandFailedWithCode(validate({level: 'linearizable'}, secondaryTestDB),
                                 ErrorCodes.InvalidOptions);

    jsTestLog("Test validate on secondary doesn't block on a prepared transaction.");
    assert.commandWorked(validate({}, secondaryTestDB));
    jsTestLog("Test validate on primary blocks on collection X lock which conflicts with " +
              "a prepared transaction.");
    assert.commandFailedWithCode(validate({}, testDB, failureTimeout), ErrorCodes.MaxTimeMSExpired);

    jsTestLog("Test read from an update blocks on a prepared transaction.");
    assert.commandFailedWithCode(testDB.runCommand({
        update: collName,
        updates: [{q: {_id: 2}, u: {_id: 2, in_prepared_txn: false, a: 1}}],
        maxTimeMS: failureTimeout,
    }),
                                 ErrorCodes.MaxTimeMSExpired);

    // Create a second session and start a new transaction to test snapshot reads.
    const session2 = conn.startSession({causalConsistency: false});
    const sessionDB2 = session2.getDatabase(dbName);
    const sessionColl2 = sessionDB2.getCollection(collName);
    // This makes future reads in the transaction use a read timestamp after the
    // prepareTimestamp.
    session2.startTransaction(
        {readConcern: {level: "snapshot", atClusterTime: clusterTimeAfterPrepare}});

    jsTestLog("Test read with read concern 'snapshot' and a read timestamp after " +
              "prepareTimestamp on non-prepared documents doesn't block on a prepared " +
              "transaction.");
    assert.commandWorked(read({}, successTimeout, sessionDB2, collName2, 1));

    jsTestLog("Test read with read concern 'snapshot' and a read timestamp after " +
              "prepareTimestamp blocks on a prepared transaction.");
    assert.commandFailedWithCode(read({}, failureTimeout, sessionDB2, collName),
                                 ErrorCodes.MaxTimeMSExpired);
    assert.commandFailedWithCode(session2.abortTransaction_forTesting(),
                                 ErrorCodes.NoSuchTransaction);

    jsTestLog("Test read with read concern 'snapshot' and atClusterTime before " +
              "prepareTimestamp doesn't block on a prepared transaction.");
    session2.startTransaction(
        {readConcern: {level: "snapshot", atClusterTime: clusterTimeBeforePrepare}});
    assert.commandWorked(read({}, successTimeout, sessionDB2, collName, 2));
    assert.commandWorked(session2.abortTransaction_forTesting());

    jsTestLog("Test read from a transaction with read concern 'majority' blocks on a prepared" +
              " transaction.");
    session2.startTransaction({readConcern: {level: "majority"}});
    assert.commandFailedWithCode(read({}, failureTimeout, sessionDB2, collName),
                                 ErrorCodes.MaxTimeMSExpired);
    assert.commandFailedWithCode(session2.abortTransaction_forTesting(),
                                 ErrorCodes.NoSuchTransaction);

    jsTestLog("Test read from a transaction with read concern 'local' blocks on a prepared " +
              "transaction.");
    session2.startTransaction({readConcern: {level: "local"}});
    assert.commandFailedWithCode(read({}, failureTimeout, sessionDB2, collName),
                                 ErrorCodes.MaxTimeMSExpired);
    assert.commandFailedWithCode(session2.abortTransaction_forTesting(),
                                 ErrorCodes.NoSuchTransaction);

    jsTestLog("Test read from a transaction with no read concern specified blocks on a " +
              "prepared transaction.");
    session2.startTransaction();
    assert.commandFailedWithCode(read({}, failureTimeout, sessionDB2, collName),
                                 ErrorCodes.MaxTimeMSExpired);
    assert.commandFailedWithCode(session2.abortTransaction_forTesting(),
                                 ErrorCodes.NoSuchTransaction);
    session2.endSession();

    assert.commandWorked(session.abortTransaction_forTesting());
    session.endSession();
}

try {
    runTest();
} finally {
    // Turn this failpoint off so that it doesn't impact other tests in the suite.
    assert.commandWorked(testDB.adminCommand({
        configureFailPoint: "WTPreserveSnapshotHistoryIndefinitely",
        mode: "off",
    }));
}

replTest.stopSet();
}());