summaryrefslogtreecommitdiff
path: root/jstests/noPassthrough/collmod_convert_to_unique_side_writes.js
blob: 3a25d0018d185c6f7a1f8346ed6ded21ca7860f6 (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
/**
 * Test that the collMod command allows concurrent writes while converting regular indexes to
 * unique indexes.
 *
 * @tags: [
 *  # TODO(SERVER-61181): Fix validation errors under ephemeralForTest.
 *  incompatible_with_eft,
 *  # TODO(SERVER-61182): Fix WiredTigerKVEngine::alterIdentMetadata() under inMemory.
 *  requires_persistence,
 * ]
 */

(function() {
'use strict';

load('jstests/libs/fail_point_util.js');
load('jstests/libs/parallel_shell_helpers.js');

const rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();

const primary = rst.getPrimary();
const collModIndexUniqueEnabled =
    assert.commandWorked(primary.adminCommand({getParameter: 1, featureFlagCollModIndexUnique: 1}))
        .featureFlagCollModIndexUnique.value;

if (!collModIndexUniqueEnabled) {
    jsTestLog('Skipping test because the collMod unique index feature flag is disabled');
    rst.stopSet();
    return;
}

let collCount = 0;
const collPrefix = 'collmod_convert_to_unique_side_writes_';

/**
 * Returns the number of unique indexes with the given key pattern.
 */
const countUnique = function(coll, key) {
    const all = coll.getIndexes().filter(function(z) {
        return z.unique && friendlyEqual(z.key, key);
    });
    return all.length;
};

/**
 * Starts and pauses a unique index conversion in the collection.
 * While the 'collMod' command in paused, runs 'doCrudOpsFunc' before resuming the
 * conversion process. Confirms expected 'collMod' behavior.
 */
const testCollModConvertUniqueWithSideWrites = function(performCrudOpsFunc, expectSuccess) {
    const testDB = primary.getDB('test');
    const collName = collPrefix + collCount++;
    const coll = testDB.getCollection(collName);

    jsTestLog('Starting test on collection: ' + coll.getFullName());
    assert.commandWorked(testDB.createCollection(collName));

    // Creates a regular index and use collMod to convert it to a unique index.
    assert.commandWorked(coll.createIndex({a: 1}));

    // Initial documents. If the conversion is expected to be successful, we
    // can check the uniquenes contraint using the values 'a' in these seed
    // documents.
    const docs = [
        {_id: 1, a: 100},
        {_id: 2, a: 200},
        {_id: 3, a: 300},
    ];
    assert.commandWorked(coll.insert(docs));

    let awaitCollMod = () => {};
    const failPoint = configureFailPoint(
        primary, 'hangAfterCollModIndexUniqueSideWriteTracker', {nss: coll.getFullName()});
    try {
        // Start collMod unique index conversion.
        if (expectSuccess) {
            awaitCollMod = assertCommandWorkedInParallelShell(
                primary, testDB, {collMod: collName, index: {keyPattern: {a: 1}, unique: true}});
        } else {
            awaitCollMod = assertCommandFailedWithCodeInParallelShell(
                primary,
                testDB,
                {collMod: collName, index: {keyPattern: {a: 1}, unique: true}},
                ErrorCodes.CannotEnableIndexConstraint);
        }
        failPoint.wait();

        // Check locks held by collMod while waiting on fail point.
        const currentOpResult = testDB.getSiblingDB("admin")
                                    .aggregate(
                                        [
                                            {$currentOp: {allUsers: true, idleConnections: true}},
                                            {
                                                $match: {
                                                    type: 'op',
                                                    op: 'command',
                                                    connectionId: {$exists: true},
                                                    ns: `${coll.getDB().$cmd.getFullName()}`,
                                                    'command.collMod': coll.getName(),
                                                    'locks.Collection': 'r'
                                                }
                                            },
                                        ],
                                        {readConcern: {level: "local"}})
                                    .toArray();
        assert.eq(
            currentOpResult.length,
            1,
            'unable to find collMod command in db.currentOp() result: ' + tojson(currentOpResult));
        const collModOp = currentOpResult[0];
        assert(collModOp.hasOwnProperty('locks'),
               'no lock info in collMod op from db.currentOp(): ' + tojson(collModOp));
        assert.eq(collModOp.locks.Collection,
                  'r',
                  'collMod is not holding collection lock in read mode: ' + tojson(collModOp));

        jsTestLog('Performing CRUD ops on collection while collMod is paused: ' +
                  performCrudOpsFunc);
        try {
            performCrudOpsFunc(coll);
        } catch (ex) {
            jsTestLog('CRUD ops failed: ' + ex);
            doassert('CRUD ops failed: ' + ex + ': ' + performCrudOpsFunc);
        }
    } finally {
        failPoint.off();
        awaitCollMod();
    }

    if (expectSuccess) {
        assert.eq(countUnique(coll, {a: 1}),
                  1,
                  'index should be unique now: ' + tojson(coll.getIndexes()));

        // Test uniqueness constraint.
        assert.commandFailedWithCode(coll.insert({_id: 100, a: 100}), ErrorCodes.DuplicateKey);
    } else {
        assert.eq(
            countUnique(coll, {a: 1}), 0, 'index should not unique: ' + tojson(coll.getIndexes()));

        // Check that uniquenesss constraint is not enforceed.
        assert.commandWorked(coll.insert({_id: 100, a: 100}));
    }
    jsTestLog('Successsfully completed test on collection: ' + coll.getFullName());
};

// Checks successful conversion with non-conflicting documents inserted during collMod.
testCollModConvertUniqueWithSideWrites((coll) => {
    const docs = [
        {_id: 4, a: 400},
        {_id: 5, a: 500},
        {_id: 6, a: 600},
    ];
    jsTestLog('Inserting additional documents after collMod completed index scan: ' + tojson(docs));
    assert.commandWorked(coll.insert(docs));
    jsTestLog('Successfully inserted documents. Resuming collMod index conversion: ' +
              tojson(docs));
}, true /* expectSuccess */);

// Confirms that conversion fails with a conflicting document inserted during collMod.
testCollModConvertUniqueWithSideWrites((coll) => {
    const docs = [
        {_id: 1000, a: 100},
    ];
    jsTestLog('Inserting additional documents after collMod completed index scan: ' + tojson(docs));
    assert.commandWorked(coll.insert(docs));
    jsTestLog('Successfully inserted documents. Resuming collMod index conversion: ' +
              tojson(docs));
}, false /* expectSuccess */);

// Confirms that conversion fails if an update during collMod leads to a conflict.
testCollModConvertUniqueWithSideWrites((coll) => {
    jsTestLog('Updating single document after collMod completed index scan.');
    assert.commandWorked(coll.update({_id: 1}, {a: 200}));
    jsTestLog('Successfully updated document. Resuming collMod index conversion.');
}, false /* expectSuccess */);

// Inserting and deleting a conflicting document before collMod obtains exclusive access to the
// collection to complete the conversion should result in a successful conversion.
testCollModConvertUniqueWithSideWrites((coll) => {
    jsTestLog('Inserting and removing a conflicting document after collMod completed index scan.');
    assert.commandWorked(coll.insert({_id: 101, a: 100}));
    assert.commandWorked(coll.remove({_id: 101}));
    jsTestLog('Successfully inserted and removed document. Resuming collMod index conversion.');
}, true /* expectSuccess */);

// Inserting a non-conflicting document containing an unindexed field should not affect conversion.
testCollModConvertUniqueWithSideWrites((coll) => {
    jsTestLog('Inserting a non-conflicting document containing an unindexed field.');
    assert.commandWorked(coll.insert({_id: 7, a: 700, b: 2222}));
    jsTestLog('Successfully inserted a non-conflicting document containing an unindexed field. ' +
              'Resuming collMod index conversion.');
}, true /* expectSuccess */);

// Removing the last entry in the index should not throw off the index scan.
testCollModConvertUniqueWithSideWrites((coll) => {
    jsTestLog('Removing the last index entry');
    assert.commandWorked(coll.remove({_id: 3}));
    jsTestLog('Successfully the last index entry. Resuming collMod index conversion.');
}, true /* expectSuccess */);

// Make the index multikey with a non-conflicting document.
testCollModConvertUniqueWithSideWrites((coll) => {
    jsTestLog('Converting the index to multikey with non-conflicting document');
    assert.commandWorked(coll.insert({_id: 8, a: [400, 500]}));
    jsTestLog('Successfully converted the index to multikey with non-conflicting document');
}, true /* expectSuccess */);

// Make the index multikey with a conflicting document.
testCollModConvertUniqueWithSideWrites((coll) => {
    jsTestLog('Converting the index to multikey with conflicting document');
    assert.commandWorked(coll.insert({_id: 9, a: [900, 100]}));
    jsTestLog('Successfully converted the index to multikey with conflicting document');
}, false /* expectSuccess */);

rst.stopSet();
})();