Skip to main content

Module idempotency

Module idempotency 

Source
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:

  1. claim the key — atomically, so exactly one caller owns it;
  2. execute, only if the claim came back as yours;
  3. 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§

ClaimToken
Proof that one caller, and only that caller, owns a key right now.
CompletedResults
The transition table, with no lock and no I/O.
IdempotencyKey
The key a side effect is deduplicated by.
StaleClaim
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.
IdempotencyError
Why a claim could not be taken.