182 lines
8.0 KiB
JavaScript
182 lines
8.0 KiB
JavaScript
//
|
|
// Test to verify that updates that would change the resharding key value are replicated as an
|
|
// insert, delete pair.
|
|
// @tags: [
|
|
// uses_atclustertime,
|
|
// ]
|
|
//
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
load('jstests/libs/discover_topology.js');
|
|
load('jstests/sharding/libs/resharding_test_fixture.js');
|
|
load('jstests/sharding/libs/sharded_transactions_helpers.js');
|
|
load("jstests/sharding/updateOne_without_shard_key/libs/write_without_shard_key_test_util.js");
|
|
|
|
const reshardingTest = new ReshardingTest({numDonors: 2, numRecipients: 2, reshardInPlace: true});
|
|
reshardingTest.setup();
|
|
|
|
const donorShardNames = reshardingTest.donorShardNames;
|
|
const testColl = reshardingTest.createShardedCollection({
|
|
ns: 'test.foo',
|
|
shardKeyPattern: {x: 1, s: 1},
|
|
chunks: [
|
|
{min: {x: MinKey, s: MinKey}, max: {x: 5, s: 5}, shard: donorShardNames[0]},
|
|
{min: {x: 5, s: 5}, max: {x: MaxKey, s: MaxKey}, shard: donorShardNames[1]},
|
|
],
|
|
});
|
|
|
|
const docToUpdate = ({_id: 0, x: 2, s: 2, y: 2});
|
|
assert.commandWorked(testColl.insert(docToUpdate));
|
|
|
|
let retryableWriteTs;
|
|
let txnWriteTs;
|
|
|
|
const mongos = testColl.getMongo();
|
|
|
|
const updateDocumentShardKeyUsingTransactionApiEnabled =
|
|
isUpdateDocumentShardKeyUsingTransactionApiEnabled(mongos);
|
|
|
|
const recipientShardNames = reshardingTest.recipientShardNames;
|
|
reshardingTest.withReshardingInBackground( //
|
|
{
|
|
newShardKeyPattern: {y: 1, s: 1},
|
|
newChunks: [
|
|
{min: {y: MinKey, s: MinKey}, max: {y: 5, s: 5}, shard: recipientShardNames[0]},
|
|
{min: {y: 5, s: 5}, max: {y: MaxKey, s: MaxKey}, shard: recipientShardNames[1]},
|
|
],
|
|
},
|
|
(tempNs) => {
|
|
// Wait for cloning to have at least started on the recipient shards to know that the donor
|
|
// shards have begun including the "destinedRecipient" field in their oplog entries.
|
|
const tempColl = mongos.getCollection(tempNs);
|
|
assert.soon(() => tempColl.findOne(docToUpdate) !== null);
|
|
|
|
// When the updateDocumentShardKeyUsingTransactionApi feature flag is enabled, ordinary
|
|
// updates that modify a document's shard key will complete.
|
|
assert.commandWorked(testColl.insert({_id: 1, x: 2, s: 2, y: 2}));
|
|
const updateRes = testColl.update({_id: 1, x: 2, s: 2}, {$set: {y: 10}});
|
|
if (updateDocumentShardKeyUsingTransactionApiEnabled) {
|
|
assert.commandWorked(updateRes);
|
|
} else {
|
|
assert.commandFailedWithCode(
|
|
updateRes,
|
|
ErrorCodes.IllegalOperation,
|
|
'was able to update value under new shard key as ordinary write');
|
|
}
|
|
|
|
const session = testColl.getMongo().startSession({retryWrites: true});
|
|
const sessionColl =
|
|
session.getDatabase(testColl.getDB().getName()).getCollection(testColl.getName());
|
|
|
|
assert.commandFailedWithCode(
|
|
sessionColl.update({_id: 0, x: 2, s: 2}, {$set: {y: 10}}, {multi: true}),
|
|
ErrorCodes.InvalidOptions,
|
|
'was able to update value under new shard key when {multi: true} specified');
|
|
|
|
// Sharded updateOnes that do not directly target a shard can now use the two phase write
|
|
// protocol to execute.
|
|
if (WriteWithoutShardKeyTestUtil.isWriteWithoutShardKeyFeatureEnabled(mongos)) {
|
|
// TODO: SERVER-70581 Handle WCOS for update and findAndModify if replacement document
|
|
// changes data placement
|
|
// assert.commandWorked(sessionColl.update({_id: 0}, {$set: {y: 10}}));
|
|
} else {
|
|
assert.commandFailedWithCode(
|
|
sessionColl.update({_id: 0}, {$set: {y: 10}}),
|
|
31025,
|
|
'was able to update value under new shard key without specifying the full shard ' +
|
|
'key in the query');
|
|
}
|
|
|
|
let res;
|
|
assert.soon(
|
|
() => {
|
|
res = sessionColl.update({_id: 0, x: 2, s: 2}, {$set: {y: 20, s: 3}});
|
|
|
|
if (res.nModified === 1) {
|
|
assert.commandWorked(res);
|
|
retryableWriteTs = session.getOperationTime();
|
|
return true;
|
|
}
|
|
|
|
assert.commandFailedWithCode(res, [
|
|
ErrorCodes.StaleConfig,
|
|
ErrorCodes.NoSuchTransaction,
|
|
ErrorCodes.ShardCannotRefreshDueToLocksHeld
|
|
]);
|
|
return false;
|
|
},
|
|
() => `was unable to update value under new shard key as retryable write: ${
|
|
tojson(res)}`);
|
|
|
|
assert.soon(() => {
|
|
session.startTransaction();
|
|
res = sessionColl.update({_id: 0, x: 2, s: 3}, {$set: {y: -30, s: 1}});
|
|
|
|
if (res.nModified === 1) {
|
|
session.commitTransaction();
|
|
txnWriteTs = session.getOperationTime();
|
|
return true;
|
|
}
|
|
|
|
// mongos will automatically retry the update as a pair of delete and insert commands in
|
|
// a multi-document transaction. We permit NoSuchTransaction errors because it is
|
|
// possible for the resharding operation running in the background to cause the shard
|
|
// version to be bumped. The StaleConfig error won't be automatically retried by mongos
|
|
// for the second statement in the transaction (the insert) and would lead to a
|
|
// NoSuchTransaction error.
|
|
if (updateDocumentShardKeyUsingTransactionApiEnabled) {
|
|
// The handling of WCOS errors with internal transactions advances the router's
|
|
// notion of the transaction "statement" number between the initial update, the
|
|
// delete, and the insert, so if the shard version changes and is detected by the
|
|
// delete or insert, the router will refuse to retry and return the stale version
|
|
// error instead.
|
|
assert.commandFailedWithCode(res, [
|
|
ErrorCodes.NoSuchTransaction,
|
|
ErrorCodes.ShardCannotRefreshDueToLocksHeld,
|
|
ErrorCodes.NoSuchTransaction
|
|
]);
|
|
} else {
|
|
assert.commandFailedWithCode(res, ErrorCodes.NoSuchTransaction);
|
|
}
|
|
session.abortTransaction();
|
|
return false;
|
|
}, () => `was unable to update value under new shard key in transaction: ${tojson(res)}`);
|
|
});
|
|
|
|
const topology = DiscoverTopology.findConnectedNodes(mongos);
|
|
const donor0 = new Mongo(topology.shards[donorShardNames[0]].primary);
|
|
const donorOplogColl0 = donor0.getCollection('local.oplog.rs');
|
|
|
|
function assertOplogEntryIsDeleteInsertApplyOps(entry, isRetryableWrite) {
|
|
assert(entry.o.hasOwnProperty('applyOps'), entry);
|
|
if (updateDocumentShardKeyUsingTransactionApiEnabled && isRetryableWrite) {
|
|
// With internal transactions the applyOps array for a retryable write update will have a
|
|
// noop entry at the front.
|
|
assert.eq(entry.o.applyOps.length, 3, entry);
|
|
assert.eq(entry.o.applyOps[0].op, 'n', entry);
|
|
assert.eq(entry.o.applyOps[1].op, 'd', entry);
|
|
assert.eq(entry.o.applyOps[1].ns, testColl.getFullName(), entry);
|
|
assert.eq(entry.o.applyOps[2].op, 'i', entry);
|
|
assert.eq(entry.o.applyOps[2].ns, testColl.getFullName(), entry);
|
|
} else {
|
|
assert.eq(entry.o.applyOps.length, 2, entry);
|
|
assert.eq(entry.o.applyOps[0].op, 'd', entry);
|
|
assert.eq(entry.o.applyOps[0].ns, testColl.getFullName(), entry);
|
|
assert.eq(entry.o.applyOps[1].op, 'i', entry);
|
|
assert.eq(entry.o.applyOps[1].ns, testColl.getFullName(), entry);
|
|
}
|
|
}
|
|
|
|
const retryableWriteEntry = donorOplogColl0.findOne({ts: retryableWriteTs});
|
|
assert.neq(null, retryableWriteEntry, 'failed to find oplog entry for retryable write');
|
|
assertOplogEntryIsDeleteInsertApplyOps(retryableWriteEntry, true /* isRetryableWrite */);
|
|
|
|
const txnWriteEntry = donorOplogColl0.findOne({ts: txnWriteTs});
|
|
assert.neq(null, txnWriteEntry, 'failed to find oplog entry for transaction');
|
|
assertOplogEntryIsDeleteInsertApplyOps(txnWriteEntry);
|
|
|
|
reshardingTest.teardown();
|
|
})();
|