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§
- Idempotency
Store - 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.