summaryrefslogtreecommitdiff
path: root/jstests/replsets/speculative_transaction.js
blob: 60cab2ac951b3e7f6d39aae1ac901e7d37e1cc19 (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
/**
 * Test that transactions are executed speculatively.  This means two transactions affecting
 * the same document can run back to back without waiting for the first transaction to
 * commit to a majority.
 *
 * @tags: [uses_transactions, requires_majority_read_concern]
 */
(function() {
"use strict";
load("jstests/libs/write_concern_util.js");  // For stopServerReplication

const dbName = "test";
const collName = "speculative_transaction";

const rst = new ReplSetTest({name: collName, nodes: [{}, {rsConfig: {priority: 0}}]});
rst.startSet();
rst.initiate();

const primary = rst.getPrimary();
const secondary = rst.getSecondary();
var testDB = primary.getDB(dbName);
const coll = testDB[collName];
// The default WC is majority and stopServerReplication will prevent satisfying any majority writes.
assert.commandWorked(primary.adminCommand(
    {setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}, writeConcern: {w: "majority"}}));

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

    // Do an initial write so we have something to update.
    assert.commandWorked(coll.insert([{_id: 0}, {_id: 1}], {w: "majority"}));
    rst.awaitLastOpCommitted();

    // Stop replication on the secondary so the majority commit never moves forward.
    stopServerReplication(secondary);

    // Initiate a session on the primary.
    const session = testDB.getMongo().startSession(sessionOptions);
    const sessionDb = session.getDatabase(dbName);
    const sessionColl = sessionDb.getCollection(collName);

    // Start the first transaction.  Do not use majority commit for this one.
    jsTestLog("Starting first transaction");
    session.startTransaction({readConcern: {level: "snapshot"}, writeConcern: {w: 1}});

    assert.commandWorked(sessionColl.update({_id: 0}, {$set: {x: 1}}));

    assert.commandWorked(session.commitTransaction_forTesting());

    // The document should be updated on the local snapshot.
    assert.eq(coll.findOne({_id: 0}), {_id: 0, x: 1});

    // The document should not be updated in the majority snapshot.
    assert.eq(coll.find({_id: 0}).readConcern("majority").next(), {_id: 0});

    jsTestLog("Starting second transaction");
    // Start a second transaction.  Still do not use majority commit for this one.
    session.startTransaction({readConcern: {level: "snapshot"}, writeConcern: {w: 1}});

    // We should see the updated doc within the transaction as a result of speculative read
    // concern.
    assert.eq(sessionColl.findOne({_id: 0}), {_id: 0, x: 1});

    // Update it again.
    assert.commandWorked(sessionColl.update({_id: 0}, {$inc: {x: 1}}));

    // Update a different document outside the transaction.
    assert.commandWorked(coll.update({_id: 1}, {$set: {y: 1}}));

    // Within the transaction, we should not see the out-of-transaction update.
    assert.eq(sessionColl.findOne({_id: 1}), {_id: 1});

    assert.commandWorked(session.commitTransaction_forTesting());

    // The document should be updated on the local snapshot.
    assert.eq(coll.findOne({_id: 0}), {_id: 0, x: 2});

    // The document should not be updated in the majority snapshot.
    assert.eq(coll.find({_id: 0}).readConcern("majority").next(), {_id: 0});

    // Make sure write conflicts are caught with speculative transactions.
    jsTestLog("Starting a conflicting transaction which will be auto-aborted");
    session.startTransaction({readConcern: {level: "snapshot"}, writeConcern: {w: 1}});

    // Read some data inside the transaction.
    assert.eq(sessionColl.findOne({_id: 1}), {_id: 1, y: 1});

    // Write it outside the transaction.
    assert.commandWorked(coll.update({_id: 1}, {$inc: {x: 1}}));

    // Can still read old data in transaction.
    assert.eq(sessionColl.findOne({_id: 1}), {_id: 1, y: 1});

    // But update fails
    assert.commandFailedWithCode(sessionColl.update({_id: 1}, {$inc: {x: 1}}),
                                 ErrorCodes.WriteConflict);

    assert.commandFailedWithCode(session.abortTransaction_forTesting(),
                                 ErrorCodes.NoSuchTransaction);

    // Restart server replication to allow majority commit point to advance.
    restartServerReplication(secondary);

    jsTestLog("Starting final transaction (with majority commit)");
    // Start a third transaction, with majority commit.
    session.startTransaction({readConcern: {level: "snapshot"}, writeConcern: {w: "majority"}});

    // We should see the updated doc within the transaction.
    assert.eq(sessionColl.findOne({_id: 0}), {_id: 0, x: 2});

    // Update it one more time.
    assert.commandWorked(sessionColl.update({_id: 0}, {$inc: {x: 1}}));

    assert.commandWorked(session.commitTransaction_forTesting());

    // The document should be updated on the local snapshot.
    assert.eq(coll.findOne({_id: 0}), {_id: 0, x: 3});

    // The document should also be updated in the majority snapshot.
    assert.eq(coll.find({_id: 0}).readConcern("majority").next(), {_id: 0, x: 3});

    session.endSession();
}

runTest({causalConsistency: false});
runTest({causalConsistency: true});
rst.stopSet();
}());