Call Chains
A request rarely ends at the first operation it reaches. That operation calls another, whose commit publishes an event, whose receiver invokes a third. The whole path is a chain: every operation and event a single originating request passed through.
The runtime carries the chain with each call and each event. Operations do not write it and do not read it. Having it lets the runtime enforce two properties of an application that no single component can check for itself.
§Cycles
Components are connected by declarations spread across manifests. A receiver in billing listens to orders; a receiver in orders listens to billing. Each declaration is reasonable
alone. Together they can form a circle:
orders.place → ~orders.updated → billing.charge → ~billing.updated → orders.place → … Here ~ marks an event. An order is placed, which updates it, which charges it, which updates
the charge, which places the order again. Nothing stops such a circle from the inside. It
consumes resources until something fails, and the symptom appears far from the cause.
Because a chain records where a call has already been, the runtime can see the circle close. A call that would enter the same place repeatedly on one chain, or a chain that has grown implausibly long, is refused before the operation runs. Nothing is read, committed, or published by the refused call.
The refusal is an exception, and it names the chain that led to it. Where somebody is waiting, it reaches them. Where nobody is — the usual case, since circles are mostly closed by events — the message is set aside at once instead of being retried: a circle is the same circle on the next attempt.
Two clarifications define what counts.
A chain is one path, not a period of time. An operation that calls the same endpoint fifty times makes fifty short chains, not a cycle. Work that repeats on a schedule starts a new chain each time.
Intentional re-entry is declared. Some operations are meant to be re-entered: a walk over a tree, a job that arranges its own next run. Such an operation states that every call to it begins a chain of its own. The limit no longer applies to it, and stopping becomes the operation’s own responsibility.
§Readonly chains
An observation or a computation is safe: it cannot change state. That would mean little if it could call something that does. An observation that calls another component’s transition changes state, however innocent it looks.
So a safe operation begins a readonly chain. The mark travels with every call made under it, to any depth, and because operations are safe or unsafe by type, the runtime knows before making a call whether it is permitted:
| Called under a readonly chain | |
|---|---|
| Observation, computation | Proceeds. |
| Transition, assignment, effect | Refused. |
The refusal happens where the call is made, before it is sent. Nothing is read, committed, published, or queued. A component cannot remove the mark for the calls it makes itself. Scheduling a call to be made later stores something, so that is refused as well, whatever the scheduled call would have been.
An operation that needs to call something unsafe is declared an effect.
Events are never part of a readonly chain: an event is produced by a state change, and a readonly chain makes none.
§Reading methods of the gateway
Some requests are supposed to have no effect. An HTTP GET is the standard example: clients,
caches, and crawlers repeat such requests freely, on the understanding that doing so changes
nothing.
A request that enters through the gateway by a reading method therefore begins a readonly chain as well, whatever operation it reaches.
A route can state the opposite, that its reading method is in fact allowed to change state. That is a declaration on the route, visible where the route is defined, and not something an operation deep in the chain can decide.
§What a chain is not
Not a security boundary. Both mechanisms catch an application wired incorrectly by accident. Neither defends against a participant that deliberately sends something else.
Not a view beyond the runtime. A chain covers calls between operations and the events between them. What an operation does through other aspects of the context — a request to an external system, a lock — is not a call to an operation, and a readonly mark does not restrain it.
§The common idea
Cycle detection and readonly chains are different rules built on the same observation. Some properties belong to a whole path through the application rather than to any component on it. A component sees only its own operation and the call it is about to make. The runtime, carrying the chain, sees the path, and can hold the application to a property that would otherwise have to be maintained by convention across every team that contributes a component.