summaryrefslogtreecommitdiff
path: root/jstests/replsets/read_committed_with_catalog_changes.js
blob: 9e1c349ff3c3b446cf69b967c96ea0fa49de1d5d (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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
/**
 * Test read committed functionality when mixed with catalog changes. Since we don't support
 * multiple versions of the catalog, operations that modify the catalog may need to lock out
 * committed readers until the modification is in the committed snapshot.
 *
 * The following replicated operations are tested here:
 *  - creating a collection in an existing db
 *  - creating a collection in a new db
 *  - dropping a collection
 *  - dropping a db
 *  - dropping a collection and creating one with the same name
 *  - dropping a db and creating one with the same name
 *  - renaming a collection to a new, unused name
 *  - renaming a collection on top of an existing collection
 *  - creating a foreground index
 *  - creating a background index
 *  - dropping an index
 *
 * The following non-replicated operations are tested here:
 *  - repair database
 *  - reindex collection
 *  - compact collection
 *
 * @tags: [
 *   requires_fcv_47,
 *   requires_majority_read_concern,
 * ]
 */

load("jstests/libs/parallelTester.js");  // For Thread.
load("jstests/libs/write_concern_util.js");

(function() {
"use strict";

// Each test case includes a 'prepare' method that sets up the initial state starting with a
// database that has been dropped, a 'performOp' method that does some operation, and two
// arrays, 'blockedCollections' and 'unblockedCollections', that list the collections that
// should be blocked or unblocked between the time the operation is performed until it is
// committed. If the operation is local only and isn't replicated, the test case should include
// a 'localOnly' field set to true. Test cases are not allowed to touch any databases other than
// the one passed in.
const testCases = {
    createCollectionInExistingDB: {
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
        },
        performOp: function(db) {
            assert.commandWorked(db.coll.insert({_id: 1}));
        },
        blockedCollections: ['coll'],
        unblockedCollections: ['other'],
    },
    createCollectionInNewDB: {
        prepare: function(db) {},
        performOp: function(db) {
            assert.commandWorked(db.coll.insert({_id: 1}));
        },
        blockedCollections: ['coll'],
        unblockedCollections: ['otherDoesNotExist'],  // Only existent collections are blocked.
    },
    dropCollection: {
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
            assert.commandWorked(db.coll.insert({_id: 1}));
        },
        performOp: function(db) {
            assert(db.coll.drop());
        },
        blockedCollections: [],
        unblockedCollections: ['coll', 'other'],
    },
    dropDB: {
        prepare: function(db) {
            assert.commandWorked(db.coll.insert({_id: 1}));
            // Drop collection explicitly during the preparation phase while we are still able
            // to write to a majority. Otherwise, dropDatabase() will drop the collection
            // and wait for the collection drop to be replicated to a majority of the nodes.
            assert(db.coll.drop());
        },
        performOp: function(db) {
            assert.commandWorked(db.dropDatabase({w: 1}));
        },
        blockedCollections: [],
        unblockedCollections: ['coll'],
    },
    dropAndRecreateCollection: {
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
            assert.commandWorked(db.coll.insert({_id: 1}));
        },
        performOp: function(db) {
            assert(db.coll.drop());
            assert.commandWorked(db.coll.insert({_id: 1}));
        },
        blockedCollections: ['coll'],
        unblockedCollections: ['other'],
    },
    dropAndRecreateDB: {
        prepare: function(db) {
            assert.commandWorked(db.coll.insert({_id: 1}));
            // Drop collection explicitly during the preparation phase while we are still able
            // to write to a majority. Otherwise, dropDatabase() will drop the collection
            // and wait for the collection drop to be replicated to a majority of the nodes.
            assert(db.coll.drop());
        },
        performOp: function(db) {
            assert.commandWorked(db.dropDatabase({w: 1}));
            assert.commandWorked(db.coll.insert({_id: 1}));
        },
        blockedCollections: ['coll'],
        unblockedCollections: ['otherDoesNotExist'],
    },
    renameCollectionToNewName: {
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
            assert.commandWorked(db.from.insert({_id: 1}));
        },
        performOp: function(db) {
            assert.commandWorked(db.from.renameCollection('coll'));
        },
        blockedCollections: ['coll'],
        unblockedCollections: ['other', 'from' /*doesNotExist*/],
    },
    renameCollectionToExistingName: {
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
            assert.commandWorked(db.from.insert({_id: 'from'}));
            assert.commandWorked(db.coll.insert({_id: 'coll'}));
        },
        performOp: function(db) {
            assert.commandWorked(db.from.renameCollection('coll', true));
        },
        blockedCollections: ['coll'],
        unblockedCollections: ['other', 'from' /*doesNotExist*/],
    },
    createIndex: {
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
            assert.commandWorked(db.coll.insert({_id: 1}));
        },
        performOp: function(db) {
            // This test create indexes with majority of nodes not available for replication.
            // So, disabling index build commit quorum.
            assert.commandWorked(db.coll.createIndex({x: 1}, {}, 0));
        },
        blockedCollections: [],
        unblockedCollections: ['coll', 'other'],
    },
    collMod: {
        prepare: function(db) {
            // This test create indexes with majority of nodes not available for replication.
            // So, disabling index build commit quorum.
            assert.commandWorked(db.coll.createIndex({x: 1}, {expireAfterSeconds: 60 * 60}, 0));
            assert.commandWorked(db.coll.insert({_id: 1, x: 1}));
        },
        performOp: function(db) {
            assert.commandWorked(db.coll.runCommand(
                'collMod', {index: {keyPattern: {x: 1}, expireAfterSeconds: 60 * 61}}));
        },
        blockedCollections: [],
        unblockedCollections: ['coll'],
    },
    dropIndex: {
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
            assert.commandWorked(db.coll.insert({_id: 1}));

            // This test create indexes with majority of nodes not available for replication.
            // So, disabling index build commit quorum.
            assert.commandWorked(db.coll.createIndex({x: 1}, {}, 0));
        },
        performOp: function(db) {
            assert.commandWorked(db.coll.dropIndex({x: 1}));
        },
        blockedCollections: [],
        unblockedCollections: ['coll', 'other'],
    },

    // Remaining case is a local-only operation.
    compact: {
        // At least on WiredTiger, compact is fully inplace so it doesn't need to block readers.
        prepare: function(db) {
            assert.commandWorked(db.other.insert({_id: 1}));
            assert.commandWorked(db.coll.insert({_id: 1}));

            // This test create indexes with majority of nodes not available for replication.
            // So, disabling index build commit quorum.
            assert.commandWorked(db.coll.createIndex({x: 1}, {}, 0));
        },
        performOp: function(db) {
            var res = db.coll.runCommand('compact', {force: true});
            if (res.code != ErrorCodes.CommandNotSupported) {
                // It is fine for a storage engine to support snapshots but not compact. Since
                // compact doesn't block any collections we are fine with doing a no-op here.
                // Other errors should fail the test.
                assert.commandWorked(res);
            }
        },
        blockedCollections: [],
        unblockedCollections: ['coll', 'other'],
        localOnly: true,
    },
};

