summaryrefslogtreecommitdiff
path: root/jstests/noPassthrough/txn_override_causal_consistency.js
blob: eda37592d4dde6314830cde557932cddbe6955ed (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
/**
 * Verifies the network_error_and_txn_override passthrough respects the causal consistency setting
 * on TestData when starting a transaction.
 *
 * @tags: [requires_replication, uses_transactions]
 */
(function() {
"use strict";

const dbName = "test";
const collName = "foo";

const rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();
const conn = new Mongo(rst.getPrimary().host);

// Create the collection so the override doesn't try to when it is not expected.
assert.commandWorked(conn.getDB(dbName).createCollection(collName));

// Override runCommand to add each command it sees to a global array that can be inspected by
// this test and to allow mocking certain responses.
let cmdObjsSeen = [];
let mockNetworkError, mockFirstResponse, mockFirstCommitResponse;
const mongoRunCommandOriginal = Mongo.prototype.runCommand;
Mongo.prototype.runCommand = function runCommandSpy(dbName, cmdObj, options) {
    cmdObjsSeen.push(cmdObj);

    if (mockNetworkError) {
        mockNetworkError = undefined;
        throw new Error("network error");
    }

    if (mockFirstResponse) {
        const mockedRes = mockFirstResponse;
        mockFirstResponse = undefined;
        return mockedRes;
    }

    const cmdName = Object.keys(cmdObj)[0];
    if (cmdName === "commitTransaction" && mockFirstCommitResponse) {
        const mockedRes = mockFirstCommitResponse;
        mockFirstCommitResponse = undefined;
        return mockedRes;
    }

    return mongoRunCommandOriginal.apply(this, arguments);
};

// Runs the given function with a collection from a session made with the sessionOptions on
// TestData and asserts the seen commands that would start a transaction have or do not have
// afterClusterTime.
function inspectFirstCommandForAfterClusterTime(conn, cmdName, isCausal, expectRetry, func) {
    const session = conn.startSession(TestData.sessionOptions);
    const sessionDB = session.getDatabase(dbName);
    const sessionColl = sessionDB[collName];

    cmdObjsSeen = [];
    func(sessionColl);

    // Find all requests sent with the expected command name, in case the scenario allows
    // retrying more than once or expects to end with a commit.
    let cmds = [];
    if (!expectRetry) {
        assert.lte(1, cmdObjsSeen.length);
        cmds.push(cmdObjsSeen[0]);
    } else {
        assert.lt(1, cmdObjsSeen.length);
        cmds = cmdObjsSeen.filter(obj => Object.keys(obj)[0] === cmdName);
    }

    for (let cmd of cmds) {
        if (isCausal) {
            assert(cmd.hasOwnProperty("$clusterTime"),
                   "Expected " + tojson(cmd) + " to have a $clusterTime.");
            assert(cmd.hasOwnProperty("readConcern"),
                   "Expected " + tojson(cmd) + " to have a read concern.");
            assert(cmd.readConcern.hasOwnProperty("afterClusterTime"),
                   "Expected " + tojson(cmd) + " to have an afterClusterTime.");
        } else {
            if (TestData.hasOwnProperty("enableMajorityReadConcern") &&
                TestData.enableMajorityReadConcern === false) {
                // Commands not allowed in a transaction without causal consistency will not
                // have a read concern on variants that don't enable majority read concern.
                continue;
            }

            assert(cmd.hasOwnProperty("readConcern"),
                   "Expected " + tojson(cmd) + " to have a read concern.");
            assert(!cmd.readConcern.hasOwnProperty("afterClusterTime"),
                   "Expected " + tojson(cmd) + " to not have an afterClusterTime.");
        }
    }

    // Run a command not runnable in a transaction to reset the override's transaction state.
    assert.commandWorked(sessionDB.runCommand({ping: 1}));

    session.endSession();
}

// Helper methods for testing specific commands.

function testInsert(conn, isCausal, expectRetry) {
    inspectFirstCommandForAfterClusterTime(conn, "insert", isCausal, expectRetry, (coll) => {
        assert.commandWorked(coll.insert({x: 1}));
    });
}

function testFind(conn, isCausal, expectRetry) {
    inspectFirstCommandForAfterClusterTime(conn, "find", isCausal, expectRetry, (coll) => {
        assert.eq(0, coll.find({y: 1}).itcount());
    });
}

function testCount(conn, isCausal, expectRetry) {
    inspectFirstCommandForAfterClusterTime(conn, "count", isCausal, expectRetry, (coll) => {
        assert.eq(0, coll.count({y: 1}));
    });
}

function testCommit(conn, isCausal, expectRetry) {
    inspectFirstCommandForAfterClusterTime(conn, "find", isCausal, expectRetry, (coll) => {
        assert.eq(0, coll.find({y: 1}).itcount());
        assert.commandWorked(coll.getDB().runCommand({ping: 1}));  // commits the transaction.
    });
}

// Load the txn_override after creating the spy, so the spy will see commands after being
// transformed by the override. Also configure network error retries because several suites use
// both.
TestData.networkErrorAndTxnOverrideConfig = {
    wrapCRUDinTransactions: true,
    retryOnNetworkErrors: true
};
load("jstests/libs/override_methods/network_error_and_txn_override.js");

TestData.logRetryAttempts = true;

// Run a command to guarantee operation time is initialized on the database's session.
assert.commandWorked(conn.getDB(dbName).runCommand({ping: 1}));

function runTest() {
    for (let isCausal of [false, true]) {
        jsTestLog("Testing with isCausal = " + isCausal);
        TestData.sessionOptions = {causalConsistency: isCausal};

        // Commands that accept read and write concern allowed in a transaction.
        testInsert(conn, isCausal, false /*expectRetry*/);
        testFind(conn, isCausal, false /*expectRetry*/);

        // Command that can accept read concern not allowed in a transaction.
        testCount(conn, isCausal, false /*expectRetry*/);

        // Command that attempts to implicitly create a collection.
        conn.getDB(dbName)[collName].drop();
        testInsert(conn, isCausal, false /*expectRetry*/);

        // Command that can accept read concern with retryable error.
        mockFirstResponse = {ok: 0, code: ErrorCodes.CursorKilled};
        testFind(conn, isCausal, true /*expectRetry*/);

        // Commands that can accept read and write concern with network error.
        mockNetworkError = true;
        testInsert(conn, isCausal, true /*expectRetry*/);

        mockNetworkError = true;
        testFind(conn, isCausal, true /*expectRetry*/);

        // Command that can accept read concern not allowed in a transaction with network error.
        mockNetworkError = true;
        testCount(conn, isCausal, true /*expectRetry*/);

        // Commands that can accept read and write concern with transient transaction error.
        mockFirstResponse = {
            ok: 0,
            code: ErrorCodes.NoSuchTransaction,
            errorLabels: ["TransientTransactionError"]
        };
        testFind(conn, isCausal, true /*expectRetry*/);

        mockFirstResponse = {
            ok: 0,
            code: ErrorCodes.NoSuchTransaction,
            errorLabels: ["TransientTransactionError"]
        };
        testInsert(conn, isCausal, true /*expectRetry*/);

        // Transient transaction error on commit attempt.
        mockFirstCommitResponse = {
            ok: 0,
            code: ErrorCodes.NoSuchTransaction,
            errorLabels: ["TransientTransactionError"]
        };
        testCommit(conn, isCausal, true /*expectRetry*/);

        // Network error on commit attempt.
        mockFirstCommitResponse = {ok: 0, code: ErrorCodes.NotWritablePrimary};
        testCommit(conn, isCausal, true /*expectRetry*/);
    }
}

runTest();

// With read concern majority disabled.
TestData.enableMajorityReadConcern = false;
runTest();
delete TestData.enableMajorityReadConcern;

rst.stopSet();
})();