Waiting and Resuming
Park a statement that needs a held lock, and run it again when the lock is free, with no threads
When a lock request conflicts, undo what the statement did so far, print who it waits for, and read the next line; when a transaction ends, resume parked statements in the order they parked by running them again from the start. UPDATE assignments may now read the row: set n = n + 1.
- A conflicting request waits; waiting is a state of the session, not of a thread
- A parked statement has changed nothing and keeps its locks, so it can simply run again
- Waiters are served in arrival order
- A resumed statement sees the rows as the releaser left them
- Waiting is what makes n = n + 1 safe: the second increment reads the first
Why this stage exists
Locks now record who uses what, but nothing happens when two requests collide. Collisions are the point. Session 1 has changed row 5 and not committed; session 2 wants to change it too. Session 2 must not touch it, and must not fail either: it should simply wait until session 1 is done.
A server would block session 2's thread and wake it later. This program has one thread and reads the next line. So waiting becomes a state of the session: its statement is parked, and the program carries on with the next line of input.
>@1 begin;@1| Begin statement executed successfully>@1 update c set n = 100 where id = 5;@1| Update statement executed successfully (1 rows)@1| [EXEC] rows_examined=1>@2 update c set n = n + 1 where id = 5;@2| [LOCK] session 2 waits for session 1 on c:5>@1 commit;@1| Commit statement executed successfully@2| Update statement executed successfully (1 rows)@2| [EXEC] rows_examined=1>select n from c where id = 5;n101(1 rows)[EXEC] rows_examined=1
Session 2's result appears right after session 1's COMMIT, because that is the moment it ran.
Parking and running again
A statement can wait halfway: an UPDATE of ten rows may get five and then find the sixth locked. Resuming it exactly where it stopped would mean saving its whole execution state. There is a simpler way that gives the same answer:
- When it parks, undo what it changed so far, as for a statement that fails, but keep the locks it took.
- When its lock is granted, run it again from the start, as if its line had just been typed.
The rows it had locked are still locked by it, so nobody changed them in between, and running again finds them the same. Rows it had not reached yet are read fresh, which is what a real engine's woken thread would see too.
Running again is also what makes n = n + 1 safe. From this stage an assignment may be an expression over the row, and every assignment reads the row as it was before the statement changed it, so set a = b, b = a swaps the two. the waiting UPDATE reads n when it runs again, after session 1's COMMIT, and adds one to 100, not to the 10 it would have seen before waiting. If session 1 had rolled back, it would add one to 10.
Who goes first
Requests are served in the order they began to wait. A request also waits when it would only conflict with a request already waiting: otherwise a stream of readers could keep a writer waiting forever. When a transaction ends, the parked statements are looked at oldest first; each one that can now have its lock runs. One that commits, as an autocommit statement does, releases its own locks in turn, so a whole queue can drain after one COMMIT, before the next input line is read.
While a statement waits, its session cannot do anything else: a line for it, prefixed or, for session 1, not, is 01803. sessions shows it WAITING, whom for, and the transaction the statement belongs to: in autocommit, the id the statement was given when it first ran, which it keeps when it runs again. If the program ends while statements are parked, they are dropped without printing anything; locks lists its request among the resource's waiters:
>locks[LOCKS] c holders=[1:IX,2:IX] waiters=[][LOCKS] c:5 holders=[1:X] waiters=[2:X]
The algorithm in words
When a request conflicts
Park, report, move on.
- 1.A request conflicts if another transaction holds an incompatible mode on the resource, or already waits there with one.
- 2.Undo what the statement changed so far, as for a statement that fails. Keep every lock it holds.
- 3.Remember the statement's line and its session's tag, and add its request to the resource's waiters.
- 4.Print [LOCK] session <n> waits for session <m> on <resource>: m is the lowest session holding a conflicting mode, or, if none does, the first waiter ahead with one.
- 5.Read the next input line. A line for this session, while it waits, is 01803.
When a transaction ends
Release, then let the waiters in, oldest first.
- 1.Release every lock of the ending transaction.
- 2.Go through the parked statements in the order they parked. The first whose request can now be granted gets it and runs its line again from the start, printing under its own tag.
- 3.If it ends a transaction too, as an autocommit statement does, that release counts like any other.
- 4.Start again from the first parked statement, until none can resume; only then read the next input line.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, section 85.
| Input | Output |
|---|---|
| a statement whose lock conflicts | [LOCK] session <n> waits for session <m> on <resource>, tagged as its line |
| the holder's COMMIT or ROLLBACK | its line, then each resumed statement's lines, tagged as its own line was (untagged if it had no prefix) |
| a line for a waiting session | [ERROR:01803] ... |
sessions while waiting | [SESSIONS] <n> trx=<id> state=WAITING waits_for=<m> iso=REPEATABLE_READ |
locks while waiting | waiters=[<n>:<mode>] on the resource |
What you should see when done
$droid --db w.dbWelcome to droid!>create table c (id int primary key, n int);Create table statement executed successfully>insert into c (id, n) values (9, 0);Insert statement executed successfully (1 rows)>@1 begin;@1| Begin statement executed successfully>@1 insert into c (id, n) values (10, 1);@1| Insert statement executed successfully (1 rows)>@2 insert into c (id, n) values (10, 2);@2| [LOCK] session 2 waits for session 1 on c:10>@2 select * from c;@2| [ERROR:01803] session 2 is waiting for a lock>@1 rollback;@1| Rollback statement executed successfully@2| Insert statement executed successfully (1 rows)>.exitexiting.. good bye!
Common bugs in this stage
- Keeping the parked statement's changes. A parked statement must have changed nothing, or running it again does its work twice.
- Releasing its locks when it parks. It keeps them; strict two-phase locking does not stop for waiting.
- Resuming before printing the releaser's line. The COMMIT's own line comes first.
- Resuming only one statement. Keep going until no parked statement can run.
- Reading the next line while a resumed statement could still run. The queue drains first.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: store a parked statement as a copy of its line and its session number in a small queue ordered by arrival; resuming is calling the same function the REPL calls for a typed line.
- C++: a
std::deque<Parked>; erase the entry before running its line, since running may park it again. - Rust: a
VecDeque<Parked>; take the entry out withremove(i)before running it, which keeps the borrow checker out of the way. - Zig: an
ArrayList(Parked)withorderedRemove, owning a copy of the line made with the session's allocator.
Checklist
One line per thing the tests check. The tags name the test cases.
- An UPDATE may assign an expression over the row: n = n + 1expression-assignment
- A statement that needs a held lock prints who it waits for, and runs after the holder commitswait-line
- sessions shows the session WAITING and locks lists its request as a waiterwaiting-shown
- After the holder rolls back, the resumed statement runs on the old rowresume-after-rollback
- Statements waiting for one row resume in the order they began to waitfifo
- A line for a waiting session is 01803input-while-parked
- An INSERT of a key another open transaction inserted waits, then fails or succeeds with itsame-key-insert
- Two holders of S on a row: one asking for X waits for the otherupgrade-waits
- A parent DELETE waits for a transaction whose foreign key check holds the rowparent-delete-waits
- DROP TABLE waits for a transaction using the tableddl-waits
- The end of the program drops waiting statements and rolls everything backexit-while-parked