Introduction
The Lock-sys orchestrates access to tables and rows. Each table, and each row, can be thought of as a resource, and a transaction may request access right for a resource. As two transactions operating on a single resource can lead to problems if the two operations conflict with each other, each lock request also specifies the way the transaction intends to use it, by providing a mode
. For example a LOCK_X mode, means that transaction needs exclusive access (presumably, it will modify the resource), and LOCK_S mode means that a transaction can share the resource with other transaction which also use LOCK_S mode. There are many different possible modes beside these two, and the logic of checking if given two modes are in conflict is a responsibility of the Lock-sys. A lock request, is called "a lock" for short. A lock can be WAITING or GRANTED.
So, a lock, conceptually is a tuple identifying:
- requesting transaction
- resource (a particular row, a particular table)
- mode (LOCK_X, LOCK_S,...)
- state (WAITING or GRANTED)
Conceptually, the Lock-sys maintains a separate queue for each resource, thus one can analyze and reason about its operations in the scope of a single queue.
The life cycle of a lock is usually as follows:
- The transaction requests the lock, which can either be immediately GRANTED, or, in case of a conflict with an existing lock, goes to the WAITING state.
- In case the lock is WAITING the thread (voluntarily) goes to sleep.
- A WAITING lock either becomes GRANTED (once the conflicting transactions finished and it is our turn) or (in case of a rollback) it gets canceled.
- Once the transaction is finishing (due to commit or rollback) it releases all of its locks.
When a lock is released (due to cancellation in Step 3, or clean up in Step 4), the Lock-sys inspects the corresponding lock queue to see if one or more of the WAITING locks in it can now be granted. If so, some locks become GRANTED and the Lock-sys signals their threads to wake up.
The scheduling algorithm
We use a variant of the algorithm described in paper "Contention-Aware Lock
Scheduling for Transactional Databases" by Boyu Tian, Jiamin Huang, Barzan Mozafari and Grant Schoenebeck. The algorithm, "CATS" for short, analyzes the Wait-for graph, and assigns a weight to each WAITING transaction, equal to the number of transactions which it (transitively) blocks. The idea being that favoring heavy transactions will help to make more progress by helping more transactions to become eventually runnable.
The actual implementation of this theoretical idea is currently as follows.
- Locks can be thought of being in 2 logical groups (Granted & Waiting) maintained in the same queue.
- Granted locks are added at the HEAD of the queue.
- Waiting locks are added at the TAIL of the queue.
The queue looks like: |
Grows <---- [HEAD] [G7 -- G3 -- G2 -- G1] -|- [W4 -- W5 -- W6] [TAIL] ---> Grows
Grant Group | Wait Group
G - Granted W - waiting,
suffix number is the chronological order of requests.
- When a new lock request comes, we check for conflict with all (GRANTED and WAITING) locks already in the queue.
- If there is a conflicting lock already in the queue, then the new lock request is put into WAITING state and appended at the TAIL. The transaction which requested the conflicting lock found is said to be the Blocking Transaction for the incoming transaction. As each transaction can have at most one WAITING lock, it also can have at most one Blocking Transaction, and thus we store the information about Blocking Transaction (if any) in the transaction object itself (as opposed to: separately for each lock request).
- If there is no conflict, the request can be GRANTED, and lock is prepended at the HEAD.
- When we release a lock, locks which conflict with it need to be checked again if they can now be granted. Note that if there are multiple locks which could be granted, the order in which we decide to grant has an impact on who will have to wait: granting a lock to one transaction, can prevent another waiting transaction from being granted if their request conflict with each other. At the minimum, the Lock-sys must guarantee that a newly GRANTED lock, does not conflict with any other GRANTED lock. Therefore, we will specify the order in which the Lock-sys checks the WAITING locks one by one, and assume that such check involves checking if there is any conflict with already GRANTED locks - if so, the lock remains WAITING, we update the Blocking Transaction of the lock to be the newly identified conflicting transaction, and we check a next lock from the sorted list, otherwise, we grant it (and thus it is checked against in subsequent checks). The Lock-sys uses CATS weight for ordering: it favors transactions with highest CATS weight. Moreover, only the locks which point to the transaction currently releasing the lock as their Blocking Transaction participate as candidates for granting a lock.
How do we choose the Blocking Transaction?
It is done differently for new lock requests in point 2. and differently for old lock requests in point 3.
For new lock requests, we simply scan the whole queue in its natural order, and the first conflicting lock is chosen. In particular, a WAITING transaction can be chosen, if it is conflicting, and there are no GRATNED conflicting locks.
For old lock requests we scan only the Grant Group, and we do so in the chronological order, starting from the oldest lock requests [G1,G2,G3,G7] that is from the middle of the queue towards HEAD. In particular we also check against the locks which recently become GRANTED as they were processed before us in the sorting order, and we do so in a chronological order as well.