summaryrefslogtreecommitdiff
path: root/jstests/libs/election_timing_test.js
blob: 8ff4daf8bbb8fd6f8ee36ff4f67064391417caf3 (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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/**
 * ElectionTimingTest - set up a ReplSetTest and use default or provided functions to
 *  trigger an election. The time it takes to discover a new primary is recorded.
 */
var ElectionTimingTest = function(opts) {
    // How many times do we start a new ReplSetTest.
    this.testRuns = opts.testRuns || 1;

    // How many times do we step down during a ReplSetTest"s lifetime.
    this.testCycles = opts.testCycles || 1;

    // The config is set to two electable nodes since we use waitForMemberState
    // to wait for the electable secondary to become primary.
    this.nodes = opts.nodes || [
        {},
        {},
        {rsConfig: {arbiterOnly: true}}
    ];

    // The name of the replica set and of the collection.
    this.name = opts.name || "election_timing";

    // Pass additional replicaSet config options.
    this.settings = opts.settings || {};

    // pv1 is the default in master and here.
    this.protocolVersion = opts.hasOwnProperty("protocolVersion") ? opts.protocolVersion : 1;

    // A function that runs after the ReplSetTest is initialized.
    this.testSetup = opts.testSetup || Function.prototype;

    // A function that triggers election, default is to kill the mongod process.
    this.electionTrigger = opts.electionTrigger || this.stopPrimary;

    // A function that waits for new primary to be elected.
    this.waitForNewPrimary = opts.waitForNewPrimary || this.waitForNewPrimary;

    // A function that cleans up after the election trigger.
    this.testReset = opts.testReset || this.stopPrimaryReset;

    // The interval passed to stepdown that primaries may not seek re-election.
    // We also have to wait out this interval before allowing another stepdown.
    this.stepDownGuardTime = opts.stepDownGuardTime || 60;

    // Test results will be stored in these arrays.
    this.testResults = [];
    this.testErrors = [];

    this._runTimingTest();
};

ElectionTimingTest.prototype._runTimingTest = function() {
    for (var run = 0; run < this.testRuns; run++) {
        var collectionName = "test." + this.name;
        var cycleData = {testRun: run, results: []};

        jsTestLog("Starting ReplSetTest for test " + this.name + " run: " + run);
        this.rst = new ReplSetTest({name: this.name, nodes: this.nodes, nodeOptions: {verbose:""}});
        this.rst.startSet();

        // Get the replset config and apply the settings object.
        var conf = this.rst.getReplSetConfig();
        conf.settings = conf.settings || {};
        conf.settings = Object.merge(conf.settings, this.settings);

        // Explicitly setting protocolVersion.
        conf.protocolVersion = this.protocolVersion;
        this.rst.initiate(conf);

        // Run the user supplied testSetup() method. Typical uses would be to set up
        // bridging, or wait for a particular state after initiate().
        try {
            this.testSetup();
        } catch (e) {
            // If testSetup() fails, we are in an unknown state, log and return.
            this.testErrors.push({testRun: run, status: "testSetup() failed", error: e});
            this.rst.stopSet();
            return;
        }

        // Create and populate a collection.
        var primary = this.rst.getPrimary();

        this.electionTimeoutLimitMillis =
            ElectionTimingTest.calculateElectionTimeoutLimitMillis(primary);
        jsTestLog('Election timeout limit: ' + this.electionTimeoutLimitMillis + ' ms');

        var coll = primary.getCollection(collectionName);
        for (var i = 0; i < 100; i++) {
            assert.writeOK(coll.insert({_id: i,
                                        x: i * 3,
                                        arbitraryStr: "this is a string"}));
        }

        // Run the election tests on this ReplSetTest instance.
        var secondary;
        for (var cycle = 0; cycle < this.testCycles; cycle++) {
            // Wait for replication.
            this.rst.awaitSecondaryNodes();
            this.rst.awaitReplication();
            primary = this.rst.getPrimary();
            secondary = this.rst.getSecondary();

            jsTestLog("Starting test: " + this.name + " run: " + run + " cycle: " + cycle);
            var oldElectionId = primary.getDB("admin").isMaster().electionId;

            // Time the new election.
            var stepDownTime = Date.now();

            // Run the specified election trigger method. Default is to sigstop the primary.
            try {
                this.electionTrigger();
            } catch (e) {
                // Left empty on purpose.
            }

            // Wait for the electable secondary to become primary.
            try {
                this.waitForNewPrimary(this.rst, secondary);
            } catch (e) {
                // If we didn"t find a primary, save the error, break so this
                // ReplSetTest is stopped. We can"t continue from a flaky state.
                this.testErrors.push({testRun: run,
                                      cycle: cycle,
                                      status: "new primary not elected",
                                      error: e});
                break;
            }

            var electionCompleteTime = Date.now();

            // Verify we had an election and we have a new primary.
            var newPrimary = this.rst.getPrimary();
            var newElectionId = newPrimary.getDB("admin").isMaster().electionId;
            if (bsonWoCompare(oldElectionId, newElectionId) !== 0) {
                this.testErrors.push({testRun: run,
                                      cycle: cycle,
                                      status: "electionId not changed, no election was triggered"});
                break;
            }

            if (primary.host === newPrimary.host) {
                this.testErrors.push({testRun: run,
                                      cycle: cycle,
                                      status: "Previous primary was re-elected"});
                break;
            }

            cycleData.results.push((electionCompleteTime - stepDownTime) / 1000);

            // If we are running another test on this ReplSetTest, call the reset function.
            if (cycle + 1 < this.testCycles) {
                try {
                    this.testReset();
                } catch (e) {
                    this.testErrors.push({testRun: run,
                                          cycle: cycle,
                                          status: "testReset() failed",
                                          error: e});
                    break;
                }
            }
        }
        this.testResults.push(cycleData);
        this.rst.stopSet();
    }
};

ElectionTimingTest.prototype.stopPrimary = function() {
    this.originalPrimary = this.rst.getNodeId(this.rst.getPrimary());
    this.rst.stop(this.originalPrimary);
};

ElectionTimingTest.prototype.stopPrimaryReset = function() {
    this.rst.restart(this.originalPrimary);
};

ElectionTimingTest.prototype.stepDownPrimary = function() {
    var adminDB = this.rst.getPrimary().getDB("admin");
    adminDB.runCommand({replSetStepDown: this.stepDownGuardTime, force: true});
};

ElectionTimingTest.prototype.stepDownPrimaryReset = function() {
    sleep(this.stepDownGuardTime * 1000);
};

ElectionTimingTest.prototype.waitForNewPrimary = function(rst, secondary) {
    assert.commandWorked(
        secondary.adminCommand({
            replSetTest: 1,
            waitForMemberState: ReplSetTest.State.PRIMARY,
            timeoutMillis: 60 * 1000
        }),
        "node " + secondary.host + " failed to become primary"
    );
};

/**
 * Calculates upper limit for actual failover time in milliseconds.
 */
ElectionTimingTest.calculateElectionTimeoutLimitMillis = function(primary) {
    var configResult = assert.commandWorked(primary.adminCommand({replSetGetConfig: 1}));
    var config = configResult.config;
    // Protocol version is 0 if missing from config.
    var protocolVersion = config.hasOwnProperty("protocolVersion") ? config.protocolVersion : 0;
    var electionTimeoutMillis = 0;
    var electionTimeoutOffsetLimitFraction = 0;
    if (protocolVersion === 0) {
        electionTimeoutMillis = 30000;  // from TopologyCoordinatorImpl::VoteLease::leaseTime
        electionTimeoutOffsetLimitFraction = 0;
    } else {
        electionTimeoutMillis = config.settings.electionTimeoutMillis;
        var getParameterResult = assert.commandWorked(primary.adminCommand({
            getParameter: 1,
            replElectionTimeoutOffsetLimitFraction: 1,
        }));
        electionTimeoutOffsetLimitFraction =
            getParameterResult.replElectionTimeoutOffsetLimitFraction;
    }
    var assertSoonIntervalMillis = 200;  // from assert.js
    var applierDrainWaitMillis = 1000;  // from SyncTail::tryPopAndWaitForMore()
    var electionTimeoutLimitMillis =
        (1 + electionTimeoutOffsetLimitFraction) * electionTimeoutMillis +
        applierDrainWaitMillis +
        assertSoonIntervalMillis;
    return electionTimeoutLimitMillis;
};