Execution Classes
The Quick Start deliberately leaves four things out of main(). This page is where they
come back.
Everything a HORUS program needs is three ideas: a node with a tick(), a topic
it sends and receives on, and the scheduler that runs the nodes. Add nodes to a
scheduler, call run(), and every node ticks best-effort at 100 Hz. That program works.
What it does not have is a timing policy — a statement of which node runs first, how often, and what should happen when one takes too long. That is what an execution class is, and it is the second thing to learn, not the first.
let mut sched = Scheduler::new().tick_rate(1000_u64.hz());
sched.add(Sensor::new()?).order(0).build()?;
sched.add(Controller::new()?).order(1).rate(1000_u64.hz()).on_miss(Miss::SafeMode).build()?;
sched.run()
Delete tick_rate, order, rate and on_miss from that and it still runs. Each one
answers a different question.
Four questions, four builder calls
How fast does the scheduler itself run? .tick_rate(freq), on the scheduler.
The default is 100 Hz. It is the clock everything else is measured against.
In what order do nodes run within one tick? .order(n), lower first, default 0 — but
only as a fallback. As soon as any node in the program has topic metadata (any send() or
recv() the scheduler has seen), it builds a dependency graph from that pub/sub metadata
and dispatches from the graph: a subscriber runs after its publisher even when it has the
lower order, and two nodes with no dependency between them run in the same step whatever
their order values say. .order() tiers decide the sequence only when there is no topic
metadata to build a graph from. .deterministic(true) makes the graph's dispatch strictly
sequential and reproducible — it does not hand ordering back to .order().
Does this node need its own clock? .rate(freq) gives a node a tick rate
independent of the scheduler's, and — this is the part worth knowing — it also moves the
node into the real-time class, onto its own thread. A 1 kHz control loop next to a 10 Hz
logger is two .rate() calls, not two schedulers.
What happens when it overruns? .on_miss(policy) — warn, skip the tick, drop to a
safe state, or stop. A node with no deadline cannot miss one, which is why this only
appears once a rate, budget or deadline does.
Choosing a class
Every node ends up in exactly one class, and you pick it by which builder method you call rather than by naming the class:
- Best-effort — the default, when you call none of them. Ticked on the main loop. Right for anything where "roughly this often" is good enough: logging, telemetry, diagnostics.
- Real-time — entered implicitly by
.rate(),.budget()or.deadline(). Its own thread, bounded latency. Right for control loops, safety monitors, sensor fusion. - Compute —
.compute(), a worker pool. Right for work that can take longer than one tick without holding anything else up: planning, SLAM, ML inference. - Event-driven —
.on(topic), ticks only when that topic receives data. Right for reactive behaviour that would otherwise poll. - Async I/O —
.async_io(), an async executor. Right for network, disk and cloud calls that must never block a control loop.
There is no .rt() method to call; the real-time class has no name in the API because
you enter it by stating a timing requirement, not by asking for it.
When to reach for this
Not while your first program is being written. The point of the default is that a program with no timing policy is a valid program — it just has no guarantees.
Reach for a class the first time one of these is true:
- Two nodes with no topic between them have to run in a fixed order within a tick →
.order() - One node must run faster than the rest →
.rate() - Something must happen when a node is late, rather than nothing →
.on_miss() - A slow node is delaying a fast one →
.compute()or.async_io()
Going deeper
This page is the shape of the idea. The full builder API — every method, every default, what combinations are rejected, and the sequential/parallel execution modes — is in Scheduler Configuration. The real-time thread, priority and CPU-affinity settings are in Real-Time Configuration, and what "safe state" means per node is in Safety Monitor.
- Scheduler — what the scheduler is, before policy
- Real-Time Nodes — the RT class in depth
- Execution Modes — deterministic versus parallel dispatch