summaryrefslogtreecommitdiff
path: root/jstests/replsets/assert_on_prepare_conflict_with_hole.js
blob: b3379ff8b9d3a27832afbde8f38fb08184a3c904 (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
/**
 * Constructs the following cycle that can lead to stalling a sharded cluster:
 * | Preparer                              | Insert                    | OplogVisibility Ts |
 * |---------------------------------------+---------------------------+--------------------|
 * | BeginTxn                              |                           |                    |
 * | Write A                               |                           |                    |
 * |                                       | BeginTxn                  |                    |
 * |                                       | Preallocates TS(10)       |                  9 |
 * | (side txn commits prepare oplog @ 11) |                           |                    |
 * | Prepare 11                            |                           |                    |
 * |                                       | Write A (PrepareConflict) |                    |
 *
 * In this scenario, the prepared transaction blocks waiting for its prepare oplog entry at
 * timestamp 11 to become majority committed. However, the prepare oplog entry cannot replicate to
 * secondaries until the oplog visibility timestamp advances to 11. The oplog visibility timestamp
 * advancing is blocked on the insert that allocated timestamps 10. The insert cannot make progress
 * because it has hit a prepare conflict. The prepare conflict this test specifically exercises is
 * for duplicate key detection on a non-_id unique index.
 *
 * @tags: [uses_transactions, uses_prepare_transaction]
 */
(function() {
"use strict";

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

// Use a single node replica set for simplicity. Note that an oplog hole on a single node replica
// will block new writes from becoming majority committed.
const rst = new ReplSetTest({
    nodes: 1,
    nodeOptions: {
        setParameter: {logComponentVerbosity: tojson({storage: 1})},
    }
});
rst.startSet();
rst.initiate();

const primary = rst.getPrimary();
assert.commandWorked(primary.adminCommand(
    {setDefaultRWConcern: 1, defaultWriteConcern: {w: 1}, writeConcern: {w: "majority"}}));
const db = primary.getDB("test");

const collName = "mycoll";
assert.commandWorked(db.runCommand({create: collName, writeConcern: {w: "majority"}}));
// A secondary unique index requires cursor positioning in WT which can result in hitting a prepare
// conflict.
assert.commandWorked(db[collName].createIndex({a: 1}, {unique: true}));

// Start a multi-document transaction that inserts an `a: 2` update.
const lsid = ({id: UUID()});
assert.commandWorked(db.runCommand({
    insert: collName,
    documents: [{a: 2}],
    lsid,
    txnNumber: NumberLong(1),
    autocommit: false,
    startTransaction: true,
}));

// Prepare the `a: 2` update.
let prepTs = assert.commandWorked(db.adminCommand({
    prepareTransaction: 1,
    lsid,
    txnNumber: NumberLong(1),
    autocommit: false
}))["prepareTimestamp"];

// In another thread, perform an insert that also attempts to touch the `a: 2` update. This insert
// will block until the above transaction commits or aborts. If the above transaction commits, this
// insert will fail with a duplicate key. If the above transaction is aborted, this insert will
// succeed.
//
// This insert will open up a hole in the oplog preventing writes from becoming majority
// committed. In a properly behaving system, we will notice this resource being held while
// entering a blocking call (prepare conflict resolution) and retry the transaction (which
// releases the resource that prevents writes from becoming majority committed).
const triggerPrepareConflictThread = new Thread(function(host, ns) {
    const conn = new Mongo(host);
    const collection = conn.getCollection(ns);
    jsTestLog("Inserting a conflicting operation while keeping a hole open.");
    assert.commandFailedWithCode(collection.insert([{a: 1}, {a: 2}, {a: 3}]),
                                 ErrorCodes.DuplicateKey);
}, primary.host, db[collName].getFullName());

triggerPrepareConflictThread.start();

// Wait for the insert to be in the system before attempting the majority write. Technically, this
// is insufficient to prove we're properly exercising the code that detects a possible deadlock and
// releases resources. In these cases, the test succeeds because the (yet to happen) majority write
// occurs before the above thread creates a hole.
assert.soon(() => {
    const ops = primary.getDB("admin")
                    .aggregate([
                        {$currentOp: {allUsers: true}},
                        {
                            $match: {
                                type: "op",
                                ns: db[collName].getFullName(),
                                "command.insert": {$exists: true},
                            }
                        }
                    ])
                    .toArray();

    if (ops.length === 0) {
        return false;
    }

    assert.eq(ops.length, 1, ops);
    return true;
});

// If the system is misbehaving, this write will fail to "majority replicate". As noted above, in a
// single node replica set, an operation must be visible in the oplog before it can be considered
// majority replicated.
jsTestLog("Doing the majority write.");
assert.soon(() => {
    assert.commandWorked(db.bla.insert({}, {writeConcern: {w: "majority"}}));
    return true;
});

// We could stop the test here, but by committing the transaction we can also assert that the
// `triggerPrepareConflictThread` sees a `DuplicateKey` error.
jsTestLog({"Committing. CommitTs": prepTs});
assert.commandWorked(db.adminCommand({
    commitTransaction: 1,
    lsid,
    txnNumber: NumberLong(1),
    autocommit: false,
    commitTimestamp: prepTs
}));

triggerPrepareConflictThread.join();

rst.stopSet();
})();