// Assertion helpers. These must get all state as arguments rather than through closure since
// they may be passed in to a Thread.
function assertReadsBlock(coll) {
    var res = coll.runCommand('find', {"readConcern": {"level": "majority"}, "maxTimeMS": 5000});
    assert.commandFailedWithCode(
        res, ErrorCodes.MaxTimeMSExpired, "Expected read of " + coll.getFullName() + " to block");
}

function assertReadsSucceed(coll, timeoutMs = 20000) {
    var res =
        coll.runCommand('find', {"readConcern": {"level": "majority"}, "maxTimeMS": timeoutMs});
    assert.commandWorked(res, 'reading from ' + coll.getFullName());
    // Exhaust the cursor to avoid leaking cursors on the server.
    new DBCommandCursor(coll.getDB(), res).itcount();
}

// Set up a set and grab things for later.
var name = "read_committed_with_catalog_changes";
var replTest = new ReplSetTest({
    name: name,
    nodes: 3,
    nodeOptions: {enableMajorityReadConcern: ''},
});

replTest.startSet();
var nodes = replTest.nodeList();
var config = {
    "_id": name,
    "members": [
        {"_id": 0, "host": nodes[0]},
        {"_id": 1, "host": nodes[1], priority: 0},
        {"_id": 2, "host": nodes[2], arbiterOnly: true}
    ]
};

