summaryrefslogtreecommitdiff
path: root/jstests/sharding/update_replace_id.js
blob: 5c80a54840c110a69144a12fc2f9dd0544bec717 (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
/**
 * Test to confirm that mongoS's special handling of replacement updates with an exact query on _id
 * behaves as expected in the case where a collection's shard key includes _id:
 *
 * - For update replacements, mongoS combines the _id from the query with the replacement document
 * to target the query towards a single shard, rather than scattering to all shards.
 * - For upsert replacements, which always require an exact shard key match, mongoS combines the _id
 * from the query with the replacement document to produce a complete shard key.
 *
 * These special cases are allowed because mongoD always propagates the _id of an existing document
 * into its replacement, and in the case of an upsert will use the value of _id from the query
 * filter.
 *
 * @tags: [
 *   uses_multi_shard_transactions,
 *   uses_transactions,
 * ]
 */
(function() {
load("jstests/libs/profiler.js");  // For profilerHas*OrThrow helper functions.

// Test deliberately inserts orphans outside of migrations.
TestData.skipCheckOrphans = true;

const st = new ShardingTest({shards: 2, mongos: 1, other: {enableBalancer: false}});

const mongosDB = st.s0.getDB(jsTestName());
const mongosColl = mongosDB.test;

const shard0DB = st.shard0.getDB(jsTestName());
const shard1DB = st.shard1.getDB(jsTestName());

assert.commandWorked(mongosDB.dropDatabase());

// Enable sharding on the test DB and ensure its primary is shard0.
assert.commandWorked(mongosDB.adminCommand({enableSharding: mongosDB.getName()}));
st.ensurePrimaryShard(mongosDB.getName(), st.shard0.shardName);

// Enables profiling on both shards so that we can verify the targeting behaviour.
function restartProfiling() {
    for (let shardDB of [shard0DB, shard1DB]) {
        shardDB.setProfilingLevel(0);
        shardDB.system.profile.drop();
        shardDB.setProfilingLevel(2);
    }
}

function setUpData() {
    // Write a single document to shard0 and verify that it is present.
    mongosColl.insert({_id: -100, a: -100, msg: "not_updated"});
    assert.docEq(shard0DB.test.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "not_updated"}]);

    // Write a document with the same key directly to shard1. This simulates an orphaned
    // document, or the duplicate document which temporarily exists during a chunk migration.
    shard1DB.test.insert({_id: -100, a: -100, msg: "not_updated"});

    // Clear and restart the profiler on both shards.
    restartProfiling();
}

function runReplacementUpdateTestsForHashedShardKey() {
    setUpData();

    // Perform a replacement update whose query is an exact match on _id and whose replacement
    // document contains the remainder of the shard key. Despite the fact that the replacement
    // document does not contain the entire shard key, we expect that mongoS will extract the
    // _id from the query and combine it with the replacement doc to target a single shard.
    let writeRes = assert.commandWorked(
        mongosColl.update({_id: -100}, {a: -100, msg: "update_extracted_id_from_query"}));

    // Verify that the update did not modify the orphan document.
    assert.docEq(shard1DB.test.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "not_updated"}]);
    assert.eq(writeRes.nMatched, 1);
    assert.eq(writeRes.nModified, 1);

    // Verify that the update only targeted shard0 and that the resulting document appears as
    // expected.
    assert.docEq(mongosColl.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "update_extracted_id_from_query"}]);
    profilerHasSingleMatchingEntryOrThrow({
        profileDB: shard0DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });
    profilerHasZeroMatchingEntriesOrThrow({
        profileDB: shard1DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });

    // Perform an upsert replacement whose query is an exact match on _id and whose replacement
    // doc contains the remainder of the shard key. The _id taken from the query should be used
    // both in targeting the update and in generating the new document.
    writeRes = assert.commandWorked(mongosColl.update(
        {_id: 101}, {a: 101, msg: "upsert_extracted_id_from_query"}, {upsert: true}));
    assert.eq(writeRes.nUpserted, 1);

    // Verify that the update only targeted shard1, and that the resulting document appears as
    // expected. At this point in the test we expect shard1 to be stale, because it was the
    // destination shard for the first moveChunk; we therefore explicitly check the profiler for
    // a successful update, i.e. one which did not report a stale config exception.
    assert.docEq(mongosColl.find({_id: 101}).toArray(),
                 [{_id: 101, a: 101, msg: "upsert_extracted_id_from_query"}]);
    assert.docEq(shard1DB.test.find({_id: 101}).toArray(),
                 [{_id: 101, a: 101, msg: "upsert_extracted_id_from_query"}]);
    profilerHasZeroMatchingEntriesOrThrow({
        profileDB: shard0DB,
        filter: {op: "update", "command.u.msg": "upsert_extracted_id_from_query"}
    });
    profilerHasSingleMatchingEntryOrThrow({
        profileDB: shard1DB,
        filter: {
            op: "update",
            "command.u.msg": "upsert_extracted_id_from_query",
            errName: {$exists: false}
        }
    });
}

