Idempotency — Two Clicks, One Charge
Stop an old worker from overwriting a new result: design principles
In one line
You implement an ownership lease and an increasing fencing token in SQLite.
Why this was needed
Worker A took a job and then stalled. When the lease expired, B took the same job and completed it, but A, which came back to life late, also wrote a completion result. Deciding the lease duration alone cannot block the writes of an old worker. At the time of storing, you must also check the owner and the generation number.
How it works
jobs stores id, owner, until, fence, result, and done. claim reads the current state inside BEGIN IMMEDIATE, takes only expired jobs, and increments fence. Renewal and completion are allowed only when both the current owner and fence are the same and the lease is still valid. It does not guarantee exactly-once execution, but it closes the path by which a past generation overwrites a stored result.
claim A fence=1 → 만료 → claim B fence=2 → 완료
A의 fence=1 완료 ───────────→ 거절
Worksheet: read the contract and predict the failure
What follows is not an answer sheet for memorizing the implementation, but a step-by-step code review. Each changed fragment deliberately breaks the contract. Note that normal cases may still pass after the change. Before running, predict which input, exception, or state you would have to observe to reveal the difference, and after implementing, compare that prediction with the result.
1. Create the lease state table
init_db(path) idempotently creates jobs(id TEXT PRIMARY KEY, owner TEXT, until REAL NOT NULL DEFAULT 0, fence INTEGER NOT NULL DEFAULT 0, result TEXT, done INTEGER NOT NULL DEFAULT 0).
Basis for the judgment: if the generation number is reset when a worker restarts, old tokens become valid again.
The wrong changed fragment to review:
CREATE TABLE jobs
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
2. Register only the first job
enqueue(path, job_id) inserts an id that does not exist in the default state and returns True, and if it already exists it does not change the state and returns False.
Basis for the judgment: use INSERT OR IGNORE so that re-registration does not reset a lease in progress.
The wrong changed fragment to review:
INSERT OR REPLACE INTO jobs
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
3. Read the current state
state(path, job_id) returns the row as a dict with the keys id, owner, until, fence, result, and done, and None if it does not exist.
Basis for the judgment: read the stored state over a separate DB connection to distinguish it from a cache inside the process.
The wrong changed fragment to review:
if row else {}
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
4. Take an expired lease atomically
claim(path, job_id, owner, now, ttl) is a ValueError if ttl<=0. A job that does not exist, a completed job, or a job with until>now gives None. Otherwise it stores owner and until=now+ttl, increments fence by 1, and returns the new fence.
Basis for the judgment: do the read and the update inside a single BEGIN IMMEDIATE. The boundary now==until can be reassigned.
The wrong changed fragment to review:
row[0] >= now
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
5. Only the current generation renews the lease
renew(path, job_id, owner, fence, now, ttl) is a ValueError if ttl<=0. Only when owner and fence match, done=0, and until>now does it change until=now+ttl and return True; otherwise it returns False.
Basis for the judgment: if you revive an already expired ownership with renew, it conflicts with the new worker.
The wrong changed fragment to review:
AND until>=?", (now+ttl
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
6. Reject a stale completion
complete(path, job_id, owner, fence, now, result) stores result and done=1 and returns True only when it is the current lease (owner and fence match, done=0, until>now). Otherwise it returns False and preserves the existing result.
Basis for the judgment: the completion write must also have the lease check to block a worker that returns late.
The wrong changed fragment to review:
AND done>=0 AND until>?", (result
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
7. Only the current worker returns the lease
release(path, job_id, owner, fence) changes the row where owner and fence are the same and done=0 to owner=NULL and until=0, and returns True. fence is preserved. Otherwise it returns False.
Basis for the judgment: if you reset even fence when returning the lease, past token numbers get reused.
The wrong changed fragment to review:
SET owner=NULL,until=0,fence=0 WHERE
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
8. There is one winner among concurrent claims
claim_many(path, job_id, owners, now, ttl) calls the claim of each owner concurrently with ThreadPoolExecutor and returns a list of return values in input order. For a new job, exactly one gets a fence and the rest must be None.
Basis for the judgment: do not close the connection after a pre-lookup and then update. The lock must protect both operations together.
The wrong changed fragment to review:
return [claim(path,job_id,owners[0],now,ttl)]
Compare it with the public contract of the function that contains this fragment. If a single success case cannot tell the difference, choose as the observation target an input that should be rejected or the state left after a failure.
What it looks like in the field
The clock is a non-decreasing number given by the caller, and clock synchronization between several servers is not modeled. The external side effects of the business logic are not fenced automatically. Systems outside the storage also need the same token verification or separate idempotency handling. SQLite uses real locks and transactions, but it does not represent the throughput of a large distributed queue.
What you will do in the next lab
The eight steps connect into one runnable deliverable. Create the lease state table → register only the first job → read the current state → take an expired lease atomically → only the current generation renews the lease → reject a stale completion → only the current worker returns the lease → there is one winner among concurrent claims.
Each step checks not the fact that a function or file exists but the actual return values, exceptions, and state changes. After you see the answer, deliberately change a boundary comparison or the cleanup code and check which test fails. Explain why the earlier tests are kept in the next step too, and write down one operational condition that this lab does not guarantee.