Transactional Inbox
At-least-once delivery means an operation may be asked to do the same thing twice. For operations that set a value, repetition is harmless. For operations that accumulate — charge a wallet, increment a counter, append a line — it is not.
An operation can ask the runtime to make its state change happen once per call, however many times that call arrives. The mechanism is the transactional inbox: the mirror of the transactional outbox, on the receiving side.
operations:
charge:
type: transition
concurrency: retry
once: true §The identity of a call
To recognize a repetition, the runtime has to know that two arrivals are the same call. Every call therefore has an identity, assigned when the call originates and preserved through redelivery.
The identity is not derived from the call’s content. Two charges of the same amount to the same wallet are two calls if they were made twice, and one call if a single request was delivered twice.
§What once gives
The state changes once. The record that a call was applied is committed together with the state change it caused. There is no moment at which one exists without the other, so no failure can make the change apply twice or make the runtime forget that it applied.
A retried transition does not call it. A transition that retries runs again because state
moved, and that is a second attempt, not a second delivery: its call has an identity of its own.
What promised to apply once would apply once per attempt, so a call from such a transition to an
operation that declares once is refused before it is sent. An operation such a transition
calls is made safe to repeat by what it is — keyed by something the caller passes, so that a
second arrival finds the first — and declares no once.
A repetition gets the first answer. A duplicate is not refused. It is answered with the reply the first arrival produced. The most common duplicate is a request whose reply was lost on the way back; its sender is still waiting, and what it needs is the answer, not an error about having asked before.
Chains hold together. A call made by an operation takes its identity from the call that
operation is serving. If orders.pay calls wallets.charge, and orders.pay is delivered
twice, the second run makes the same call to wallets.charge, which recognizes it. A
repetition at the start of a chain is absorbed at every step that declares once. A step that
does not declare it is a break in that protection.
§What it does not give
The promise is about committed state, and it is bounded.
The implementation may still run more than once. A duplicate that arrives while the first is in progress runs to completion before its commit is rejected. What the runtime prevents is the second write, not the second execution. Anything the operation does outside its own state happens on every run.
A call that changed nothing is not remembered. An operation that returned an error, failed, or discarded its change left no record. The same call made again runs again, and, nothing having changed, answers the same way.
It is remembered for a limited time. A call is recognized within a window that the operation or the deployment states. The window is chosen for how late a duplicate can realistically arrive, not for how long the change matters.
It holds within one deployment. Where the same application runs in several regions, a call is remembered in the region where it was made.
§Which operations can ask
once applies to operations that write a single change whose reply can be stored with it:
transitions over one entity, and assignments. An observation has nothing to protect. An effect’s
own work lies outside state, which this mechanism does not reach.
It also requires a storage that can commit the record with the entity atomically. A component
that declares once on a storage that cannot do so does not start. There is no weaker form to
fall back to: a record kept separately from the change guarantees nothing.
§Calls from outside
Inside the application, the runtime assigns identities. A client outside it — a mobile app retrying a request it never saw answered — is making what looks like a new call each time. Only the client knows the two requests are one.
The gateway lets the client say so, by sending a key with the request. The key, together with who is calling and what is being called, becomes the call’s identity. A retry with the same key is the same call, and is answered with the first reply.
This extends the same guarantee to the edge of the system: from a client’s request, through the
gateway, down every once step of the chain it starts.