summaryrefslogtreecommitdiff
path: root/jstests/replsets/tenant_migration_donor_rollback_during_cloning.js
blob: 31ea505c6d5fbaa0b93d9e43b44477ae8847f985 (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
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/**
 * Tests that in a tenant migration, values coming from non-majority reads that the recipient's
 * tenant cloner performs (such as 'listCollections' and 'listDatabases') account for donor
 * rollback.
 *
 * @tags: [
 *   incompatible_with_eft,
 *   incompatible_with_macos,
 *   incompatible_with_windows_tls,
 *   requires_majority_read_concern,
 *   requires_persistence,
 * ]
 */

(function() {
"use strict";

load("jstests/libs/fail_point_util.js");
load("jstests/libs/uuid_util.js");           // for 'extractUUIDFromObject'
load("jstests/libs/write_concern_util.js");  // for 'stopReplicationOnSecondaries'
load("jstests/replsets/libs/tenant_migration_test.js");
load("jstests/replsets/libs/tenant_migration_util.js");

const migrationX509Options = TenantMigrationUtil.makeX509OptionsForTest();

const recipientRst =
    new ReplSetTest({name: "recipientRst", nodes: 1, nodeOptions: migrationX509Options.recipient});

recipientRst.startSet();
recipientRst.initiateWithHighElectionTimeout();

if (!TenantMigrationUtil.isFeatureFlagEnabled(recipientRst.getPrimary())) {
    jsTestLog("Skipping test because the tenant migrations feature flag is disabled");
    recipientRst.stopSet();
    return;
}

// This function does the following:
// 1. Runs the setup function, which typically involves loading the donor RST with some data.
// 2. Configures the failpoints passed in to pause. These failpoints usually mark the steps right
//    before and after calling a 'list*' command respectively. For example, the first failpoint may
//    cause the cloner to pause right before it calls 'listCollections', while the second failpoint
//    causes the cloner to pause right after calling 'listCollections'.
// 3. Once the first failpoint has been hit (i.e. right before calling 'list*'), replication is
//    paused on the donor. The 'whilePausedFunction' is run, which performs operations on the donor
//    that will not be majority committed (since we have paused replication). These operations will
//    later be rolled back.
// 4. We allow the 'list*' read to be performed, and then wait at the second failpoint (i.e. after
//    'list*' has been called).
// 5. One of the other nodes from the donor RST is made to step up.
// 6. The second failpoint is lifted. Thus the cloner continues, with the 'list*' read that wasn't
//    majority committed.
// 7. The migration is allowed to completed, and a 'forgetMigration' command is issued.
function runTest(tenantId,
                 setupFunction,
                 whilePausedFunction,
                 postMigrationFunction,
                 firstFailpointData,
                 secondFailpoint) {
    const donorRst = new ReplSetTest({
        name: "donorRst",
        nodes: 5,
        nodeOptions: Object.assign(migrationX509Options.donor, {
            setParameter: {
                // Allow non-timestamped reads on donor after migration completes for testing.
                'failpoint.tenantMigrationDonorAllowsNonTimestampedReads':
                    tojson({mode: 'alwaysOn'}),
            }
        }),
        // Set the 'catchUpTimeoutMillis' to 0, so that the new primary doesn't fetch operations
        // that we want rolled back. Turn off chaining to make sure that the secondary the recipient
        // initially syncs from is able to keep in sync with the primary.
        settings: {catchUpTimeoutMillis: 0, chainingAllowed: false}
    });
    donorRst.startSet();
    donorRst.initiateWithHighElectionTimeout();

    const recipientPrimary = recipientRst.getPrimary();

    const tenantMigrationTest = new TenantMigrationTest(
        {name: jsTestName(), donorRst: donorRst, recipientRst: recipientRst});

    const migrationId = UUID();
    const migrationOpts = {
        migrationIdString: extractUUIDFromObject(migrationId),
        recipientConnString: tenantMigrationTest.getRecipientConnString(),
        tenantId: tenantId,
        readPreference: {mode: 'secondary'}
    };

    firstFailpointData.database = tenantMigrationTest.tenantDB(tenantId, "testDB");
    // The failpoints correspond to the instants right before and after the 'list*' call that the
    // recipient cloners make.
    const fpBeforeListCall =
        configureFailPoint(recipientPrimary, "hangBeforeClonerStage", firstFailpointData);
    const fpAfterListCall = configureFailPoint(recipientPrimary, secondFailpoint);

    // Pause the oplog fetcher to make sure that the cloner's failure rather than the fetcher's
    // failure results in restarting the migration.
    const fpPauseOplogFetcher =
        configureFailPoint(recipientPrimary, "hangBeforeStartingOplogFetcher");

    // Perform any initial setup on the donor before running the tenant migration.
    setupFunction(tenantId, tenantMigrationTest);
    donorRst.awaitReplication();

    jsTestLog(`Starting tenant migration with migrationId: ${
        migrationOpts.migrationIdString}, tenantId: ${tenantId}`);
    assert.commandWorked(tenantMigrationTest.startMigration(migrationOpts));

    // Wait until right before the 'list*' call is made, and then stop replication on the donor RST.
    fpBeforeListCall.wait();

    jsTestLog("Stopping donor replication.");
    // Figure out which donor node the recipient is syncing from.
    let res = recipientPrimary.adminCommand(
        {currentOp: true, desc: "tenant recipient migration", tenantId: tenantId});
    assert.eq(res.inprog.length, 1, res);
    let currOp = res.inprog[0];
    assert.eq(bsonWoCompare(currOp.instanceID, migrationId), 0, res);
    assert.eq(currOp.numRestartsDueToDonorConnectionFailure, 0, res);

    // 'syncSourceNode' is the donor secondary from which the recipient is syncing. 'otherNodes'
    // are the other secondaries in the donor RST.
    let syncSourceNode = undefined;
    let otherNodes = [];

    donorRst.getSecondaries().forEach(node => {
        if (node.host != currOp.donorSyncSource) {
            otherNodes.push(node);
        } else {
            syncSourceNode = node;
        }
    });
    assert.eq(otherNodes.length, 3, otherNodes);
    jsTestLog(`Sync source node: ${syncSourceNode}, other secondaries: ${otherNodes}`);

    stopServerReplication(otherNodes);

    jsTestLog("Performing work that will be rolled back.");
    // Perform some work on the donor primary while replication is paused. This work will not be
    // replicated, and will later be rolled back due to donor primary step down.
    whilePausedFunction(tenantId, syncSourceNode, tenantMigrationTest);
    fpBeforeListCall.off();

    jsTestLog("Stepping a new node up.");
    // Once the 'list*' call has been made, the tenant migration can proceed. The 'list*' call will
    // have returned information that wasn't majority committed. Step up a new primary to expose
    // this situation. Allow replication once again.
    fpAfterListCall.wait();
    const newDonorPrimary = otherNodes[0];
    newDonorPrimary.adminCommand({replSetStepUp: 1});
    restartServerReplication(otherNodes);

    // Advance the cluster time by applying new operations on the new primary. We insert documents
    // into a non-tenant DB, so this data will not be migrated but will still advance the cluster
    // time.
    tenantMigrationTest.insertDonorDB(
        tenantMigrationTest.nonTenantDB(tenantId, 'alternateDB'),
        'alternateColl',
        [{x: "Tom Petty", y: "Free Fallin"}, {x: "Sushin Shyam", y: "Cherathukal"}]);

    fpAfterListCall.off();

    jsTestLog("Make sure that the recipient has had to restart the migration.");
    assert.soon(() => {
        res = recipientPrimary.adminCommand(
            {currentOp: true, desc: "tenant recipient migration", tenantId: tenantId});
        assert.eq(res.inprog.length, 1, res);
        currOp = res.inprog[0];
        assert.eq(bsonWoCompare(currOp.instanceID, migrationId), 0, res);
        return currOp.numRestartsDueToDonorConnectionFailure == 1;
    }, "Expected the recipient to have restarted: " + tojson(res));

    fpPauseOplogFetcher.off();

    jsTestLog("Waiting for migration to complete.");
    TenantMigrationTest.assertCommitted(
        tenantMigrationTest.waitForMigrationToComplete(migrationOpts));

    donorRst.awaitReplication();

    // Test to make sure some conditions post migration have been met.
    postMigrationFunction(tenantId, tenantMigrationTest);

    assert.commandWorked(tenantMigrationTest.forgetMigration(migrationOpts.migrationIdString));

    donorRst.stopSet();
}

// Creates a collection on the donor.
function listCollectionsSetupFunction(tenantId, tenantMigrationTest) {
    const dbName = tenantMigrationTest.tenantDB(tenantId, "testDB");
    tenantMigrationTest.insertDonorDB(dbName, 'testColl');
}

// Creates another collection on the donor, that isn't majority committed due to replication being
// halted.
function listCollectionsWhilePausedFunction(tenantId, syncSourceNode, tenantMigrationTest) {
    const dbName = tenantMigrationTest.tenantDB(tenantId, "testDB");
    const donorPrimary = tenantMigrationTest.getDonorPrimary();
    const donorTemporaryColl = donorPrimary.getDB(dbName).getCollection('tempColl');

    jsTestLog("Inserting a single document into tempColl.");
    assert.commandWorked(
        donorTemporaryColl.insert([{_id: 0, a: "siberian breaks"}], {writeConcern: {w: 2}}));
    assert.eq(1, donorTemporaryColl.find().readConcern('local').itcount());

    jsTestLog("Waiting for it to reach the secondary.");
    assert.soon(() => {
        return syncSourceNode.getDB(dbName)
                   .getCollection('tempColl')
                   .find()
                   .readConcern('local')
                   .itcount() == 1;
    }, "Document did not replicate to secondary on time.");
}

// Makes sure that the collection that the donor RST failed to replicate does not exist on the
// recipient.
function listCollectionsPostMigrationFunction(tenantId, tenantMigrationTest) {
    const dbName = tenantMigrationTest.tenantDB(tenantId, "testDB");
    const recipientPrimary = tenantMigrationTest.getRecipientPrimary();

    const collNames = recipientPrimary.getDB(dbName).getCollectionNames();
    assert.eq(1, collNames.length);
    assert(collNames.includes('testColl'));
}

// Create a database on the donor RST.
function listDatabasesSetupFunction(tenantId, tenantMigrationTest) {
    const dbName = tenantMigrationTest.tenantDB(tenantId, "testDB");
    tenantMigrationTest.insertDonorDB(dbName, 'testColl');
}

// Create another database on the donor RST. This database doesn't exist on a majority of donor RST
// nodes, as replication has been paused.
function listDatabasesWhilePausedFunction(tenantId, syncSourceNode, tenantMigrationTest) {
    const dbTemp = tenantMigrationTest.tenantDB(tenantId, "tempDB");
    const donorPrimary = tenantMigrationTest.getDonorPrimary();
    const donorTemporaryColl = donorPrimary.getDB(dbTemp).getCollection('tempColl');

    jsTestLog("Inserting document into tempDB.");
    assert.commandWorked(
        donorTemporaryColl.insert([{_id: 0, a: "siberian breaks"}], {writeConcern: {w: 2}}));
    assert.eq(1, donorTemporaryColl.find().readConcern('local').itcount());

    jsTestLog("Waiting for it to reach the secondary.");
    assert.soon(() => {
        return syncSourceNode.getDB(dbTemp)
                   .getCollection('tempColl')
                   .find()
                   .readConcern('local')
                   .itcount() == 1;
    }, "Document did not replicate to secondary on time.");
}

// The database that failed to replicate on the donor RST must not exist on the recipient.
function listDatabasesPostMigrationFunction(tenantId, tenantMigrationTest) {
    const recipientPrimary = tenantMigrationTest.getRecipientPrimary();

    // Get all databases corresponding to the given tenant.
    const dbNames = recipientPrimary.adminCommand(
        {listDatabases: 1, nameOnly: true, filter: {"name": new RegExp("^" + tenantId)}});
    assert.eq(1, dbNames.databases.length, dbNames);
    assert.eq(dbNames.databases[0].name, tenantMigrationTest.tenantDB(tenantId, "testDB"), dbNames);
}

function listIndexesSetupFunction(tenantId, tenantMigrationTest) {
    const dbName = tenantMigrationTest.tenantDB(tenantId, "testDB");
    const donorPrimary = tenantMigrationTest.getDonorPrimary();
    const donorColl = donorPrimary.getDB(dbName)['testColl'];

    assert.commandWorked(donorColl.insert([
        {_id: 0, a: "Bonnie and Clyde", b: "Serge Gainsbourg"},
        {_id: 1, a: "Bittersweet Symphony", b: "The Verve"}
    ]));

    assert.commandWorked(donorPrimary.getDB(dbName).runCommand({
        createIndexes: 'testColl',
        indexes: [{key: {a: 1}, name: "a_1"}],
        commitQuorum: "majority"
    }));

    const indexes = donorColl.getIndexes();
    assert.eq(2, indexes.length, indexes);

    const indexNames = indexes.map(function(idx) {
        return idx.name;
    });
    assert(indexNames.includes("_id_"), indexes);
    assert(indexNames.includes("a_1"), indexes);
}

function listIndexesWhilePausedFunction(tenantId, syncSourceNode, tenantMigrationTest) {
    const dbName = tenantMigrationTest.tenantDB(tenantId, "testDB");
    const donorPrimary = tenantMigrationTest.getDonorPrimary();
    const donorDB = donorPrimary.getDB(dbName);
    const donorColl = donorDB['testColl'];

    jsTestLog("Dropping index.");
    assert.commandWorked(
        donorDB.runCommand({dropIndexes: 'testColl', index: "a_1", writeConcern: {w: 2}}));

    let indexes = donorColl.getIndexes();
    assert.eq(1, indexes.length, indexes);

    let indexNames = indexes.map(function(idx) {
        return idx.name;
    });
    assert(indexNames.includes("_id_"), indexes);

    jsTestLog("Making sure index doesn't exist on secondary.");
    assert.soon(() => {
        const collOnSecondary = syncSourceNode.getDB(dbName)['testColl'];
        indexes = collOnSecondary.getIndexes();
        return indexes.length == 1;
    }, `Received the following indexes: ${tojson(indexes)}`);

    indexNames = indexes.map(function(idx) {
        return idx.name;
    });
    assert(indexNames.includes("_id_"), indexes);
}

function listIndexesPostMigrationFunction(tenantId, tenantMigrationTest) {
    const dbTest = tenantMigrationTest.tenantDB(tenantId, "testDB");
    const recipientPrimary = tenantMigrationTest.getRecipientPrimary();
    const testColl = recipientPrimary.getDB(dbTest)['testColl'];

    const indexes = testColl.getIndexes();
    assert.eq(2, indexes.length, indexes);

    const indexNames = indexes.map(function(idx) {
        return idx.name;
    });
    assert(indexNames.includes("_id_"), indexes);
    assert(indexNames.includes("a_1"), indexes);
}

runTest('tenantId1',
        listCollectionsSetupFunction,
        listCollectionsWhilePausedFunction,
        listCollectionsPostMigrationFunction,
        {cloner: "TenantDatabaseCloner", stage: "listCollections"},
        "tenantDatabaseClonerHangAfterGettingOperationTime");

runTest('tenantId2',
        listDatabasesSetupFunction,
        listDatabasesWhilePausedFunction,
        listDatabasesPostMigrationFunction,
        {cloner: "TenantAllDatabaseCloner", stage: "listDatabases"},
        "tenantAllDatabaseClonerHangAfterGettingOperationTime");

runTest('tenantId3',
        listIndexesSetupFunction,
        listIndexesWhilePausedFunction,
        listIndexesPostMigrationFunction,
        {cloner: "TenantCollectionCloner", stage: "listIndexes"},
        "tenantCollectionClonerHangAfterGettingOperationTime");

recipientRst.stopSet();
})();