Stateful Operations
A call is addressed to a component. Whichever replica is free takes it, and the caller neither knows nor cares which one. This is what lets a component scale, restart, and move, and it depends on operations being stateless.
Occasionally something exists in exactly one process: an open connection to a device, a media stream being produced, a session held in memory. To reach it, a call has to arrive at that process and no other.
§Addressing a process
An operation declared stateful is served by every process of its component under that
process’s own name:
operations:
watch:
type: computation
stateful: true A process learns its name from the context and gives it to whoever will need it later, usually by storing it in state. A caller then names the process the call is for:
// in the process that holds the stream
const instance = context.instance
// in any caller
await context.remote.streams.watch({ input: { id }, instance }) The rest of the call is unchanged. It has a contract, it is validated and traced, and it is addressed by name.
§What an addressed call gives up
An ordinary call reaches a component, and a component comes back. If one replica stops, another takes the call. If none is running, the call waits.
An addressed call reaches one process. When that process is gone, no replica can answer in its place, and nothing says the process will return. The guarantees that rest on substitutability no longer apply:
It can end without an answer. An addressed call waits for a bounded time. If nothing answers, the call is abandoned, and the caller cannot tell whether it ran.
It can find nobody. A call to a name no process holds is refused outright, and did not run.
It cannot be deferred. A task or a delayed call is taken later by whichever process is free, which contradicts naming one.
| Outcome | Meaning for the caller |
|---|---|
| The reply | The process answered. |
| Nobody holds the name | The call did not run. |
| Abandoned | The wait ended; the call may have run, and may still. |
Both failures are exceptions. They are also ordinary events in the life of such a system, since processes come and go, and a caller of a stateful operation is written to expect them.
§Keeping the exception small
stateful is a declared departure from the model, like unmanaged. The manifest shows which
operations are tied to a process, and everything else in the component keeps its guarantees.
The usual design keeps durable facts in entity state and uses the stateful operation only for
the live resource. A streams component stores which process holds each stream. Its ordinary
operations create, list, and end streams, reliably. Only watch is stateful. When the process
holding a stream disappears, the stored state is what lets the rest of the system notice and
recover.
§Process memory
The same need appears on a smaller scale as memory that outlives a single call: a parsed
dictionary, a pool of connections. A component can ask for a state aspect, an object that
belongs to one process of the component and is shared by its operations there.
Such memory is a cache, not state in the sense of State. Each replica has its own, it is lost when the process stops, and nothing in it is visible to the runtime. Whatever has to survive, or be seen by another replica, belongs in an entity.