summaryrefslogtreecommitdiff
path: root/jstests/sharding/compound_hashed_shard_key_zoning.js
blob: 83692acbb38f4c596ad5a8140e9a1bb7ff413069 (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
/**
 * Test that updateZoneKeyRange command works correctly in combination with shardCollection command.
 * In this test we verify the behaviour of:
 *  - Creating zones after sharding the collection.
 *  - Creating zones before sharding the collection.
 *  - Creating zones in collection which has data and then sharding the collection.
 *
 * @tags: [
 *   multiversion_incompatible,
 * ]
 */
(function() {
'use strict';

const st = new ShardingTest({shards: 3});
const kDbName = 'test';
const kCollName = 'foo';
const ns = kDbName + '.' + kCollName;
const zoneName = 'zoneName';
const mongos = st.s0;
const testDB = mongos.getDB(kDbName);
const configDB = mongos.getDB('config');
const shardName = st.shard0.shardName;
assert.commandWorked(mongos.adminCommand({enableSharding: kDbName}));
assert.commandWorked(st.s.adminCommand({addShardToZone: shardName, zone: 'zoneName'}));

function fillMissingShardKeyFields(shardKey, doc, value) {
    for (let key in shardKey) {
        if (!(key in doc)) {
            doc[key] = value ? value : {"$minKey": 1};
        }
    }
    return doc;
}
/**
 * Test that 'updateZoneKeyRange' works correctly by verifying 'tags' collection, after sharding the
 * collection.
 */
function testZoningAfterSharding(namespace, shardKey, NumberType) {
    assert.commandWorked(st.s.adminCommand({shardCollection: namespace, key: shardKey}));

    if (shardKey.x === "hashed") {
        // Cannot assign with a non-NumberLong range value on a hashed shard key field.
        assert.commandFailedWithCode(
            st.s.adminCommand(
                {updateZoneKeyRange: namespace, min: {x: 0}, max: {x: 10}, zone: 'zoneName'}),
            ErrorCodes.InvalidOptions);
    }

    // Testing basic assign.
    assert.commandWorked(st.s.adminCommand({
        updateZoneKeyRange: namespace,
        min: {x: NumberType(0)},
        max: {x: NumberType(10)},
        zone: 'zoneName'
    }));

    let tagDoc = configDB.tags.findOne();

    assert.eq(namespace, tagDoc.ns);
    assert.eq(fillMissingShardKeyFields(shardKey, {x: NumberType(0)}), tagDoc.min);
    assert.eq(fillMissingShardKeyFields(shardKey, {x: NumberType(10)}), tagDoc.max);
    assert.eq('zoneName', tagDoc.tag);

    // Cannot assign overlapping ranges
    assert.commandFailedWithCode(st.s.adminCommand({
        updateZoneKeyRange: namespace,
        min: {x: NumberType(-10)},
        max: {x: NumberType(20)},
        zone: 'zoneName'
    }),
                                 ErrorCodes.RangeOverlapConflict);

    // Cannot have non-shard key fields in tag range.
    assert.commandFailedWithCode(st.s.adminCommand({
        updateZoneKeyRange: namespace,
        min: {newField: NumberType(-10)},
        max: {newField: NumberType(20)},
        zone: 'zoneName'
    }),
                                 ErrorCodes.ShardKeyNotFound);

    tagDoc = configDB.tags.findOne();
    assert.eq(namespace, tagDoc.ns);
    assert.eq(fillMissingShardKeyFields(shardKey, {x: NumberType(0)}), tagDoc.min);
    assert.eq(fillMissingShardKeyFields(shardKey, {x: NumberType(10)}), tagDoc.max);
    assert.eq('zoneName', tagDoc.tag);

    // Testing basic remove.
    assert.commandWorked(st.s.adminCommand({
        updateZoneKeyRange: namespace,
        min: fillMissingShardKeyFields(shardKey, {x: NumberType(0)}, MinKey),
        max: fillMissingShardKeyFields(shardKey, {x: NumberType(10)}, MinKey),
        zone: null
    }));
    assert.eq(null, configDB.tags.findOne());

    // Insert directly into the tags collection.
    const zone = {
        _id: 0,
        ns: namespace,
        min: fillMissingShardKeyFields(shardKey, {x: 0}, MinKey),
        max: fillMissingShardKeyFields(shardKey, {x: 10}, MinKey),
        zone: "zoneName"
    };
    assert.commandWorked(configDB.tags.insert(zone));
    assert.eq(zone, configDB.tags.findOne());

    // Remove works on entries inserted directly into the tags collection, even when those entries
    // do not adhere to the updateZoneKeyRange command requirement of having a NumberLong range
    // value for a hashed shard key field.
    assert.commandWorked(st.s.adminCommand({
        updateZoneKeyRange: namespace,
        min: fillMissingShardKeyFields(shardKey, {x: 0}, MinKey),
        max: fillMissingShardKeyFields(shardKey, {x: 10}, MinKey),
        zone: null
    }));
    assert.eq(null, configDB.tags.findOne());
}

testZoningAfterSharding("test.compound_hashed", {x: 1, y: "hashed", z: 1}, Number);
testZoningAfterSharding("test.compound_hashed", {x: 1, y: "hashed", z: 1}, NumberLong);
testZoningAfterSharding("test.compound_hashed_prefix", {x: "hashed", y: 1, z: 1}, NumberLong);

/**
 * Test that shardCollection correctly validates shard key against existing zones.
 */
function testZoningBeforeSharding({shardKey, zoneRange, failCode}) {
    assert.commandWorked(testDB.foo.createIndex(shardKey));
    assert.commandWorked(st.s.adminCommand({addShardToZone: shardName, zone: zoneName}));

    // Update zone range and verify that the 'tags' collection is updated appropriately.
    assert.commandWorked(st.s.adminCommand(
        {updateZoneKeyRange: ns, min: zoneRange[0], max: zoneRange[1], zone: zoneName}));
    assert.eq(1, configDB.tags.count({ns: ns, min: zoneRange[0], max: zoneRange[1]}));

    if (failCode) {
        assert.commandFailedWithCode(mongos.adminCommand({shardCollection: ns, key: shardKey}),
                                     failCode);
    } else {
        assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: shardKey}));
    }
    assert.commandWorked(testDB.runCommand({drop: kCollName}));
}

