Exercise 3: Avoiding Deadlock With a Consistent Global Lock Order — Possible Solution ==================================================================== THE TEST ------------------------------ lock_mgr = LockManager() results = {} def txn(name, first_page, second_page): ordered = sorted([first_page, second_page]) # ALWAYS the same global order lock_mgr.acquire_exclusive(ordered[0], name) time.sleep(0.05) lock_mgr.acquire_exclusive(ordered[1], name) results[name] = 'succeeded' lock_mgr.release_all(name) # txn A logically wants p2 then p1; txn B logically wants p1 then p2 -- # but BOTH acquire in sorted order regardless ta = threading.Thread(target=txn, args=('A', 'p2', 'p1')) tb = threading.Thread(target=txn, args=('B', 'p1', 'p2')) start = time.perf_counter() ta.start(); tb.start() ta.join(timeout=3.0); tb.join(timeout=3.0) elapsed = time.perf_counter() - start RESULT ------------------------------ results: {'A': 'succeeded', 'B': 'succeeded'} elapsed: 0.101s Both transactions succeed, and quickly -- nowhere near a 1-second timeout, let alone a genuine hang. WHY A CONSISTENT ORDER PREVENTS THE CYCLE FROM EVER FORMING ------------------------------ Every deadlock this chapter reproduced (Finding 4, and Exercise 2's own three-way version) depended on transactions acquiring the SAME set of pages in DIFFERENT orders -- A locks p1 first, B locks p2 first, so it becomes possible for each to end up holding what the other one needs next. sorted([first_page, second_page]) removes that possibility structurally: no matter which page a transaction's own logic "wants" first, it always physically requests p1 before p2 (since sorting is deterministic and identical for every transaction). The one exception -- a transaction that only ever wants ONE of the two pages -- can't participate in a two-page cycle at all, so the ordering rule only needs to apply to transactions that actually touch multiple shared pages. With every transaction following the same order, the two possible outcomes are: either A gets p1 first (and B then waits behind A for p1), or B gets p1 first (and A waits behind B) -- in both cases, ONE transaction genuinely finishes both its locks and releases them before the other can even begin its own second acquisition. There is no configuration in which each transaction ends up holding what the other one is waiting for, because neither one can ever acquire p2 before p1 in the first place. WHY THIS IS A GENUINELY DIFFERENT STRATEGY FROM DETECTION ------------------------------ Finding 5's wait-for-graph detector is a REACTIVE strategy -- it lets transactions request locks in any order they want, and catches a deadlock only once one is actually about to form, aborting a victim after the fact. Consistent lock ordering is a PREVENTIVE strategy -- it never lets the dangerous configuration exist at all, at the cost of requiring every piece of code that acquires multiple locks to agree on and follow one global ordering rule. Real database systems use both: ordering rules where they're practical to enforce, and detection as a backstop for the cases where an ordering discipline either isn't followed or genuinely can't be, because the order in which pages need to be touched isn't known in advance. WHY THIS WORKS AS AN ANSWER ------------------------------ Confirming both transactions finish quickly with no detector active at all demonstrates that consistent ordering isn't just a smaller target for deadlocks to hit -- it removes the specific structural condition (a cycle in who's-waiting-on-whom) that makes a deadlock possible in the first place, verified directly against the exact same two pages that produced a real, measured deadlock in Finding 4 when acquired in mismatched order.