[SERVER-33610] If using two-phase locking, lock manager's list of resources to unlock can grow without bound Created: 02/Mar/18  Updated: 29/Oct/23  Resolved: 05/Apr/18

Status: Closed
Project: Core Server
Component/s: Concurrency
Affects Version/s: None
Fix Version/s: 3.7.4

Type: Improvement Priority: Major - P3
Reporter: David Storch Assignee: Maria van Keulen
Resolution: Fixed Votes: 0
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Backwards Compatibility: Fully Compatible
Sprint: Storage NYC 2018-03-26, Storage NYC 2018-04-09
Participants:

 Description   

In order to support readConcern level "snapshot", the Locker can be configured to use two-phase locking. This means that when unlock() is called, actually unlocking the resource is deferred until the WriteUnitOfWork ends. This is implemented by pushing the resource id onto a list of resources to unlock at the end of the transaction:

https://github.com/mongodb/mongo/blob/a53005feed80e81610183b542b5aaa44b85dd3a9/src/mongo/db/concurrency/lock_state.cpp#L478

If a lock is recursively acquired n times and unlocked n times, then this list will contain the same resource id repeating n times. This is a particular problem for query execution paths which can repeatedly call lock() and unlock(). Aggregation is the most important such code path, and in particular queries involving $lookup can lock and unlock the same resource repeatedly. The length of the _resourcesToUnlockAtEndOfUnitOfWork list can thus grow in proportion to the number of documents processed by the $lookup.

I was able to verify this using an in-progress implementation of readConcern level "snapshot" support for agg (see SERVER-33541). The following repro involves just two collections and 20 documents, yet the length of the list of resources to unlock grows to a maximum of 43 (as indicated by logging I added to LockerImpl).

(function() {
    "use strict";
 
    const dbName = "test";
    const kNumDocs = 10;
 
    let rst = new ReplSetTest({nodes: 1});
    rst.startSet();
    rst.initiate();
 
    const primaryDB = rst.getPrimary().getDB(dbName);
    const session = primaryDB.getMongo().startSession({causalConsistency: false});
    const sessionDb = session.getDatabase(dbName);
 
    // Insert documents into two collections, which we will join with $lookup.
    for (let i = 0; i < kNumDocs; i++) {
        assert.writeOK(sessionDb.c1.insert({_id: i}));
        assert.writeOK(sessionDb.c2.insert({_id: i}));
    }
 
    let pipeline = [{$lookup: {from: "c2", localField: "_id", foreignField: "_id", as: "as"}}];
    let aggCmd = {
        aggregate: "c1",
        pipeline: pipeline,
        cursor: {},
        readConcern: {level: "snapshot"},
        txnNumber: NumberLong(0)
    };
 
    assert.commandWorked(sessionDb.runCommand(aggCmd));
 
    rst.stopSet();
}());



 Comments   
Comment by Githook User [ 05/Apr/18 ]

Author:

{'email': 'maria@mongodb.com', 'name': 'Maria van Keulen', 'username': 'mvankeulen94'}

Message: SERVER-33610 Remove unused resources queue
Branch: master
https://github.com/mongodb/mongo/commit/1bb0115790f1ee670532dae38e0f373240e23d2d

Comment by Githook User [ 05/Apr/18 ]

Author:

{'email': 'maria@mongodb.com', 'name': 'Maria van Keulen', 'username': 'mvankeulen94'}

Message: SERVER-33610 Recycle 2-phase-locks pending unlock during lock acquisition
Branch: master
https://github.com/mongodb/mongo/commit/6fbc1bbfcd5ffcfb451c300a6ef523f19d5edb55

Generated at Thu Feb 08 04:34:00 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.