// Fails when hashed field is not number long in 'zoneRange'.
testZoningBeforeSharding(
    {shardKey: {x: "hashed"}, zoneRange: [{x: -5}, {x: 5}], failCode: ErrorCodes.InvalidOptions});
testZoningBeforeSharding({
    shardKey: {x: "hashed"},
    zoneRange: [{x: NumberLong(-5)}, {x: 5}],
    failCode: ErrorCodes.InvalidOptions
});
testZoningBeforeSharding({
    shardKey: {x: "hashed"},
    zoneRange: [{x: -5}, {x: NumberLong(5)}],
    failCode: ErrorCodes.InvalidOptions
});
testZoningBeforeSharding(
    {shardKey: {x: "hashed"}, zoneRange: [{x: NumberLong(-5)}, {x: NumberLong(5)}]});
testZoningBeforeSharding({
    shardKey: {x: "hashed", y: 1},
    zoneRange: [{x: NumberLong(-5), y: MinKey}, {x: NumberLong(5), y: MinKey}]
});
testZoningBeforeSharding({
    shardKey: {x: 1, y: "hashed"},
    zoneRange: [{x: 1, y: NumberLong(-5)}, {x: 2, y: NumberLong(5)}]
});
testZoningBeforeSharding({
    shardKey: {x: 1, y: "hashed"},
    zoneRange: [{x: 1, y: NumberLong(-5)}, {x: 2, y: 5}],
    failCode: ErrorCodes.InvalidOptions
});

// Fails when 'zoneRange' doesn't have a shard key field.
testZoningBeforeSharding({
    shardKey: {x: 1, y: "hashed", z: 1},
    zoneRange: [{x: 1, y: NumberLong(-5)}, {x: 2, y: NumberLong(5)}],
    failCode: ErrorCodes.InvalidOptions
});

// Works when shard key field is defined as 'MinKey'.
testZoningBeforeSharding({
    shardKey: {x: 1, y: "hashed", z: 1},
    zoneRange: [{x: 1, y: NumberLong(-5), z: MinKey}, {x: 2, y: NumberLong(5), z: MinKey}],
});
testZoningBeforeSharding(
    {shardKey: {x: 1, y: "hashed"}, zoneRange: [{x: "DUB", y: MinKey}, {x: "NYC", y: MinKey}]});

assert.commandWorked(st.s.adminCommand({removeShardFromZone: shardName, zone: zoneName}));

/**
 * Test that shardCollection uses existing zone ranges to split chunks.
 */
