Module 4 of 9 · Lesson 10 of 26
Protect capacity decisions in one transaction
Work through protect capacity decisions in one transaction using a runnable reference, a focused regression check and a local extension.
Read in any order. All lessons stay open, including after an unanswered or incorrect check.
The parent row is the coordination point. Every path that changes active capacity must use the same protocol, including check-in in the final feature. Locking only existing rental rows cannot protect the case where no rental exists yet. A Python lock in one worker is also insufficient when multiple processes handle requests. Use a consistent lock order to reduce deadlocks, and expect real systems to need timeout and retry policies around contention. This example uses PostgreSQL's default read-committed behavior and locks equipment before rental state. A capacity conflict is a deliberate 409 outcome, not an infrastructure failure. Returning success before the transaction commits would defeat the guarantee.
The lesson10 check starts two worker threads at a barrier. Each requests all two units of the same item. Exactly one must reserve and the other must receive capacity_unavailable. The database sum must stay at two. This checks the race using independent PostgreSQL sessions, not sequential mocked responses.
def reserve(session, equipment_id, owner_id, quantity):
if quantity < 1:
raise DomainError("invalid_quantity", "Request at least one unit.", 422)
with session.begin():
item = session.scalar(
select(Equipment).where(Equipment.id == equipment_id).with_for_update()
)
if item is None:
raise missing()
used = session.scalar(
select(func.coalesce(func.sum(Rental.quantity), 0)).where(
Rental.equipment_id == equipment_id, Rental.status == "active"
)
)
if used + quantity > item.quantity:
raise DomainError(
"capacity_unavailable", "Not enough equipment units are available."
)
rental = Rental(equipment_id=equipment_id, owner_id=owner_id, quantity=quantity)
session.add(rental)
session.flush()
result = RentalOut.model_validate(rental)
return result
python run_checks.py -k lesson10
Expected result The selected lesson test passes against a new temporary PostgreSQL database; the container is removed afterward.
Keep for reference
Equipment Rental lab and lesson checks
ZIP containing Python source, real Alembic migrations, 26 lesson checks, a dependency lock and text instructions. Extract it before following the local exercise.
Practise locally
Temporarily remove with_for_update in a copy and add a barrier between reading capacity and inserting. Demonstrate the overbooking defect, then restore the lock and require one winner. Do not leave the barrier in production code. Add a test with different equipment items to show that unrelated capacity decisions do not need the same parent lock.
The lesson check verifies the reference behavior. Add your own assertions for your change. Local practice is not uploaded or scored by this learning release.
Pause and reflect
What failure does this lesson prevent, and which assertion in lesson10 would expose it?
Use a concrete input, expected result and limitation from your local work. Saving a reflection does not certify the project.
Optional knowledge check
What makes the capacity decision safe across API workers?
All capacity-changing paths coordinate on the same database row inside a transaction.
Correct. The shared database lock serializes the read-and-write decision across processes.
Each worker has its own Python lock and checks stock before starting a transaction.
Try another answer. Separate process locks and stale pre-transaction reads do not coordinate the shared database.
Lock only the existing active rental rows.
Try another answer. The empty-rental case has no row to lock; coordinate on the equipment parent.
This practice does not assess your project or award a certificate.
Your reading progress
Progress is saved in this browser when storage is available.