function runReplacementUpdateTestsForCompoundShardKey() {
    setUpData();

    // Perform a replacement update whose query is an exact match on _id and whose replacement
    // document contains the remainder of the shard key. Despite the fact that the replacement
    // document does not contain the entire shard key, we expect that mongoS will extract the
    // _id from the query and combine it with the replacement doc to target a single shard.
    let writeRes = assert.commandWorked(
        mongosColl.update({_id: -100}, {a: -100, msg: "update_extracted_id_from_query"}));

    // Verify that the update did not modify the orphan document.
    assert.docEq(shard1DB.test.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "not_updated"}]);
    assert.eq(writeRes.nMatched, 1);
    assert.eq(writeRes.nModified, 1);

    // Verify that the update only targeted shard0 and that the resulting document appears as
    // expected.
    assert.docEq(mongosColl.find({_id: -100}).toArray(),
                 [{_id: -100, a: -100, msg: "update_extracted_id_from_query"}]);
    profilerHasSingleMatchingEntryOrThrow({
        profileDB: shard0DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });
    profilerHasZeroMatchingEntriesOrThrow({
        profileDB: shard1DB,
        filter: {op: "update", "command.u.msg": "update_extracted_id_from_query"}
    });

    // An upsert whose query doesn't have full shard key will fail.
    assert.commandFailedWithCode(
        mongosColl.update(
            {_id: 101}, {a: 101, msg: "upsert_extracted_id_from_query"}, {upsert: true}),
        ErrorCodes.ShardKeyNotFound);

    // Verify that the document did not perform any writes.
    assert.docEq(mongosColl.find({_id: 101}).itcount(), 0);

    // Verify that an update whose query contains an exact match on _id but whose replacement
    // doc does not contain all other shard key fields will be targeted as if the missing shard
    // key values are null, but will write the replacement document as-is.

    // Need to start a session to change the shard key.
    const session = st.s.startSession({retryWrites: true});
    const sessionDB = session.getDatabase(jsTestName());
    const sessionColl = sessionDB.test;

    sessionColl.insert({_id: -99, a: null, msg: "not_updated"});

    assert.commandWorked(
        sessionColl.update({_id: -99}, {_id: -99, msg: "update_missing_shard_key_field"}));

    assert.docEq(sessionColl.find({_id: -99}).toArray(),
                 [{_id: -99, msg: "update_missing_shard_key_field"}]);

    // Verify that an upsert whose query contains an exact match on _id but whose replacement
    // document does not contain all other shard key fields will work properly.
    assert.commandWorked(
        sessionColl.update({_id: -100, a: -100}, {msg: "upsert_targeting_worked"}, {upsert: true}));
    assert.eq(mongosColl.find({_id: -100, a: -100}).itcount(), 0);
    assert.eq(mongosColl.find({msg: "upsert_targeting_worked"}).itcount(), 1);
}

// Shard the test collection on {_id: 1, a: 1}, split it into two chunks, and migrate one of
// these to the second shard.
st.shardColl(mongosColl, {_id: 1, a: 1}, {_id: 0, a: 0}, {_id: 1, a: 1}, mongosDB.getName(), true);

// Run the replacement behaviour tests that are relevant to a compound key that includes _id.
runReplacementUpdateTestsForCompoundShardKey();

// Drop and reshard the collection on {_id: "hashed"}, which will autosplit across both shards.
assert(mongosColl.drop());
mongosDB.adminCommand({shardCollection: mongosColl.getFullName(), key: {_id: "hashed"}});

// Run the replacement behaviour tests relevant to a collection sharded on {_id: "hashed"}.
runReplacementUpdateTestsForHashedShardKey();

st.stop();
})();