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

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

if (!RetryableWritesUtil.storageEngineSupportsRetryableWrites(jsTest.options().storageEngine)) {
    jsTestLog("Retryable writes are not supported, skipping test");
    return;
}

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();
})();