TT Lab
Get started
Learn Learning paths Courses

Idempotency — Two Clicks, One Charge

Two customers reserved the last item

Continue in TT Lab

Goal

You tie stock deduction, the reservation id, and cancellation together atomically and prevent overselling.

Why it matters

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.

Steps

  1. In /root/work/idem-stock-reservation-lab/service.py, quantity(value) returns only a positive int (not bool), and raises ValueError otherwise.

Prepare it once at the beginning. It does not overwrite an existing file.

mkdir -p /root/work/idem-stock-reservation-lab
test -e /root/work/idem-stock-reservation-lab/service.py || cp /opt/fixtures/ten_labs/idem-stock-reservation-lab/service.py /root/work/idem-stock-reservation-lab/service.py
cd /root/work/idem-stock-reservation-lab
  1. In /root/work/idem-stock-reservation-lab/service.py, 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).

  2. In /root/work/idem-stock-reservation-lab/service.py, 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.

  3. In /root/work/idem-stock-reservation-lab/service.py, available(path,sku) returns the stored quantity and raises KeyError if the product does not exist.

  4. In /root/work/idem-stock-reservation-lab/service.py, 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.

  5. In /root/work/idem-stock-reservation-lab/service.py, 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.

  6. In /root/work/idem-stock-reservation-lab/service.py, reservation(path,rid) returns a (sku,qty,cancelled) tuple or None.

  7. In /root/work/idem-stock-reservation-lab/service.py, 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.

Notes

Pin the quantity down as an integer contract

In /root/work/idem-stock-reservation-lab/service.py, quantity(value) returns only a positive int (not bool), and raises ValueError otherwise.

Prepare it once at the beginning. It does not overwrite an existing file.

mkdir -p /root/work/idem-stock-reservation-lab
test -e /root/work/idem-stock-reservation-lab/service.py || cp /opt/fixtures/ten_labs/idem-stock-reservation-lab/service.py /root/work/idem-stock-reservation-lab/service.py
cd /root/work/idem-stock-reservation-lab

Keep a negative reservation from increasing stock.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/01-contract.sh.

Store stock and reservations separately

In /root/work/idem-stock-reservation-lab/service.py, 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).

A cancelled reservation must also remain so that you can tell the same cancellation from a re-reservation.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/02-contract.sh.

Distinguish product initialization from a stock change

In /root/work/idem-stock-reservation-lab/service.py, 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.

Keep the same initialization command from overwriting stock that is in use.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/03-contract.sh.

Look up the remaining quantity

In /root/work/idem-stock-reservation-lab/service.py, available(path,sku) returns the stored quantity and raises KeyError if the product does not exist.

Distinguish a nonexistent product from sold out so that a wrong product id is not hidden.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/04-contract.sh.

Tie the reservation and the stock deduction together

In /root/work/idem-stock-reservation-lab/service.py, 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.

Put the check and the deduction in the same transaction and roll back everything on a failure.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/05-contract.sh.

Reflect a cancellation only once too

In /root/work/idem-stock-reservation-lab/service.py, 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.

Stock must not keep increasing because of a resend of the cancel message.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/06-contract.sh.

Check the reservation state

In /root/work/idem-stock-reservation-lab/service.py, reservation(path,rid) returns a (sku,qty,cancelled) tuple or None.

Read not just the return value but whether the cancelled state remained in the DB.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/07-contract.sh.

Reproduce the race for the last stock

In /root/work/idem-stock-reservation-lab/service.py, 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.

You have to compare two situations, stock 1 and stock 2, to filter out an implementation that unconditionally lets only one succeed.

After saving, check with bash /opt/lab/checks/idem-stock-reservation-lab/08-contract.sh.