Make failure and retries part of the design
Picture
Two threads both see the same free spot. Separately checking and claiming lets both succeed. A retry adds another problem: even a single caller might repeat an operation after losing the first response.
Predict
What two protections are needed for competing callers and repeated requests?
Walk through the small example
An atomic check-and-claim boundary protects availability. A stable request ID with remembered input and result makes retries idempotent and detects conflicting key reuse.
Mental model and complete Python solution
The lock guards availability and result records together. A repeated key with the same spot replays its earlier result without allocating again. The same key with a different spot raises, because it no longer identifies the same request. This example records both successful and failed claims; a new attempt after availability changes needs a new request ID. The lock protects only threads sharing this object, and records are lost on process exit.
What the function promises: ClaimStore receives a set of initially available spot IDs. claim(request_id, spot) returns whether that operation reserved the spot. Calls may run from multiple threads in one process. No release or persistent storage is included.
from threading import Lock
from concurrent.futures import ThreadPoolExecutor
class ClaimStore:
def __init__(self, spots):
self._available = set(spots)
self._results = {}
self._lock = Lock()
def claim(self, request_id, spot):
if not request_id:
raise ValueError("A stable request ID is required")
with self._lock:
if request_id in self._results:
previous_spot, result = self._results[request_id]
if previous_spot != spot:
raise ValueError("Request ID reused with different input")
return result
result = spot in self._available
if result:
self._available.remove(spot)
self._results[request_id] = (spot, result)
return result
store = ClaimStore({1})
assert store.claim("request-a", 1)
assert store.claim("request-a", 1)
assert not store.claim("request-b", 1)
try:
store.claim("request-a", 2)
except ValueError:
pass
else:
raise AssertionError("Conflicting request reuse was accepted")
racing = ClaimStore({7})
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(lambda i: racing.claim(str(i), 7), range(12)))
assert sum(results) == 1
Work and storage: Expected O(1) lookup and update inside a serialized critical section. Memory grows with spots and remembered request IDs; an expiry policy must be consistent with the promised retry window.
When to recognize it: Checking availability and claiming it must be one atomic operation; a retry needs a stable request identity.
Check before applying it: A Python lock protects one process only. Multiple servers need shared transactional storage; crash recovery needs durable idempotency records.
Change one rule
Two servers share a database but not Python memory. What must replace this local-only guarantee?
See what changes
Use a shared transactional boundary, such as a unique claim constraint plus a durable idempotency record committed with the state change. Define retries after crashes and conflicts explicitly; a local lock cannot coordinate another process.
Connect
Design a parking lot: Move the allocation invariant into one protected boundary.
Design an expense ledger: Validation and atomic updates protect conservation too.