Expand description
Running a peer-observable side effect at most once per key.
§The problem this solves
On the Streamable HTTP transport a client may re-send a request whose
response never reached it — the connection dropped between the server
running the tool and the client reading the answer. The client cannot tell
that apart from the server never having run it, so it retries, and the tool
runs twice. For a tools/call that means paying for the same thing twice,
sending the same message twice, or writing the same row twice.
The ordering is therefore not negotiable, and it is the claim that has to be first:
- claim the key — atomically, so exactly one caller owns it;
- execute, only if the claim came back as yours;
- record the result under the same key.
Claiming and checking are the same operation here on purpose. A check that does not also claim leaves a window between the two, and in that window a second retry reads the same absence and runs the tool as well. That is the same read-modify-write hazard the registry’s compare-and-swap publish carries, and it is closed here by a single lock in the adapter rather than by an ordering argument in the handler.
§Why the key is the request id
Because that is the only thing a retrying client reliably repeats. JSON-RPC
requires a request id to be unique among in-flight requests, and a client
retrying an unanswered request reuses the id it never got a response for.
So the id is the idempotency key for a dropped connection, and this type
derives from RequestId rather than asking the peer for a second header
it would have no way to invent.
§Why a u64 generation rides along with the claim
Because a claim can be abandoned. If execution fails, the key has to return
to absent so a retry can run it — and if the first attempt is merely slow
rather than failed, it may come back afterwards and record a result for a
key the second attempt now owns. Two writers, one slot, and the second
result is the one the retrying client should see. A token carrying the
generation it was issued under turns that into a typed StaleClaim
instead of a silent overwrite.
§Why the state machine is a domain type
It is the transition table and nothing else: a map, three operations and one
bounded-resource rule. It has no lock, no I/O and no clock, so it can be
tested as a state machine. What it cannot do is make the claim atomic
against another thread, because a &mut self method is only exclusive
within one caller. That is the lock’s job, and the lock lives in the
adapter: see crate::application::idempotency.
Structs§
- Claim
Token - Proof that one caller, and only that caller, owns a key right now.
- Completed
Results - The transition table, with no lock and no I/O.
- Idempotency
Key - The key a side effect is deduplicated by.
- Stale
Claim - A claim that is no longer the current holder of its key.
Enums§
- Claim
- The three things a claim can find, and the three responses a caller has.
- Idempotency
Error - Why a claim could not be taken.