summaryrefslogtreecommitdiff
path: root/jstests/noPassthrough/shell_retry_writes_on_retryable_errors.js
blob: 00ed23780244605824be1f2f57cb9a34bec3ba9b (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
/**
 * Tests that the mongo shell retries exactly once on retryable errors.
 *
 * @tags: [
 *   requires_replication,
 * ]
 */
(function() {
"use strict";

load("jstests/libs/write_concern_util.js");

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

const dbName = "test";
const collName = jsTest.name();

const rsConn = new Mongo(rst.getURL());
const db = rsConn.startSession({retryWrites: true}).getDatabase(dbName);

// We configure the mongo shell to log its retry attempts so there are more diagnostics
// available in case this test ever fails.
TestData.logRetryAttempts = true;

/**
 * The testCommandIsRetried() function serves as the fixture for writing test cases which run
 * commands against the server and assert that the mongo shell retries them correctly.
 *
 * The 'testFn' parameter is a function that performs an arbitrary number of operations against
 * the database. The command requests that the mongo shell attempts to send to the server
 * (including any command requests which are retried) are then specified as the sole argument to
 * the 'assertFn' parameter.
 *
 * The testFn(enableCapture, disableCapture) function can also selectively turn on and off the
 * capturing of command requests by calling the functions it receives for its first and second
 * parameters, respectively.
 */
function testCommandIsRetried(testFn, assertFn) {
    const mongoRunCommandOriginal = Mongo.prototype.runCommand;
    const cmdObjsSeen = [];

    let shouldCaptureCmdObjs = true;

    Mongo.prototype.runCommand = function runCommandSpy(dbName, cmdObj, options) {
        if (shouldCaptureCmdObjs) {
            cmdObjsSeen.push(cmdObj);
        }

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

    try {
        assert.doesNotThrow(() => testFn(
                                () => {
                                    shouldCaptureCmdObjs = true;
                                },
                                () => {
                                    shouldCaptureCmdObjs = false;
                                }));
    } finally {
        Mongo.prototype.runCommand = mongoRunCommandOriginal;
    }

    if (cmdObjsSeen.length === 0) {
        throw new Error("Mongo.prototype.runCommand() was never called: " + testFn.toString());
    }

    assertFn(cmdObjsSeen);
}

testCommandIsRetried(
    function testInsertRetriedOnWriteConcernError(enableCapture, disableCapture) {
        disableCapture();
        const secondary = rst.getSecondary();
        stopServerReplication(secondary);

        try {
            enableCapture();
            const res = db[collName].insert({}, {writeConcern: {w: 2, wtimeout: 1000}});
            assert.commandFailedWithCode(res, ErrorCodes.WriteConcernFailed);
            disableCapture();
        } finally {
            // We disable the failpoint in a finally block to prevent issues arising from shutting
            // down the secondary with the failpoint enabled.
            restartServerReplication(secondary);
        }
    },
    function assertInsertRetriedExactlyOnce(cmdObjsSeen) {
        assert.eq(2, cmdObjsSeen.length, () => tojson(cmdObjsSeen));
        assert(cmdObjsSeen.every(cmdObj => Object.keys(cmdObj)[0] === "insert"),
               () => "expected both attempts to be insert requests: " + tojson(cmdObjsSeen));
        assert.eq(cmdObjsSeen[0], cmdObjsSeen[1], "command request changed between retry attempts");
    });

testCommandIsRetried(
    function testUpdateRetriedOnRetryableCommandError(enableCapture, disableCapture) {
        disableCapture();

        const primary = rst.getPrimary();
        primary.adminCommand({
            configureFailPoint: "onPrimaryTransactionalWrite",
            data: {
                closeConnection: false,
                failBeforeCommitExceptionCode: ErrorCodes.InterruptedDueToReplStateChange
            },
            mode: {times: 1}
        });

        enableCapture();
        const res = db[collName].update({}, {$set: {a: 1}});
        assert.commandWorked(res);
        disableCapture();

        primary.adminCommand({configureFailPoint: "onPrimaryTransactionalWrite", mode: "off"});
    },
    function assertUpdateRetriedExactlyOnce(cmdObjsSeen) {
        assert.eq(2, cmdObjsSeen.length, () => tojson(cmdObjsSeen));
        assert(cmdObjsSeen.every(cmdObj => Object.keys(cmdObj)[0] === "update"),
               () => "expected both attempts to be update requests: " + tojson(cmdObjsSeen));
        assert.eq(cmdObjsSeen[0], cmdObjsSeen[1], "command request changed between retry attempts");
    });

rst.stopSet();
})();