Files
mongo/jstests/replsets/dbhash_lock_acquisition.js
Josef Ahmad 1888cae7b0 SERVER-66340 Generalise the FeatureCompatibilityVersion lock into a MultiDocumentTransactionsBarrier
This barrier resolves a class of deadlocks involving an uncommitted transaction in the
prepared state and a global lock acquisition request in non-intent mode. Without this
barrier, this would result in a three-way deadlock between (A) a strong global lock
request which depends on (B) a prepared transaction which holds the global lock in intent
mode which depends on (C) the transaction coordinator persisting the commit/abort
decision, which depends on (A), thus establishing a circular dependency.

The FeatureCompatibilityVersion lock was initially introduced to address this issue
during FCV upgrades/downgrades, which acquire the global lock in MODE_S. This
patch generalises this lock into a MultiDocumentTransactionsBarrier to address any
non-intent global lock acquisitions, including fsyncLock.
The MultiDocumentTransactionsBarrier transparently drains prepared transactions
before acquiring the global lock in non-intent mode. It is implicitly acquired as part of a
global lock acquisition by operations processing transaction statements, and by operations
acquiring the global lock in non-intent mode. All other global lock requests skip this
lock. It is acquired in the same mode as the requested global lock mode.

One (benign) behavioural change is that prepared transactions that commit while a global
lock request is enqueued will not be able to acknowledge their write concern to the client
until the global lock is rescinded.

GitOrigin-RevId: 6517ac7482c0aa6da26af1b530bd5755737d6d1e
2023-12-13 19:47:13 +00:00

103 lines
3.5 KiB
JavaScript

/**
* Tests that the dbHash command acquires IS mode locks on the global, database, and collection
* resources when reading a timestamp using the $_internalReadAtClusterTime option.
*
* @tags: [
* requires_fcv_73, # Renames FeatureCompatibilityVersion -> MultiDocumentTransactionsBarrier
* uses_transactions,
* ]
*/
import {Thread} from "jstests/libs/parallelTester.js";
const rst = new ReplSetTest({nodes: 1});
rst.startSet();
rst.initiate();
const primary = rst.getPrimary();
const db = primary.getDB("test");
const session = primary.startSession({causalConsistency: false});
const sessionDB = session.getDatabase(db.getName());
// We insert a document so the dbHash command has a collection to process.
assert.commandWorked(sessionDB.mycoll.insert({}, {writeConcern: {w: "majority"}}));
const clusterTime = session.getOperationTime();
// We then start a transaction in order to be able have a catalog operation queue up behind it.
session.startTransaction();
assert.commandWorked(sessionDB.mycoll.insert({}));
const ops = db.currentOp({"lsid.id": session.getSessionId().id}).inprog;
assert.eq(
1, ops.length, () => "Failed to find session in currentOp() output: " + tojson(db.currentOp()));
assert.eq(ops[0].locks, {
MultiDocumentTransactionsBarrier: "w",
ReplicationStateTransition: "w",
Global: "w",
Database: "w",
Collection: "w",
});
const threadCaptruncCmd = new Thread(function(host) {
try {
const conn = new Mongo(host);
const db = conn.getDB("test");
// We use the captrunc command as a catalog operation that requires a MODE_X lock on the
// collection. This ensures we aren't having the dbHash command queue up behind it on a
// database-level lock. The collection isn't capped so it'll fail with an
// IllegalOperation error response.
assert.commandFailedWithCode(db.runCommand({captrunc: "mycoll", n: 1}),
ErrorCodes.IllegalOperation);
return {ok: 1};
} catch (e) {
return {ok: 0, error: e.toString(), stack: e.stack};
}
}, db.getMongo().host);
threadCaptruncCmd.start();
assert.soon(() => {
const ops = db.currentOp({"command.captrunc": "mycoll", waitingForLock: true}).inprog;
return ops.length === 1;
}, () => "Failed to find create collection in currentOp() output: " + tojson(db.currentOp()));
const threadDBHash = new Thread(function(host, clusterTime) {
try {
const conn = new Mongo(host);
const db = conn.getDB("test");
assert.commandWorked(db.runCommand({
dbHash: 1,
$_internalReadAtClusterTime: eval(clusterTime),
}));
return {ok: 1};
} catch (e) {
return {ok: 0, error: e.toString(), stack: e.stack};
}
}, db.getMongo().host, tojson(clusterTime));
threadDBHash.start();
assert.soon(() => {
const ops = db.currentOp({"command.dbHash": 1, waitingForLock: true}).inprog;
if (ops.length === 0 || !ops[0].locks.hasOwnProperty("Collection")) {
return false;
}
assert.eq(ops[0].locks, {
ReplicationStateTransition: "w",
Global: "r",
Database: "r",
Collection: "r",
});
return true;
}, () => "Failed to find create collection in currentOp() output: " + tojson(db.currentOp()));
assert.commandWorked(session.commitTransaction_forTesting());
threadCaptruncCmd.join();
threadDBHash.join();
assert.commandWorked(threadCaptruncCmd.returnData());
assert.commandWorked(threadDBHash.returnData());
session.endSession();
rst.stopSet();