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

    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();
            secondary.adminCommand({configureFailPoint: "rsSyncApplyStop", mode: "alwaysOn"});

            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 a misleading fassert()
                // message from being logged by the secondary when it is shut down with the
                // failpoint enabled.
                secondary.adminCommand({configureFailPoint: "rsSyncApplyStop", mode: "off"});
            }
        },
        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();
})();