Review observed PostgreSQL schedules and transaction reasoning
Use this key after the two-session PostgreSQL exercise. Compare the actual reservation, on-call and retry outcomes with the table below. Ask the learner to explain the statement order that produced each result before naming an isolation level.
The candidate should operate two independent sessions and narrate observed state, not merely recite isolation names. The runner (download file, source below) automates the same schedules; its result is reference evidence, not the candidate's attempt.
Read the supplied code · run_schedules.py
"""Execute real two-session PostgreSQL schedules. Requires psycopg and LAB_PG_DSN."""
import os
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from retry import transaction_retry
def main():
import psycopg
dsn = os.environ["LAB_PG_DSN"]
def connect():
connection = psycopg.connect(dsn, autocommit=True)
connection.execute("SET search_path=interview_lab")
connection.execute("SET statement_timeout='8s'")
connection.execute("SET lock_timeout='6s'")
return connection
with connect() as admin:
admin.execute(Path(__file__).with_name("schema.sql").read_text())
with connect() as a, connect() as b:
# Lost update: both application reads see 1, then write the literal 0.
a.execute("BEGIN")
b.execute("BEGIN")
assert a.execute("SELECT available FROM stock WHERE id=1").fetchone()[0] == 1
assert b.execute("SELECT available FROM stock WHERE id=1").fetchone()[0] == 1
for conn, buyer in ((a, "a"), (b, "b")):
conn.execute("UPDATE stock SET available=0 WHERE id=1")
conn.execute("INSERT INTO reservations VALUES (%s,1)", (buyer,))
conn.execute("COMMIT")
assert admin.execute("SELECT count(*) FROM reservations").fetchone()[0] == 2
print("FAILURE reproduced: stock=0, reservations=2 for initial stock=1")
admin.execute("TRUNCATE reservations")
admin.execute("UPDATE stock SET available=1")
a.execute("BEGIN")
b.execute("BEGIN")
assert a.execute("UPDATE stock SET available=available-1 WHERE id=1 AND available>0 RETURNING id").fetchone() == (1,)
a.execute("INSERT INTO reservations VALUES ('a',1)")
with ThreadPoolExecutor(max_workers=1) as pool:
waiting = pool.submit(b.execute, "UPDATE stock SET available=available-1 WHERE id=1 AND available>0 RETURNING id")
a.execute("COMMIT")
assert waiting.result().fetchone() is None
b.execute("COMMIT")
assert admin.execute("SELECT count(*) FROM reservations").fetchone()[0] == 1
assert admin.execute("SELECT available FROM stock WHERE id=1").fetchone()[0] == 0
print("FIX passed: conditional decrement admits one reservation")
# Write skew under snapshot isolation: disjoint writes evade row conflict.
for conn in (a, b):
conn.execute("BEGIN ISOLATION LEVEL REPEATABLE READ")
assert conn.execute("SELECT count(*) FROM doctors WHERE on_call").fetchone()[0] == 2
a.execute("UPDATE doctors SET on_call=false WHERE id=1")
b.execute("UPDATE doctors SET on_call=false WHERE id=2")
a.execute("COMMIT")
b.execute("COMMIT")
assert admin.execute("SELECT count(*) FROM doctors WHERE on_call").fetchone()[0] == 0
print("FAILURE reproduced: repeatable-read write skew leaves zero doctors")
admin.execute("UPDATE doctors SET on_call=true")
for conn in (a, b):
conn.execute("BEGIN ISOLATION LEVEL SERIALIZABLE")
assert conn.execute("SELECT count(*) FROM doctors WHERE on_call").fetchone()[0] == 2
a.execute("UPDATE doctors SET on_call=false WHERE id=1")
b.execute("UPDATE doctors SET on_call=false WHERE id=2")
failures = []
for conn, doctor in ((a, 1), (b, 2)):
try:
conn.execute("COMMIT")
except psycopg.Error as error:
conn.execute("ROLLBACK")
assert error.sqlstate == "40001", error
failures.append(doctor)
assert len(failures) == 1
def retry_off_call():
with connect() as conn:
try:
conn.execute("BEGIN ISOLATION LEVEL SERIALIZABLE")
count = conn.execute("SELECT count(*) FROM doctors WHERE on_call").fetchone()[0]
if count > 1:
conn.execute("UPDATE doctors SET on_call=false WHERE id=%s", (failures[0],))
conn.execute("COMMIT")
return count
except Exception:
conn.execute("ROLLBACK")
raise
assert transaction_retry(retry_off_call) == 1
assert admin.execute("SELECT count(*) FROM doctors WHERE on_call").fetchone()[0] == 1
print("FIX passed: serialization abort plus complete transaction retry preserves one doctor")
# Deadlock: opposite lock order. Victim rolls back before retry.
a.execute("BEGIN")
b.execute("BEGIN")
a.execute("UPDATE counters SET hits=hits+1 WHERE id=1")
b.execute("UPDATE counters SET hits=hits+1 WHERE id=2")
def finish(conn, second_id):
try:
conn.execute("UPDATE counters SET hits=hits+1 WHERE id=%s", (second_id,))
conn.execute("COMMIT")
return "ok"
except psycopg.Error as error:
conn.execute("ROLLBACK")
return error.sqlstate
with ThreadPoolExecutor(max_workers=2) as pool:
first = pool.submit(finish, a, 2)
second = pool.submit(finish, b, 1)
outcomes = sorted((first.result(), second.result()))
assert outcomes == ["40P01", "ok"], outcomes
assert admin.execute("SELECT hits FROM counters ORDER BY id").fetchall() == [(1,), (1,)]
print("FAILURE reproduced: deadlock 40P01; victim transaction rolled back")
def ordered_transaction():
with connect() as conn:
try:
conn.execute("BEGIN")
conn.execute("SELECT id FROM counters ORDER BY id FOR UPDATE").fetchall()
conn.execute("UPDATE counters SET hits=hits+1")
conn.execute("COMMIT")
except Exception:
conn.execute("ROLLBACK")
raise
transaction_retry(ordered_transaction)
assert admin.execute("SELECT hits FROM counters ORDER BY id").fetchall() == [(2,), (2,)]
admin.execute("UPDATE counters SET hits=0")
with ThreadPoolExecutor(max_workers=2) as pool:
one = pool.submit(transaction_retry, ordered_transaction)
two = pool.submit(transaction_retry, ordered_transaction)
one.result()
two.result()
assert admin.execute("SELECT hits FROM counters ORDER BY id").fetchall() == [(2,), (2,)]
print("FIX passed: ordered locks plus bounded whole-transaction retry")
if __name__ == "__main__":
main()
| Boundary | Expected evidence | Reasoning that is insufficient |
|---|---|---|
| Lost update | Two reservations in baseline; one after conditional decrement | “Stock is nonnegative, therefore no oversell” |
| Write skew | Repeatable-read count 0; serializable 40001; whole retry count 1 | “BEGIN” or a row lock on only the doctor's own row |
| Deadlock | 40P01 victim rollback; survivor count 1; full retry counts 2 | Retrying only the blocked statement in an aborted transaction |
| Retry budget | Three total attempts; 40001/40P01 only; no external effect in callback | Retrying every integrity error or claiming every timeout is safe to repeat |
| Plans | Actual/estimated rows, buffers, ordering and limit explained | Index must always win; elapsed time alone proves heap layout |
| Physical model | Separate heap and B-tree; conditional HOT eligibility | Ordered primary key automatically clusters heap |
Score each 0 absent, 1 explained, 2 independently reproduced and defended. Senior
practice should show all boundaries without the answer file. For an unseen
follow-up, add a third doctor and a concurrent shift transfer, or make the owner
predicate broad but keep LIMIT 20; ask the candidate to adjust the invariant
or predicted plan before running it. Lead scope adds lock/retry budgets across
teams and a compatible old-writer rollout. Do not convert scores into a pass
probability. No PostgreSQL execution in the author's environment is claimed.