Computation Graph
A Computation Graph is an in-process, deterministic, event-driven DAG. Unlike a Workflow (where the task is the unit and state is persisted), a computation graph runs as a single traversal in memory, in response to an event. It is built from Nodes and is fired either by a Reactor or invoked directly from a workflow task.
- A graph is a set of nodes wired by a topology (
a -> b). - Entry nodes read named sources from an
InputCache; processing nodes take their upstream node’s output; the terminal node (no downstream) produces the result. - It compiles to a callable function (
<name>_compiledin Rust) that the reactor invokes; the Boundary events an Accumulator emits are what populate the cache.
A two-node pricing graph triggered by a reactor:
The #[computation_graph] macro declares the topology and generates the compiled
function; each pub async fn in the module is a node:
#[cloacina_macros::computation_graph(
trigger = reactor("pricing_reactor"),
graph = {
ingest(orderbook) -> format_output,
}
)]
pub mod pricing {
use super::*;
pub async fn ingest(orderbook: Option<&OrderBookSnapshot>) -> SpreadSignal { /* ... */ }
pub async fn format_output(input: &SpreadSignal) -> FormattedOutput { /* ... */ }
}
// generates pricing_compiled(&InputCache) -> GraphResult
ComputationGraphBuilder is a context manager; nodes are @cloaca.node
functions inside it, and the topology is a dict:
import cloaca
@cloaca.reactor(name="pricing_reactor", accumulators=["orderbook"], mode="when_any")
class PricingReactor:
pass
with cloaca.ComputationGraphBuilder(
"pricing", reactor=PricingReactor,
graph={
"ingest": {"inputs": ["orderbook"], "next": "format_output"},
"format_output": {}, # no `next` → terminal
},
) as g:
@cloaca.node
def ingest(orderbook):
return spread_signal(orderbook)
@cloaca.node
def format_output(ingest):
return formatted(ingest)
reactor= takes a @cloaca.reactor-decorated class, and each graph entry
is a dict (inputs list, plus next / routes, or empty for terminal). See
the topology dict schema
for the full format. Python node functions receive owned values (the boundary
copies them) where Rust takes references.
- Reactor-triggered or trigger-less. A graph either subscribes to a reactor
(
trigger = reactor("…")) or is trigger-less and invoked inline from a workflow task. - Data is serialized. Values that cross node boundaries implement
Serialize/Deserialize; the cache stores bytes. - Deterministic. Given the same cache, the traversal is deterministic.
- Embed it → Embed · Tutorials
- Ship it to a server → Run the Service · Tutorials
- Node · Reactor · Accumulator · Boundary event