replTest.initiate(config);

// Get connections.
var primary = replTest.getPrimary();
var secondary = replTest.getSecondary();

// This is the DB that all of the tests will use.
var mainDB = primary.getDB('mainDB');

// This DB won't be used by any tests so it should always be unblocked.
var otherDB = primary.getDB('otherDB');
var otherDBCollection = otherDB.collection;
assert.commandWorked(otherDBCollection.insert(
    {}, {writeConcern: {w: "majority", wtimeout: ReplSetTest.kDefaultTimeoutMS}}));
assertReadsSucceed(otherDBCollection);

for (var testName in testCases) {
    jsTestLog('Running test ' + testName);
    var test = testCases[testName];

    const setUpInitialState = function setUpInitialState() {
        assert.commandWorked(mainDB.dropDatabase());
        test.prepare(mainDB);
        replTest.awaitReplication();
        // Do some sanity checks.
        assertReadsSucceed(otherDBCollection);
        test.blockedCollections.forEach((name) => assertReadsSucceed(mainDB[name]));
        test.unblockedCollections.forEach((name) => assertReadsSucceed(mainDB[name]));
    };

    // All operations, whether replicated or not, must become visible automatically as long as
    // the secondary is keeping up.
    setUpInitialState();
    test.performOp(mainDB);
    assertReadsSucceed(otherDBCollection);
    test.blockedCollections.forEach((name) => assertReadsSucceed(mainDB[name]));
    test.unblockedCollections.forEach((name) => assertReadsSucceed(mainDB[name]));

    // Return to the initial state, then stop the secondary from applying new writes to prevent
    // them from becoming committed.
    setUpInitialState();
    stopServerReplication(secondary);

    // If the tested operation isn't replicated, do a write to the side collection before
    // performing the operation. This will ensure that the operation happens after an
    // uncommitted write which prevents it from immediately being marked as committed.
    if (test.localOnly) {
        assert.commandWorked(otherDBCollection.insert({}));
    }

    // Perform the op and ensure that blocked collections block and unblocked ones don't.
    test.performOp(mainDB);
    assertReadsSucceed(otherDBCollection);
    test.blockedCollections.forEach((name) => assertReadsBlock(mainDB[name]));
    test.unblockedCollections.forEach((name) => assertReadsSucceed(mainDB[name]));

    // Use background threads to test that reads that start blocked can complete if the
    // operation they are waiting on becomes committed while the read is still blocked.
    // We don't do this when testing auth because Thread's don't propagate auth
    // credentials.
    var threads = jsTest.options().auth ? [] : test.blockedCollections.map((name) => {
        // This function must get all inputs as arguments and can't use closure because it
        // is used in a Thread.
        function bgThread(host, collection, assertReadsSucceed) {
            // Use a longer timeout since we expect to block for a little while (at least 2
            // seconds).
            assertReadsSucceed(new Mongo(host).getCollection(collection), 30 * 1000);
        }
        var thread =
            new Thread(bgThread, primary.host, mainDB[name].getFullName(), assertReadsSucceed);
        thread.start();
        return thread;
    });
    sleep(1000);  // Give the reads a chance to block.

    try {
        // Try the committed read again after sleeping to ensure that it still blocks even if it
        // isn't immediately after the operation.
        test.blockedCollections.forEach((name) => assertReadsBlock(mainDB[name]));

        // Restart oplog application on the secondary and ensure the blocked collections become
        // unblocked.
        restartServerReplication(secondary);
        replTest.awaitReplication();
        test.blockedCollections.forEach((name) => assertReadsSucceed(mainDB[name]));

        // Wait for the threads to complete and report any errors encountered from running them.
        threads.forEach((thread) => {
            thread.join();
            thread.join = () => {};  // Make join a no-op for the finally below.
            assert(!thread.hasFailed(), "One of the threads failed. See above for details.");
        });
    } finally {
        // Make sure we wait for all threads to finish.
        threads.forEach(thread => thread.join());
    }
}

replTest.stopSet();
}());