summaryrefslogtreecommitdiff
path: root/jstests/noPassthrough/read_majority.js
blob: c2b20df292a36e0cd3ff93e64efc2ab434b7cf64 (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
/**
 * Tests a variety of functionality related to committed reads:
 *  - A killOp command can successfully kill an operation that is waiting for snapshots to be
 *    created.
 *  - A user should not be able to do any committed reads before a snapshot has been blessed.
 *  - Inserts and catalog changes should not be visible in a snapshot before they occurred.
 *  - A getMore should see the new blessed snapshot.
 *  - Dropping an index, repairing, and reIndexing should bump the min snapshot version.
 *  - Dropping a collection is visible in committed snapshot, since metadata changes are special.
 *  - 'local'-only commands should error on 'majority' level, and accept 'local' level.
 *  - An aggregation with '$out' should fail with 'majority' level.
 *
 * All of this requires support for committed reads, so this test will be skipped if the storage
 * engine does not support them.
 */

load("jstests/libs/analyze_plan.js");

(function() {
    "use strict";

    // This test needs its own mongod since the snapshot names must be in increasing order and once
    // you
    // have a majority commit point it is impossible to go back to not having one.
    var testServer =
        MongoRunner.runMongod({setParameter: 'testingSnapshotBehaviorInIsolation=true'});
    var db = testServer.getDB("test");
    var t = db.readMajority;

    function assertNoReadMajoritySnapshotAvailable() {
        var res =
            t.runCommand('find', {batchSize: 2, readConcern: {level: "majority"}, maxTimeMS: 1000});
        assert.commandFailed(res);
        assert.eq(res.code, ErrorCodes.ExceededTimeLimit);
    }

    function getReadMajorityCursor() {
        var res = t.runCommand('find', {batchSize: 2, readConcern: {level: "majority"}});
        assert.commandWorked(res);
        return new DBCommandCursor(db.getMongo(), res, 2);
    }

    function getReadMajorityAggCursor() {
        var res =
            t.runCommand('aggregate', {cursor: {batchSize: 2}, readConcern: {level: "majority"}});
        assert.commandWorked(res);
        return new DBCommandCursor(db.getMongo(), res, 2);
    }

    function getExplainPlan(query) {
        var res = db.runCommand({explain: {find: t.getName(), filter: query}});
        return assert.commandWorked(res).queryPlanner.winningPlan;
    }

    //
    // Actual Test
    //

    if (!db.serverStatus().storageEngine.supportsCommittedReads) {
        print("Skipping read_majority.js since storageEngine doesn't support it.");
        return;
    }

    // Ensure killOp will work on an op that is waiting for snapshots to be created
    var blockedReader = startParallelShell(
        "db.readMajority.runCommand('find', {batchSize: 2, readConcern: {level: 'majority'}});",
        testServer.port);

    assert.soon(function() {
        var curOps = db.currentOp(true);
        jsTestLog("curOp output: " + tojson(curOps));
        for (var i in curOps.inprog) {
            var op = curOps.inprog[i];
            if (op.op === 'query' && op.ns === "test.$cmd" && op.query.find === 'readMajority') {
                db.killOp(op.opid);
                return true;
            }
        }
        return false;
    }, "could not kill an op that was waiting for a snapshot", 60 * 1000);
    blockedReader();

    var snapshot1 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assert.commandWorked(db.runCommand({create: "readMajority"}));
    var snapshot2 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;

    for (var i = 0; i < 10; i++) {
        assert.writeOK(t.insert({_id: i, version: 3}));
    }

    assertNoReadMajoritySnapshotAvailable();

    var snapshot3 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;

    assertNoReadMajoritySnapshotAvailable();

    assert.writeOK(t.update({}, {$set: {version: 4}}, false, true));
    var snapshot4 = assert.commandWorked(db.adminCommand("makeSnapshot")).name;

    // Collection didn't exist in snapshot 1.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot1}));
    assertNoReadMajoritySnapshotAvailable();

    // Collection existed but was empty in snapshot 2.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot2}));
    assert.eq(getReadMajorityCursor().itcount(), 0);
    assert.eq(getReadMajorityAggCursor().itcount(), 0);

    // In snapshot 3 the collection was filled with {version: 3} documents.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot3}));
    assert.eq(getReadMajorityAggCursor().itcount(), 10);
    getReadMajorityAggCursor().forEach(function(doc) {
        // Note: agg uses internal batching so can't reliably test flipping snapshot. However, it
        // uses
        // the same mechanism as find, so if one works, both should.
        assert.eq(doc.version, 3);
    });

    assert.eq(getReadMajorityCursor().itcount(), 10);
    var cursor = getReadMajorityCursor();  // Note: uses batchsize=2.
    assert.eq(cursor.next().version, 3);
    assert.eq(cursor.next().version, 3);
    assert(!cursor.objsLeftInBatch());

    // In snapshot 4 the collection was filled with {version: 3} documents.
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": snapshot4}));

    // This triggers a getMore which sees the new version.
    assert.eq(cursor.next().version, 4);
    assert.eq(cursor.next().version, 4);

    // Adding an index bumps the min snapshot for a collection as of SERVER-20260. This may change
    // to
    // just filter that index out from query planning as part of SERVER-20439.
    t.ensureIndex({version: 1});
    assertNoReadMajoritySnapshotAvailable();

    // To use the index, a snapshot created after the index was completed must be marked committed.
    var newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);
    assert.eq(getReadMajorityAggCursor().itcount(), 10);
    assert(isIxscan(getExplainPlan({version: 1})));

    // Dropping an index does bump the min snapshot.
    t.dropIndex({version: 1});
    assertNoReadMajoritySnapshotAvailable();

    // To use the collection again, a snapshot created after the dropIndex must be marked committed.
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);

    // Reindex bumps the min snapshot.
    t.reIndex();
    assertNoReadMajoritySnapshotAvailable();
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);

    // Repair bumps the min snapshot.
    db.repairDatabase();
    assertNoReadMajoritySnapshotAvailable();
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 10);
    assert.eq(getReadMajorityAggCursor().itcount(), 10);

    // Dropping the collection is visible in the committed snapshot, even though it hasn't been
    // marked
    // committed yet. This is allowed by the current specification even though it violates strict
    // read-committed semantics since we don't guarantee them on metadata operations.
    t.drop();
    assert.eq(getReadMajorityCursor().itcount(), 0);
    assert.eq(getReadMajorityAggCursor().itcount(), 0);

    // Creating a new collection with the same name hides the collection until that operation is in
    // the
    // committed view.
    t.insert({_id: 0, version: 8});
    assertNoReadMajoritySnapshotAvailable();
    newSnapshot = assert.commandWorked(db.adminCommand("makeSnapshot")).name;
    assertNoReadMajoritySnapshotAvailable();
    assert.commandWorked(db.adminCommand({"setCommittedSnapshot": newSnapshot}));
    assert.eq(getReadMajorityCursor().itcount(), 1);
    assert.eq(getReadMajorityAggCursor().itcount(), 1);

    // Commands that only support read concern 'local', (such as ping) must work when it is
    // explicitly
    // specified and fail when 'majority' is specified.
    assert.commandWorked(db.adminCommand({ping: 1, readConcern: {level: 'local'}}));
    var res = assert.commandFailed(db.adminCommand({ping: 1, readConcern: {level: 'majority'}}));
    assert.eq(res.code, ErrorCodes.InvalidOptions);

    // Agg $out also doesn't support read concern majority.
    assert.commandWorked(
        t.runCommand('aggregate', {pipeline: [{$out: 'out'}], readConcern: {level: 'local'}}));
    var res = assert.commandFailed(
        t.runCommand('aggregate', {pipeline: [{$out: 'out'}], readConcern: {level: 'majority'}}));
    assert.eq(res.code, ErrorCodes.InvalidOptions);

    MongoRunner.stopMongod(testServer);
}());