Skip to main content

Module idempotency

Module idempotency 

Source
Expand description

The port a peer-observable side effect is deduplicated through.

§Why the trait lives here

Because the port belongs to its consumer. The consumer is the tools/call handler — the one use case whose side effect a peer can observe and retry — and this module is that use case’s seam. Nothing in domain/ or adapters/ declares it, and there is deliberately no shared ports module for it to be collected into: a trait collected away from its consumer is a trait whose shape drifts toward whatever is convenient to implement.

§Why the port is &self and not &mut self

Because the claim has to be atomic between concurrent callers, and the only thing a &mut self method can promise is exclusivity for the duration of one call. An HTTP server handles requests concurrently, so a store that can only be reached through &mut self is either behind a lock somewhere unavoidable or not safe to share at all. &self lets the adapter hold the lock internally — exactly where the atomicity belongs — and lets a server hold it in shared state.

§Why the result type is an associated type

So the trait is object-safe. The Streamable HTTP adapter keeps its store in router state, and router state is Arc<dyn …>; a trait with a T parameter would force every store to be monomorphised into the router, and a caller wanting to swap implementations for a test would have to name the result type at every use site. Arc<dyn IdempotencyStore<Result = …>> names it once.

§Why there is no async on these methods

Because the operations are a map lookup, an insert and a remove. Declaring them async would put a Pin<Box<dyn Future>>> in the signature of a hash-table operation and add an allocation per call to buy nothing. A store that genuinely needs to await — one writing to a database, say — implements this port and does its awaiting inside, which is what an adapter is for.

§The ordering, stated once more because it is the contract

IdempotencyStore::claim → execute → IdempotencyStore::complete. The claim is the check: it reserves the key for its caller, so a second retry that arrives before the first finishes gets Claim::InFlight rather than a second go-ahead. A handler that checked and then claimed separately would have two operations and two writers would interleave — the same read-modify-write the registry’s publish loop carries.

IdempotencyStore::abandon exists for the failed-execution case, and it is not optional bookkeeping: without it a single transient tool failure would leave the key reserved forever, and every retry of that request id would be answered InFlight with nobody to complete it.

Traits§

IdempotencyStore
A store that runs each key’s side effect at most once.

Functions§

default_capacity
How many keys a store holds when nobody has measured its workload.