Skip to main content
Deterministic execution starts with deterministic scheduling. If your middleware can’t guarantee when nodes run and in what order, then replaying a log won’t reproduce the same behavior — and debugging production failures becomes guesswork. Cerulion’s scheduler controls exactly when each node executes, based on explicit triggers rather than OS thread scheduling. This is what makes deterministic resim possible.
ROS2’s scheduling problem: ROS2 relies on OS thread scheduling and callback queues. The order in which callbacks fire depends on system load, thread priorities, and timing jitter. Two identical runs of the same graph can produce different execution orders — and different results.

Trigger Model

Every node in Cerulion has an explicit trigger that determines when it runs. There are no implicit callback queues or racing threads. The scheduler evaluates triggers deterministically — given the same inputs, nodes always execute in the same order.

Trigger Types

Periodic triggers

Nodes run on a fixed interval. Ideal for sensors and data sources that produce at a known rate.
#[node(period_ms = 33)]  // 30 Hz
pub fn camera() -> (#[out("frame")] Image) {
    (capture_frame())
}
The scheduler guarantees the period is respected regardless of system load. If a node overruns its period, the scheduler logs the overrun rather than silently skipping or double-firing.
Nodes run when a specific input receives new data. The trigger names the input explicitly — no ambiguity about which message caused execution.
#[node(trigger = "frame")]
pub fn detector(frame: &Image) -> (#[out("detections")] DetectionList) {
    (run_detection(frame))
}
When multiple inputs have data, the scheduler processes triggers in a deterministic order based on the graph topology, not arrival time.
Nodes wait for multiple inputs to arrive within specified time windows. Essential for sensor fusion where timing alignment matters.
#[node(sync = {"left": 50, "right": 50})]
pub fn stereo(left: &Image, right: &Image) -> (#[out("depth")] DepthMap) {
    (compute_stereo(left, right))
}
The time windows (in milliseconds) define how close inputs must arrive to be considered synchronized. If inputs don’t align within the window, the scheduler waits rather than processing mismatched data.
Nodes run when a custom function returns true. For hardware interrupts, button presses, or any event outside the data flow.
#[node(external_trigger = "should_capture")]
pub fn triggered_capture() -> (#[out("image")] Image) {
    (capture_high_res())
}

Why This Enables Determinism

The scheduler’s explicit trigger model means execution order is a function of the graph topology and input data — not of OS thread scheduling, system load, or timing jitter. Given the same graph and the same input sequence:
  • The same nodes fire in the same order
  • Synchronized triggers align on the same input pairs
  • Periodic nodes maintain their phase relationships
This is what makes deterministic resim possible. The scheduler is the foundation — zero-copy logging preserves the inputs, and the deterministic scheduler reproduces the execution.
For the complete API reference on trigger patterns, including code examples for each trigger type, see Publisher & Subscriber.

Next Steps

Determinism

How scheduling, logging, and runtime combine for deterministic execution

Pub/Sub Patterns

Complete API reference for all trigger patterns