Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom Schedulers

Schedulers execute normal flowgraph block tasks and async tasks spawned through the runtime. Most applications should use Runtime::new() and the platform default scheduler; write a scheduler only when you are experimenting with placement, latency, or executor integration.

The scheduler trait is:

pub trait Scheduler: Clone + 'static
where
    #[cfg(not(target_arch = "wasm32"))]
    Self: Send,
{
    fn start_normal_domain(&self, spec: NormalDomainSpec) -> Result<NormalRunningDomain>;

    fn spawn<T: Send + 'static>(
        &self,
        future: impl Future<Output = T> + Send + 'static,
    ) -> Task<T>;
}

start_normal_domain() receives the normal send-capable blocks and domain topology. It usually takes each runnable block from the specification, spawns block.run(), and returns task and stop-handle pairs through NormalRunningDomain. Each RunnableBlock already owns the flowgraph channel it needs. The runtime waits for the tasks and restores the finished block objects into the returned flowgraph.

The implementation support types in this signature, including NormalDomainSpec, NormalRunningDomain, and RunnableBlock, are available under futuresdr::runtime::scheduler::dev.

Local domains are not started by the normal scheduler. The runtime activates the already-created local-domain thread or worker directly, constructs the flowgraph’s local scheduler type with Default inside that domain, and lets it orchestrate non-Send block tasks through LocalScheduler::run_local_domain().

spawn() runs general sendable async tasks on the scheduler. Runtime::spawn(), Runtime::spawn_background(), and control-plane internals use this method.

Normal vs Local Work

Schedulers manage the implicit normal domain, which contains send-capable block tasks. Local domains are created by the flowgraph for:

  • blocks added through Flowgraph::with_local_domain(),
  • blocks marked with #[blocking].

Blocking or thread-affine work should be placed in a local domain instead of being hidden inside the normal scheduler. A local domain can select a local scheduler type with fg.local_domain_with_scheduler::<MyLocalScheduler>(); fg.local_domain() uses the built-in basic local scheduler.

Custom local schedulers implement LocalScheduler. The low-level run() hook drives the local non-Send executor. Most implementations only customize spawn() and run() and inherit the default run_local_domain() implementation. Schedulers that need a different run policy can override it and use the LocalDomainSpec primitives from futuresdr::runtime::scheduler::dev to inspect topology, take runnable local blocks, handle domain events, stop blocks, and restore stopped block state.

Starting Point

Use the existing schedulers as templates:

  • SmolScheduler is a compact general-purpose scheduler backed by async_executor.
  • FlowScheduler shows deterministic block placement onto worker-local queues.

A minimal native scheduler usually needs:

  • a clonable handle to an executor,
  • worker thread lifecycle management,
  • an implementation of start_normal_domain() that spawns every normal block and returns its tasks,
  • an implementation of spawn() for unrelated async tasks.

Selecting a Scheduler

Construct the runtime with your scheduler:

use futuresdr::prelude::*;

let scheduler = MyScheduler::new();
let rt = Runtime::with_scheduler(scheduler);

let fg = Flowgraph::new();
rt.run(fg)?;

Custom schedulers should preserve the runtime contract: every spawned block task must eventually return its block object, even if the block exits because the flowgraph was stopped. If a worker thread panics, treat it as a runtime failure rather than silently dropping block state.