summaryrefslogtreecommitdiff
path: root/jstests/core/txns/abort_transaction_thread_does_not_block_on_locks.js
blob: 79e1c1f681bfb118e92b4893546a4d0e2fc5ec17 (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
// Tests that the asychronous thread that aborts expired transactions will not get stuck behind a
// drop command blocked on two transactions. A drop cmd requires a database exclusive lock, which
// will block behind transactions holding database intent locks. Aborting a transaction must not
// require taking further locks that would queue up behind the drop cmd's database exclusive lock
// request.
//
// Specifically testing that transaction cursor cleanup, which takes intent locks after aborting a
// transaction, does not block the transaction aborter thread when another active transaction still
// holds intent locks and there is a exclusive lock request already queued up. The aborter thread
// should just abandon the cursor cleanup and continue: best-effort.
//
// @tags: [uses_transactions]
(function() {
    "use strict";

    const dbName = "test";
    const collName = "abort_transaction_thread_does_not_block_on_locks";
    const testDB = db.getSiblingDB(dbName);
    const testColl = testDB[collName];
    const sessionOptions = {causalConsistency: false};

    let dropRes = testDB.runCommand({drop: collName, writeConcern: {w: "majority"}});
    if (!dropRes.ok) {
        assert.commandFailedWithCode(dropRes, ErrorCodes.NamespaceNotFound);
    }

    const bulk = testColl.initializeUnorderedBulkOp();
    for (let i = 0; i < 4; ++i) {
        bulk.insert({_id: i});
    }
    assert.commandWorked(bulk.execute({w: "majority"}));

    const res = assert.commandWorked(
        db.adminCommand({getParameter: 1, transactionLifetimeLimitSeconds: 1}));
    const originalTransactionLifetimeLimitSeconds = res.transactionLifetimeLimitSeconds;

    try {
        let transactionLifeTime = 10;
        jsTest.log("Decrease transactionLifetimeLimitSeconds to " + transactionLifeTime +
                   " seconds.");
        assert.commandWorked(db.adminCommand(
            {setParameter: 1, transactionLifetimeLimitSeconds: transactionLifeTime}));

        // Set up two transactions with IX locks and cursors.

        let session1 = db.getMongo().startSession(sessionOptions);
        let sessionDb1 = session1.getDatabase(dbName);
        let sessionColl1 = sessionDb1[collName];

        let session2 = db.getMongo().startSession(sessionOptions);
        let sessionDb2 = session2.getDatabase(dbName);
        let sessionColl2 = sessionDb2[collName];

        let firstTxnNumber = 1;
        let secondTxnNumber = 2;

        jsTest.log("Setting up first transaction with an open cursor and IX lock");
        let cursorRes1 = assert.commandWorked(sessionDb1.runCommand({
            find: collName,
            batchSize: 2,
            readConcern: {level: "snapshot"},
            txnNumber: NumberLong(firstTxnNumber),
            stmtId: NumberInt(0),
            startTransaction: true,
            autocommit: false
        }));
        assert(cursorRes1.hasOwnProperty("cursor"), tojson(cursorRes1));
        assert.neq(0, cursorRes1.cursor.id, tojson(cursorRes1));

        jsTest.log("Setting up second transaction with an open cursor and IX lock");
        let cursorRes2 = assert.commandWorked(sessionDb2.runCommand({
            find: collName,
            batchSize: 2,
            readConcern: {level: "snapshot"},
            txnNumber: NumberLong(secondTxnNumber),
            stmtId: NumberInt(0),
            startTransaction: true,
            autocommit: false
        }));
        assert(cursorRes2.hasOwnProperty("cursor"), tojson(cursorRes2));
        assert.neq(0, cursorRes2.cursor.id, tojson(cursorRes2));

        // Start the drop.

        jsTest.log("Starting a drop operation, which will block until both transactions finish");
        let awaitDrop = startParallelShell(function() {
            db.getSiblingDB("test")["abort_transaction_thread_does_not_block_on_locks"].drop(
                {writeConcern: {w: "majority"}});
        });

        assert.soon(function() {
            // Wait for the drop to have a pending MODE_X lock on the database, which will block
            // MODE_IS lock requests behind it.
            return testDB.runCommand({find: collName, maxTimeMS: 100}).code ===
                ErrorCodes.MaxTimeMSExpired;
        });

        // Should take 'transactionLifeTime' plus the period of the transaction aborter thread,
        // which is transactionLifeTime / 2 (or a max of 1 minute).
        jsTest.log("Waiting for transactions to expire and drop to finish. Should take " +
                   transactionLifeTime * 1.5 + " seconds or less.");
        awaitDrop();

        // Verify and cleanup.

        jsTest.log("Drop finished. Verifying that the transactions were aborted as expected");
        assert.commandFailedWithCode(sessionDb1.adminCommand({
            commitTransaction: 1,
            txnNumber: NumberLong(firstTxnNumber),
            stmtId: NumberInt(2),
            autocommit: false
        }),
                                     ErrorCodes.NoSuchTransaction);
        assert.commandFailedWithCode(sessionDb2.adminCommand({
            commitTransaction: 1,
            txnNumber: NumberLong(secondTxnNumber),
            stmtId: NumberInt(2),
            autocommit: false
        }),
                                     ErrorCodes.NoSuchTransaction);

        session1.endSession();
        session2.endSession();
    } finally {
        assert.commandWorked(db.adminCommand({
            setParameter: 1,
            transactionLifetimeLimitSeconds: originalTransactionLifetimeLimitSeconds
        }));
    }
}());