function testChunkSplits({collectionExists, shardKey, zoneRanges, expectedNumChunks}) {
    const shards = configDB.shards.find().toArray();
    if (collectionExists) {
        assert.commandWorked(testDB.foo.createIndex(shardKey));
    }

    // Create a new zone and assign each zone to the shards using round-robin. Then update each of
    // the zone's range to the range specified in 'zoneRanges'.
    for (let i = 0; i < zoneRanges.length; i++) {
        assert.commandWorked(
            st.s.adminCommand({addShardToZone: shards[i % shards.length]._id, zone: zoneName + i}));
        assert.commandWorked(st.s.adminCommand({
            updateZoneKeyRange: ns,
            min: zoneRanges[i][0],
            max: zoneRanges[i][1],
            zone: zoneName + i
        }));
    }
    assert.eq(configDB.tags.count({ns: ns}), zoneRanges.length);
    assert.eq(0,
              configDB.chunks.count({ns: ns}),
              "expect to see no chunk documents for the collection before shardCollection is run");

    // Shard the collection and validate the resulting chunks.
    assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: shardKey}));
    const chunkDocs = configDB.chunks.find({ns: ns}).toArray();
    assert.eq(chunkDocs.length, expectedNumChunks, chunkDocs);

    // Verify that each of the chunks corresponding to zones are in the right shard.
    for (let i = 0; i < zoneRanges.length; i++) {
        assert.eq(1,
                  configDB.chunks.count({
                      ns: ns,
                      min: zoneRanges[i][0],
                      max: zoneRanges[i][1],
                      shard: shards[i % shards.length]._id
                  }),
                  chunkDocs);
    }
    assert.commandWorked(testDB.runCommand({drop: kCollName}));
}

// When shard key is compound hashed with range prefix.
testChunkSplits({
    shardKey: {x: 1, y: "hashed"},
    zoneRanges: [
        [{x: 0, y: MinKey}, {x: 5, y: MinKey}],
        [{x: 10, y: MinKey}, {x: 15, y: MinKey}],
        [{x: 20, y: MinKey}, {x: 25, y: MinKey}],
        [{x: 30, y: MinKey}, {x: 35, y: MinKey}],
    ],
    expectedNumChunks: 9  // 4 zones + 2 boundaries + 3 gap chunks.
});
testChunkSplits({
    shardKey: {x: 1, y: "hashed", z: 1},
    zoneRanges: [
        [{x: 0, y: NumberLong(0), z: MinKey}, {x: 5, y: NumberLong(0), z: MinKey}],
        [{x: 10, y: NumberLong(0), z: MinKey}, {x: 15, y: NumberLong(0), z: MinKey}],
        [{x: 20, y: NumberLong(0), z: MinKey}, {x: 25, y: NumberLong(0), z: MinKey}],
        [{x: 30, y: NumberLong(0), z: MinKey}, {x: 35, y: NumberLong(0), z: MinKey}],
    ],
    expectedNumChunks: 9  // 4 zones + 2 boundaries + 3 gap chunks.
});

// When shard key is compound hashed with hashed prefix.
testChunkSplits({
    collectionExists: true,
    shardKey: {x: "hashed", y: 1},
    zoneRanges: [
        [{x: NumberLong(0), y: MinKey}, {x: NumberLong(10), y: MinKey}],
        [{x: NumberLong(10), y: MinKey}, {x: NumberLong(20), y: MinKey}],
        [{x: NumberLong(20), y: MinKey}, {x: NumberLong(30), y: MinKey}],
        [{x: NumberLong(30), y: MinKey}, {x: NumberLong(40), y: MinKey}],
        [{x: NumberLong(40), y: MinKey}, {x: NumberLong(50), y: MinKey}],
    ],
    expectedNumChunks: 7  // 5 zones + 2 boundaries.
});

/**
 * Tests that a non-empty collection associated with zones can be sharded.
 */
function testNonemptyZonedCollection() {
    const shardKey = {x: 1, y: "hashed"};
    const shards = configDB.shards.find().toArray();
    const testColl = testDB.getCollection(kCollName);
    const ranges = [
        {min: {x: 0, y: MinKey}, max: {x: 10, y: MaxKey}},
        {min: {x: 10, y: MaxKey}, max: {x: 20, y: MinKey}},
        {min: {x: 20, y: MinKey}, max: {x: 40, y: MaxKey}}
    ];

    for (let i = 0; i < 40; i++) {
        assert.commandWorked(testColl.insert({x: 1, y: Math.random()}));
    }

    assert.commandWorked(testColl.createIndex(shardKey));

    for (let i = 0; i < shards.length; i++) {
        assert.commandWorked(
            mongos.adminCommand({addShardToZone: shards[i]._id, zone: zoneName + i}));
        assert.commandWorked(mongos.adminCommand(
            {updateZoneKeyRange: ns, min: ranges[i].min, max: ranges[i].max, zone: zoneName + i}));
    }

    assert.commandWorked(mongos.adminCommand({shardCollection: ns, key: shardKey}));

    // Check that there is initially 1 chunk.
    assert.eq(1, configDB.chunks.count({ns: ns}));

    st.startBalancer();

    // Check that the chunks were moved properly.
    assert.soon(
        () => configDB.chunks.count({ns: ns}) === 5, 'balancer never ran', 5 * 60 * 1000, 1000);

    assert.commandWorked(testDB.runCommand({drop: kCollName}));
}

testNonemptyZonedCollection();

st.stop();
})();