summaryrefslogtreecommitdiff
path: root/jstests/core/txns/write_conflicts_with_non_txns.js
blob: e8c3f9fcd473ad5b31740268c685b16ba9eef676 (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
/**
 * Test the write conflict behavior between transactional and non-transactional (single document)
 * writes.
 *
 * All writes in MongoDB execute inside transactions. Single document writes (which, until 4.0,
 * categorized all writes), will indefinitely retry, if their associated transaction encounters a
 * WriteConflict error. This differs from the behavior of multi-document transactions, where
 * WriteConflict exceptions that occur inside a transaction are not automatically retried, and are
 * returned to the client. This means that writes to a document D inside a multi-document
 * transaction will effectively "block" any subsequent single document writes to D, until the
 * multi-document transaction commits.
 *
 * Note that in this test we sometimes refer to a single document write as "non-transactional".
 * Internally, single document writes still execute inside a transaction, but we use this
 * terminology to distinguish them from multi-document transactions.
 *
 * @tags: [uses_transactions]
 */

(function() {

"use strict";

load('jstests/libs/parallelTester.js');  // for ScopedThread.

const dbName = "test";
const collName = "write_conflicts_with_non_txns";

const testDB = db.getSiblingDB(dbName);
const testColl = testDB[collName];

// Clean up and create test collection.
testDB.runCommand({drop: collName, writeConcern: {w: "majority"}});
assert.commandWorked(testDB.runCommand({create: collName, writeConcern: {w: "majority"}}));

const sessionOptions = {
    causalConsistency: false
};
const session = db.getMongo().startSession(sessionOptions);
const sessionDb = session.getDatabase(dbName);
const sessionColl = sessionDb[collName];

// Two conflicting documents to be inserted by a multi-document transaction and a
// non-transactional write, respectively.
const txnDoc = {
    _id: 1
};
const nonTxnDoc = {
    _id: 1,
    nonTxn: true
};

// Performs a single document insert on the test collection. Returns the command result object.
function singleDocWrite(dbName, collName, doc) {
    const testColl = db.getSiblingDB(dbName)[collName];
    return testColl.runCommand({insert: collName, documents: [doc]});
}

// Returns true if a single document insert has started running on the server.
function writeStarted() {
    return testDB.currentOp().inprog.some(op => {
        return op.active && (op.ns === testColl.getFullName()) && (op.op === "insert") &&
            (op.writeConflicts > 0);
    });
}

/**
 * A non-transactional (single document) write should keep retrying when attempting to insert a
 * document that conflicts with a previous write done by a running transaction, and should be
 * allowed to continue after the transaction commits. If 'maxTimeMS' is specified, a single
 * document write should timeout after the given time limit if there is a write conflict.
 */

jsTestLog("Start a multi-document transaction with a document insert.");
session.startTransaction();
assert.commandWorked(sessionColl.insert(txnDoc));

jsTestLog("Do a conflicting single document insert outside of transaction with maxTimeMS.");
assert.commandFailedWithCode(
    testColl.runCommand({insert: collName, documents: [nonTxnDoc], maxTimeMS: 100}),
    ErrorCodes.MaxTimeMSExpired);

jsTestLog("Doing conflicting single document write in separate thread.");
let thread = new ScopedThread(singleDocWrite, dbName, collName, nonTxnDoc);
thread.start();

// Wait for the single doc write to start.
assert.soon(writeStarted);

// Commit the transaction, which should allow the single document write to finish. Since the
// single doc write should get serialized after the transaction, we expect it to fail with a
// duplicate key error.
jsTestLog("Commit the multi-document transaction.");
assert.commandWorked(session.commitTransaction_forTesting());
thread.join();
assert.commandFailedWithCode(thread.returnData(), ErrorCodes.DuplicateKey);

// Check the final documents.
assert.sameMembers([txnDoc], testColl.find().toArray());

// Clean up the test collection.
assert.commandWorked(testColl.remove({}));

/**
 * A non-transactional (single document) write should keep retrying when attempting to insert a
 * document that conflicts with a previous write done by a running transaction, and should be
 * allowed to continue and complete successfully after the transaction aborts.
 */

jsTestLog("Start a multi-document transaction with a document insert.");
session.startTransaction();
assert.commandWorked(sessionColl.insert(txnDoc));

jsTestLog("Doing conflicting single document write in separate thread.");
thread = new ScopedThread(singleDocWrite, dbName, collName, nonTxnDoc);
thread.start();

// Wait for the single doc write to start.
assert.soon(writeStarted);

// Abort the transaction, which should allow the single document write to finish and insert its
// document successfully.
jsTestLog("Abort the multi-document transaction.");
assert.commandWorked(session.abortTransaction_forTesting());
thread.join();
assert.commandWorked(thread.returnData());

// Check the final documents.
assert.sameMembers([nonTxnDoc], testColl.find().toArray());

// Clean up the test collection.
assert.commandWorked(testColl.remove({}));

/**
 * A transaction that tries to write to a document that was updated by a non-transaction after
 * it started should fail with a WriteConflict.
 */

jsTestLog("Start a multi-document transaction.");
session.startTransaction();
assert.commandWorked(sessionColl.runCommand({find: collName}));

jsTestLog("Do a single document insert outside of the transaction.");
assert.commandWorked(testColl.insert(nonTxnDoc));

jsTestLog("Insert a conflicting document inside the multi-document transaction.");
assert.commandFailedWithCode(sessionColl.insert(txnDoc), ErrorCodes.WriteConflict);
assert.commandFailedWithCode(session.commitTransaction_forTesting(), ErrorCodes.NoSuchTransaction);

// Check the final documents.
assert.sameMembers([nonTxnDoc], testColl.find().toArray());
}());