Exercise 3: Retrying After a Detected Conflict — Possible Solution ==================================================================== THE TEST ------------------------------ store = MVCCStore(detect_conflicts=True) setup = store.begin(); store.write(setup, 'balance', 100); store.commit(setup) ra = store.begin(); ca = store.read(ra, 'balance') rb = store.begin(); cb = store.read(rb, 'balance') store.write(ra, 'balance', ca + 10) store.write(rb, 'balance', cb + 20) store.commit(ra) # succeeds: 100 -> 110 try: store.commit(rb) raise AssertionError("expected a WriteConflict here") except WriteConflict: pass # rb correctly rejected # RETRY: a brand new transaction, re-reading the CURRENT value retry = store.begin() current_value = store.read(retry, 'balance') store.write(retry, 'balance', current_value + 20) store.commit(retry) final = store.read(store.begin(), 'balance') RESULT ------------------------------ value after r1's commit + rb's conflict + a retry of rb's own +20: 130 The final balance is 130 -- the true total of the original 100 plus both real increments (+10 and +20), with nothing silently lost. WHY THE RETRY HAS TO USE A FRESH TRANSACTION, NOT THE OLD ONE ------------------------------ rb's own snapshot was captured back when the balance was still 100 -- that's the whole reason its commit was rejected in the first place (its own write was computed from data that was no longer current by the time it tried to save it). Simply calling store.commit(rb) again wouldn't help: rb's own txn_snapshot value never changes after begin(), and its own pending write (cb + 20, using the STALE cb = 100) never changes either, so a second commit attempt would fail for the exact same reason as the first. A genuine retry needs a transaction whose snapshot reflects the CURRENT state of the database -- which means calling store.begin() again, capturing a fresh snapshot (one that correctly places ra's own already-committed change in the past), then reading the row again under that new snapshot (getting 110, not the stale 100), and only then computing and writing the new value. WHY THIS PRODUCES THE CORRECT RESULT ------------------------------ The retry's own read genuinely reflects ra's own committed change (it reads 110, not 100), so current_value + 20 correctly computes 130 -- built on real, current data rather than a stale snapshot. This is exactly the behavior Finding 5's own WITHOUT-conflict-detection test failed to produce: there, the second transaction's write silently overwrote the first's, landing on 120 (100 + 20, with the +10 completely discarded) instead of the true 130. Conflict detection alone doesn't recover the lost update -- it only PREVENTS the silent loss by forcing an explicit retry, which is what actually recovers it. WHY THIS WORKS AS AN ANSWER ------------------------------ Completing the retry loop the chapter's own Finding 5 stopped short of -- catching the WriteConflict is only half the story -- and confirming the final balance is the true, undiminished sum of every real increment demonstrates that first-committer-wins is a genuinely usable strategy in practice, not just a way to fail loudly instead of silently.