summaryrefslogtreecommitdiff
path: root/jstests/replsets/libs/two_phase_drops.js
blob: bb772012fdb283bf11106a5d8452c8e40ee03bad (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
/**
 * TwoPhaseDropCollectionTest is a library for testing two phase collection drops in a replica set.
 *
 * External tests can utilize this library to verify various aspects of the two phase collection
 * drop behavior. It provides a way to easily create a replicated collection and control its
 * transition between the 'PREPARE' and 'COMMIT' phase of a collection drop.
 *
 * Details:
 *
 * The test uses a 2 node replica set to verify both phases of a 2-phase collection drop: the
 * 'Prepare' and 'Commit' phase. Executing a 'drop' collection command should put that collection
 * into the 'Prepare' phase. The 'Commit' phase (physically dropping the collection) of a drop
 * operation with optime T should only be executed when C >= T, where C is the majority commit point
 * of the replica set.
 *
 */
"use strict";

load("jstests/libs/check_log.js");            // For 'checkLog'.
load("jstests/libs/fixture_helpers.js");      // For 'FixtureHelpers'.
load("jstests/aggregation/extras/utils.js");  // For 'arrayEq'.

class TwoPhaseDropCollectionTest {
    constructor(testName, dbName) {
        this.testName = testName;
        this.dbName = dbName;

        this.oplogApplicationFailpoint = "rsSyncApplyStop";
    }

    /**
     * Log a message for 'TwoPhaseDropCollectionTest'.
     */
    static _testLog(msg) {
        jsTestLog("[TwoPhaseDropCollectionTest] " + msg);
    }

    /**
     * Returns true if the replica set supports two phase drops that use 'system.drop' namespaces.
     * Since 4.2, 'system.drop' drop pending collections will be disabled on storage engines that
     * support drop pending idents natively. See serverStatus().storageEngine.supportsPendingDrops.
     */
    static supportsDropPendingNamespaces(replSetTest) {
        const primaryDB = replSetTest.getPrimary().getDB('admin');
        const serverStatus = assert.commandWorked(primaryDB.serverStatus());
        const storageEngineSection = serverStatus.storageEngine;
        TwoPhaseDropCollectionTest._testLog('Storage engine features (from serverStatus()): ' +
                                            tojson(storageEngineSection));
        return !storageEngineSection.supportsPendingDrops;
    }

    /**
     * Instance method version of supportsDropPendingNamespaces().
     */
    supportsDropPendingNamespaces() {
        return TwoPhaseDropCollectionTest.supportsDropPendingNamespaces(this.replTest);
    }

    /**
     * Pause oplog application on a specified node.
     */
    pauseOplogApplication(node) {
        assert.commandWorked(node.adminCommand(
            {configureFailPoint: this.oplogApplicationFailpoint, mode: "alwaysOn"}));
        checkLog.contains(node, this.oplogApplicationFailpoint + " fail point enabled");
    }

    /**
     * Resume oplog application on a specified node.
     */
    resumeOplogApplication(node) {
        assert.commandWorked(
            node.adminCommand({configureFailPoint: this.oplogApplicationFailpoint, mode: "off"}));
    }

    /**
     * Return a list of all collections in a given database. Use 'args' as the 'listCollections'
     * command arguments.
     */
    static listCollections(database, args) {
        args = args || {};
        let failMsg = "'listCollections' command failed";
        let res = assert.commandWorked(database.runCommand("listCollections", args), failMsg);
        return res.cursor.firstBatch;
    }

    /**
     * Waits for all collections pending drop to be completely dropped on the given connection.
     */
    static waitForAllCollectionDropsToComplete(conn) {
        assert.soon(function() {
            const dbNames = conn.getDBNames();
            for (let dbName of dbNames) {
                const currDB = conn.getDB(dbName);
                let collectionsWithPending =
                    TwoPhaseDropCollectionTest.listCollections(currDB, {includePendingDrops: true});
                let collectionsNoPending = TwoPhaseDropCollectionTest.listCollections(currDB);
                if (!arrayEq(collectionsWithPending, collectionsNoPending)) {
                    // Do a write on the primary to ensure that the commit point advances.
                    let cmd = {
                        appendOplogNote: 1,
                        data: {id: "waitForAllCollectionDropsToCompleteHelper"}
                    };
                    FixtureHelpers.runCommandOnEachPrimary({db: conn.getDB("admin"), cmdObj: cmd});
                    return false;
                }
            }
            return true;
        }, "Not all collection drops completed on " + conn.host);
    }

    /**
     * Return a list of all collection names in a given database.
     */
    listCollectionNames(database, args) {
        return TwoPhaseDropCollectionTest.listCollections(database, args).map(c => c.name);
    }

    /**
     * Initiates a 2 node replica set to be used for the test. Returns the constructed ReplSetTest.
     */
    initReplSet() {
        let nodes = [{}, {rsConfig: {priority: 0}}];
        this.replTest = new ReplSetTest({name: this.testName, nodes: nodes});

        // Initiate the replica set.
        this.replTest.startSet();
        this.replTest.initiate();
        this.replTest.awaitReplication();

        return this.replTest;
    }

    /**
     * Creates a collection with name 'collName' in the test database and then awaits replication.
     */
    createCollection(collName) {
        // Create the collection that will be dropped and let it replicate.
        let primaryDB = this.replTest.getPrimary().getDB(this.dbName);
        assert.commandWorked(primaryDB.createCollection(collName));
        this.replTest.awaitReplication();
    }

    /**
     * Return a regex matching a drop-pending namespace string for a collection with name
     * 'collName'.
     *
     * Drop pending names should be of the format "system.drop.<optime>.<collectionName>", where
     * 'optime' is the optime of the collection drop operation, encoded as a string, and
     * 'collectionName' is the original collection name.
     */
    static pendingDropRegex(collName) {
        return new RegExp("system\.drop\..*\." + collName + "$");
    }

    /**
     * Returns true if the collection 'collName' exists on the primary.
     */
    collectionExists(collName) {
        let primaryDB = this.replTest.getPrimary().getDB(this.dbName);
        let coll =
            TwoPhaseDropCollectionTest.listCollections(primaryDB).find(c => c.name === collName);
        return coll !== undefined;
    }

    /**
     * If 'collName' is in drop pending state on the primary, returns the name of the collection
     * after drop pending rename. If collection is not in drop pending state, returns false.
     */
    collectionIsPendingDrop(collName) {
        let primaryDB = this.replTest.getPrimary().getDB(this.dbName);
        return TwoPhaseDropCollectionTest.collectionIsPendingDropInDatabase(primaryDB, collName);
    }

    /**
     * If 'collName' in database 'db' is in drop pending state on the primary, returns the name
     * of the collection after drop pending rename. If collection is not in drop pending state,
     * returns false.
     */
    static collectionIsPendingDropInDatabase(db, collName) {
        let collections =
            TwoPhaseDropCollectionTest.listCollections(db, {includePendingDrops: true});

        TwoPhaseDropCollectionTest._testLog("Checking presence of drop-pending collection for " +
                                            collName +
                                            " in the collection list: " + tojson(collections));

        let pendingDropRegex = TwoPhaseDropCollectionTest.pendingDropRegex(collName);
        return collections.find(c => pendingDropRegex.test(c.name));
    }

    /**
     * Waits until 'collName' in database 'db' is not in drop pending state.
     */
    static waitForDropToComplete(db, collName) {
        assert.soon(
            () => !TwoPhaseDropCollectionTest.collectionIsPendingDropInDatabase(db, collName));
    }

    /**
     * Puts a collection with name 'collName' into the drop pending state. Returns the name of the
     * collection after it has been renamed to the 'system.drop' namespace.
     */
    prepareDropCollection(collName) {
        let primaryDB = this.replTest.getPrimary().getDB(this.dbName);

        // Pause application on secondary so that commit point doesn't advance, meaning that a
        // dropped collection on the primary will remain in 'drop-pending' state.
        TwoPhaseDropCollectionTest._testLog("Pausing oplog application on the secondary node.");
        this.pauseOplogApplication(this.replTest.getSecondary());

        // Drop the collection on the primary.
        TwoPhaseDropCollectionTest._testLog("Dropping collection '" + collName +
                                            "' on primary node.");
        assert.commandWorked(primaryDB.runCommand({drop: collName, writeConcern: {w: 1}}));

        // Make sure the collection doesn't appear in the normal collection list and that it is now
        // in 'drop-pending' state.
        assert(!this.collectionExists(collName));
        let droppedColl = this.collectionIsPendingDrop(collName);

        assert(
            droppedColl,
            "Dropped collection '" + collName + "' was not found in the 'system.drop' namespace");

        return droppedColl.name;
    }

    /**
     * Restarts oplog application on the secondary and waits for the drop of collection 'collName'
     * to be committed (physically dropped).
     */
    commitDropCollection(collName) {
        // Let the secondary apply the collection drop operation, so that the replica set commit
        // point will advance, and the 'Commit' phase of the collection drop will complete on the
        // primary.
        TwoPhaseDropCollectionTest._testLog("Restarting oplog application on the secondary node.");
        this.resumeOplogApplication(this.replTest.getSecondary());

        TwoPhaseDropCollectionTest._testLog(
            "Waiting for collection drop operation to replicate to all nodes.");
        this.replTest.awaitReplication();

        // Make sure the collection has been fully dropped. It should not appear as a normal
        // collection or under the 'system.drop' namespace any longer. Physical collection drops may
        // happen asynchronously, any time after the drop operation is committed, so we wait to make
        // sure the collection is eventually dropped.
        TwoPhaseDropCollectionTest._testLog("Waiting for collection drop of '" + collName +
                                            "' to commit.");
        // Bind the member functions onto this instead of the anonymous function.
        const twoPhaseDrop = this;
        assert.soonNoExcept(function() {
            assert(!twoPhaseDrop.collectionExists(collName));
            assert(!twoPhaseDrop.collectionIsPendingDrop(collName));
            return true;
        });
    }

    /**
     * Disable all fail points and shut down the replica set.
     */
    stop() {
        TwoPhaseDropCollectionTest._testLog("Disabling fail points and shutting down replica set.");
        this.resumeOplogApplication(this.replTest.getSecondary());
        this.replTest.stopSet();
    }
}