Idempotency — Two Clicks, One Charge
Two customers reserved the last item: design principles
In one line
You tie stock deduction, the reservation id, and cancellation together atomically and prevent overselling.
Why this was needed
One item of stock was left, but two requests looked it up at the same time and both succeeded in reserving. Afterward, when the same cancel message arrived twice, the stock became larger than the original. Deduplication of a reservation and deduplication of a cancellation are different state transitions, and the check of the remaining quantity must also be in the same transaction as the deduction.
How it works
stock holds the current remaining quantity, and reservations holds the reservation id, product, quantity, and whether it is cancelled. A re-request with the same reservation id and the same content ends with False, and different content is a conflict. Stock is deducted only at the first reservation, and a cancellation returns stock just once, when the state changes from active to cancelled. Reusing a cancelled reservation id as the same reservation does not create a new reservation.
재고 검사 + 예약 삽입 + 차감 → COMMIT
예약 active → 취소 + 재고 반환 → cancelled → 재취소 무동작
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. Pin the quantity down as an integer contract
quantity(value) returns only a positive int (not bool), and raises ValueError otherwise.
Basis for the judgment: keep a negative reservation from increasing stock.
The wrong changed fragment to review:
not isinstance(value,int)
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. Store stock and reservations separately
init_db(path) idempotently creates stock(sku TEXT PRIMARY KEY,available INTEGER NOT NULL) and reservations(id TEXT PRIMARY KEY,sku TEXT NOT NULL,qty INTEGER NOT NULL,cancelled INTEGER NOT NULL DEFAULT 0).
Basis for the judgment: a cancelled reservation must also remain so that you can tell the same cancellation from a re-reservation.
The wrong changed fragment to review:
CREATE TABLE reservations
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. Distinguish product initialization from a stock change
add_stock(path,sku,amount) checks for an int of 0 or more (not bool) and INSERTs only a new product. A duplicate product is an IntegrityError.
Basis for the judgment: keep the same initialization command from overwriting stock that is in use.
The wrong changed fragment to review:
INSERT OR REPLACE INTO stock VALUES
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. Look up the remaining quantity
available(path,sku) returns the stored quantity and raises KeyError if the product does not exist.
Basis for the judgment: distinguish a nonexistent product from sold out so that a wrong product id is not hidden.
The wrong changed fragment to review:
return 0
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. Tie the reservation and the stock deduction together
reserve(path,rid,sku,qty,fault=lambda:None) validates the quantity. An existing reservation id gives False if it has the same product and quantity, and ValueError if it differs. A new reservation checks that the product exists and the stock, and returns True after deduct → fault → insert reservation. A shortage or a nonexistent product is a ValueError.
Basis for the judgment: put the check and the deduction in the same transaction and roll back everything on a failure.
The wrong changed fragment to review:
db.commit()
fault()
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. Reflect a cancellation only once too
cancel(path,rid) returns False for a reservation that does not exist or is already cancelled. For an active reservation, it performs cancelled=1 and the stock return in the same transaction and returns True.
Basis for the judgment: stock must not keep increasing because of a resend of the cancel message.
The wrong changed fragment to review:
if row is None:
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. Check the reservation state
reservation(path,rid) returns a (sku,qty,cancelled) tuple or None.
Basis for the judgment: read not just the return value but whether the cancelled state remained in the DB.
The wrong changed fragment to review:
return db.execute("SELECT sku,qty,0
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. Reproduce the race for the last stock
compete(path,sku) runs reserve in two threads with different ids first and second and a quantity of 1. It returns the list of results in input order, with only ValueError turned into False.
Basis for the judgment: you have to compare two situations, stock 1 and stock 2, to filter out an implementation that unconditionally lets only one succeed.
The wrong changed fragment to review:
["first","first"]
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
This is not a lab that ties payment approval and stock reservation into one distributed transaction. Reservation expiry and compensation for payment failure are separate flows. With a small SQLite DB, you learn the principle of not trusting the stock number at lookup time and checking it inside the transaction that writes.
What you will do in the next lab
The eight steps connect into one runnable deliverable. Pin the quantity down as an integer contract → store stock and reservations separately → distinguish product initialization from a stock change → look up the remaining quantity → tie the reservation and the stock deduction together → reflect a cancellation only once too → check the reservation state → reproduce the race for the last stock.
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.