summaryrefslogtreecommitdiff
path: root/jstests/replsets/tenant_migration_conflicting_recipient_sync_data_cmds.js
blob: 4edff2c8f395a824460f8ba4e8722fe3ce037a3d (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
/**
 * Test that tenant migration recipient rejects conflicting recipientSyncData commands.
 *
 * @tags: [
 *   incompatible_with_eft,
 *   incompatible_with_macos,
 *   requires_fcv_52,
 *   incompatible_with_windows_tls,
 *   requires_majority_read_concern,
 *   requires_persistence,
 * ]
 */
(function() {

"use strict";
load("jstests/libs/fail_point_util.js");
load("jstests/libs/parallelTester.js");
load("jstests/libs/uuid_util.js");
load("jstests/replsets/libs/tenant_migration_util.js");

var rst =
    new ReplSetTest({nodes: 1, nodeOptions: TenantMigrationUtil.makeX509OptionsForTest().donor});
rst.startSet();
rst.initiate();
const primary = rst.getPrimary();
const configDB = primary.getDB("config");
const configRecipientsColl = configDB["tenantMigrationRecipients"];

const kDonorConnectionString0 = "foo/bar:12345";
const kDonorConnectionString1 = "foo/bar:56789";
const kPrimaryReadPreference = {
    mode: "primary"
};
const kSecondaryReadPreference = {
    mode: "secondary"
};
const kRecipientCertificateForDonor =
    TenantMigrationUtil.getCertificateAndPrivateKey("jstests/libs/tenant_migration_recipient.pem");
const kExpiredRecipientCertificateForDonor = TenantMigrationUtil.getCertificateAndPrivateKey(
    "jstests/libs/tenant_migration_recipient_expired.pem");

TestData.stopFailPointErrorCode = 4880402;

/**
 * Runs recipientSyncData on the given host and returns the response.
 */
function runRecipientSyncDataCmd(primaryHost, {
    migrationIdString,
    tenantId,
    donorConnectionString,
    readPreference,
    recipientCertificateForDonor
}) {
    jsTestLog("Starting a recipientSyncDataCmd for migrationId: " + migrationIdString +
              " tenantId: '" + tenantId + "'");
    const primary = new Mongo(primaryHost);
    const res = primary.adminCommand({
        recipientSyncData: 1,
        migrationId: UUID(migrationIdString),
        donorConnectionString: donorConnectionString,
        tenantId: tenantId,
        readPreference: readPreference,
        startMigrationDonorTimestamp: Timestamp(1, 1),
        recipientCertificateForDonor: recipientCertificateForDonor
    });
    return res;
}

/**
 * Returns an array of currentOp entries for the TenantMigrationRecipientService instances that
 * match the given query.
 */
function getTenantMigrationRecipientCurrentOpEntries(recipientPrimary, query) {
    const cmdObj = Object.assign({currentOp: true, desc: "tenant recipient migration"}, query);
    return assert.commandWorked(recipientPrimary.adminCommand(cmdObj)).inprog;
}

/**
 * Asserts that the string does not contain certificate or private pem string.
 */
function assertNoCertificateOrPrivateKey(string) {
    assert(!string.includes("CERTIFICATE"), "found certificate");
    assert(!string.includes("PRIVATE KEY"), "found private key");
}

const kTenantIdPrefix = "testTenantId";
let tenantCounter = 0;

/**
 * Returns a tenantId that will not match any existing prefix.
 */
function generateUniqueTenantId() {
    return kTenantIdPrefix + tenantCounter++;
}

// Enable the failpoint to stop the tenant migration after persisting the state doc.
assert.commandWorked(primary.adminCommand({
    configureFailPoint: "fpAfterPersistingTenantMigrationRecipientInstanceStateDoc",
    mode: "alwaysOn",
    data: {action: "stop", stopErrorCode: NumberInt(TestData.stopFailPointErrorCode)}
}));

// Test migrations with different migrationIds but identical settings.
(() => {
    const tenantId = generateUniqueTenantId() + "DiffMigrationId";
    // Enable failPoint to pause the migration just as it starts.
    const fpPauseBeforeRunTenantMigrationRecipientInstance =
        configureFailPoint(primary, "pauseBeforeRunTenantMigrationRecipientInstance");

    // Start the conflicting recipientSyncData cmds.
    const migrationOpts0 = {
        migrationIdString: extractUUIDFromObject(UUID()),
        tenantId: tenantId,
        donorConnectionString: kDonorConnectionString0,
        readPreference: kPrimaryReadPreference,
        recipientCertificateForDonor: kRecipientCertificateForDonor
    };
    const migrationOpts1 = Object.extend({}, migrationOpts0, true);
    migrationOpts1.migrationIdString = extractUUIDFromObject(UUID());
    const recipientSyncDataThread0 =
        new Thread(runRecipientSyncDataCmd, primary.host, migrationOpts0);
    const recipientSyncDataThread1 =
        new Thread(runRecipientSyncDataCmd, primary.host, migrationOpts1);
    recipientSyncDataThread0.start();
    recipientSyncDataThread1.start();

    jsTestLog("Waiting until one gets started and hits the failPoint.");
    assert.commandWorked(primary.adminCommand({
        waitForFailPoint: "pauseBeforeRunTenantMigrationRecipientInstance",
        timesEntered: fpPauseBeforeRunTenantMigrationRecipientInstance.timesEntered + 1,
        maxTimeMS: kDefaultWaitForFailPointTimeout
    }));

    // One instance is expected as the tenantId conflict is still unresolved.
    jsTestLog("Fetching current operations before conflict is resolved.");
    const currentOpEntriesBeforeInsert = getTenantMigrationRecipientCurrentOpEntries(
        primary, {desc: "tenant recipient migration", tenantId});
    assert.eq(1, currentOpEntriesBeforeInsert.length, tojson(currentOpEntriesBeforeInsert));

    jsTestLog("Unblocking the tenant migration instance from persisting the state doc.");
    fpPauseBeforeRunTenantMigrationRecipientInstance.off();

    // Check responses for both commands. One will return with
    // ErrorCodes.ConflictingOperationInProgress, and the other with a
    // TestData.stopFailPointErrorCode (a failpoint indicating that we have persisted the document).
    const res0 = assert.commandFailed(recipientSyncDataThread0.returnData());
    const res1 = assert.commandFailed(recipientSyncDataThread1.returnData());

    if (res0.code == TestData.stopFailPointErrorCode) {
        assert.commandFailedWithCode(res0, TestData.stopFailPointErrorCode);
        assert.commandFailedWithCode(res1, ErrorCodes.ConflictingOperationInProgress);
        assertNoCertificateOrPrivateKey(res1.errmsg);
    } else {
        assert.commandFailedWithCode(res0, ErrorCodes.ConflictingOperationInProgress);
        assert.commandFailedWithCode(res1, TestData.stopFailPointErrorCode);
        assertNoCertificateOrPrivateKey(res0.errmsg);
    }

    // One of the two instances should have been cleaned up, and therefore only one will remain.
    const currentOpEntriesAfterInsert = getTenantMigrationRecipientCurrentOpEntries(
        primary, {desc: "tenant recipient migration", tenantId});
    assert.eq(1, currentOpEntriesAfterInsert.length, tojson(currentOpEntriesAfterInsert));

    // Only one instance should have succeeded in persisting the state doc, other should have failed
    // with ErrorCodes.ConflictingOperationInProgress.
    assert.eq(1, configRecipientsColl.count({tenantId: tenantId}));

    // Run another recipientSyncData cmd for the tenant. Since the previous migration hasn't been
    // garbage collected, the migration is considered as active. So this command should fail with
    // ErrorCodes.ConflictingOperationInProgress.
    const migrationOpts2 = Object.extend({}, migrationOpts0, true);
    migrationOpts2.migrationIdString = extractUUIDFromObject(UUID());
    const recipientSyncDataCmd2 = new Thread(runRecipientSyncDataCmd, primary.host, migrationOpts2);
    recipientSyncDataCmd2.start();
    const res2 = recipientSyncDataCmd2.returnData();
    assert.commandFailedWithCode(res2, ErrorCodes.ConflictingOperationInProgress);

    // Collection count should remain the same.
    assert.eq(1, configRecipientsColl.count({tenantId: tenantId}));
    fpPauseBeforeRunTenantMigrationRecipientInstance.off();
})();

/**
 * Tests that if the client runs multiple recipientSyncData commands that would start conflicting
 * migrations, only one of the migrations will start and succeed.
 */
function testConcurrentConflictingMigration(migrationOpts0, migrationOpts1) {
    // Start the conflicting recipientSyncData cmds.
    const recipientSyncDataThread0 =
        new Thread(runRecipientSyncDataCmd, primary.host, migrationOpts0);
    const recipientSyncDataThread1 =
        new Thread(runRecipientSyncDataCmd, primary.host, migrationOpts1);
    recipientSyncDataThread0.start();
    recipientSyncDataThread1.start();

    const res0 = assert.commandFailed(recipientSyncDataThread0.returnData());
    const res1 = assert.commandFailed(recipientSyncDataThread1.returnData());

    if (res0.code == TestData.stopFailPointErrorCode) {
        assert.commandFailedWithCode(res0, TestData.stopFailPointErrorCode);
        assert.commandFailedWithCode(res1, ErrorCodes.ConflictingOperationInProgress);
        assertNoCertificateOrPrivateKey(res1.errmsg);
        assert.eq(1, configRecipientsColl.count({_id: UUID(migrationOpts0.migrationIdString)}));
        assert.eq(1, getTenantMigrationRecipientCurrentOpEntries(primary, {
                         "instanceID": UUID(migrationOpts0.migrationIdString)
                     }).length);
        if (migrationOpts0.migrationIdString != migrationOpts1.migrationIdString) {
            assert.eq(0, configRecipientsColl.count({_id: UUID(migrationOpts1.migrationIdString)}));
            assert.eq(0, getTenantMigrationRecipientCurrentOpEntries(primary, {
                             "instanceID": UUID(migrationOpts1.migrationIdString)
                         }).length);
        } else if (migrationOpts0.tenantId != migrationOpts1.tenantId) {
            assert.eq(0, configRecipientsColl.count({tenantId: migrationOpts1.tenantId}));
            assert.eq(0, getTenantMigrationRecipientCurrentOpEntries(primary, {
                             tenantId: migrationOpts1.tenantId
                         }).length);
        }
    } else {
        assert.commandFailedWithCode(res0, ErrorCodes.ConflictingOperationInProgress);
        assert.commandFailedWithCode(res1, TestData.stopFailPointErrorCode);
        assertNoCertificateOrPrivateKey(res0.errmsg);
        assert.eq(1, configRecipientsColl.count({_id: UUID(migrationOpts1.migrationIdString)}));
        assert.eq(1, getTenantMigrationRecipientCurrentOpEntries(primary, {
                         "instanceID": UUID(migrationOpts1.migrationIdString)
                     }).length);
        if (migrationOpts0.migrationIdString != migrationOpts1.migrationIdString) {
            assert.eq(0, configRecipientsColl.count({_id: UUID(migrationOpts0.migrationIdString)}));
            assert.eq(0, getTenantMigrationRecipientCurrentOpEntries(primary, {
                             "instanceID": UUID(migrationOpts0.migrationIdString)
                         }).length);
        } else if (migrationOpts0.tenantId != migrationOpts1.tenantId) {
            assert.eq(0, configRecipientsColl.count({tenantId: migrationOpts0.tenantId}));
            assert.eq(0, getTenantMigrationRecipientCurrentOpEntries(primary, {
                             tenantId: migrationOpts0.tenantId
                         }).length);
        }
    }
}

// Test reusing a migrationId with different migration settings.

// Test different tenantIds.
(() => {
    const migrationOpts0 = {
        migrationIdString: extractUUIDFromObject(UUID()),
        tenantId: generateUniqueTenantId() + "DiffTenantId",
        donorConnectionString: kDonorConnectionString0,
        readPreference: kPrimaryReadPreference,
        recipientCertificateForDonor: kRecipientCertificateForDonor
    };
    const migrationOpts1 = Object.extend({}, migrationOpts0, true);
    migrationOpts1.tenantId = generateUniqueTenantId() + "DiffTenantId";
    testConcurrentConflictingMigration(migrationOpts0, migrationOpts1);
})();

// Test different donor connection strings.
(() => {
    const migrationOpts0 = {
        migrationIdString: extractUUIDFromObject(UUID()),
        tenantId: generateUniqueTenantId() + "DiffDonorConnString",
        donorConnectionString: kDonorConnectionString0,
        readPreference: kPrimaryReadPreference,
        recipientCertificateForDonor: kRecipientCertificateForDonor
    };
    const migrationOpts1 = Object.extend({}, migrationOpts0, true);
    migrationOpts1.donorConnectionString = kDonorConnectionString1;
    testConcurrentConflictingMigration(migrationOpts0, migrationOpts1);
})();

// Test different read preference.
(() => {
    const migrationOpts0 = {
        migrationIdString: extractUUIDFromObject(UUID()),
        tenantId: generateUniqueTenantId() + "DiffReadPreference",
        donorConnectionString: kDonorConnectionString0,
        readPreference: kPrimaryReadPreference,
        recipientCertificateForDonor: kRecipientCertificateForDonor
    };
    const migrationOpts1 = Object.extend({}, migrationOpts0, true);
    migrationOpts1.readPreference = kSecondaryReadPreference;
    testConcurrentConflictingMigration(migrationOpts0, migrationOpts1);
})();

// Test different certificates.
(() => {
    const migrationOpts0 = {
        migrationIdString: extractUUIDFromObject(UUID()),
        tenantId: generateUniqueTenantId() + "DiffCertificate",
        donorConnectionString: kDonorConnectionString0,
        readPreference: kPrimaryReadPreference,
        recipientCertificateForDonor: kRecipientCertificateForDonor
    };
    const migrationOpts1 = Object.extend({}, migrationOpts0, true);
    migrationOpts1.recipientCertificateForDonor = kExpiredRecipientCertificateForDonor;
    testConcurrentConflictingMigration(migrationOpts0, migrationOpts1);
})();

rst.stopSet();
})();