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 default SmolScheduler; write a scheduler only when you are experimenting with placement, latency, or executor integration.

The scheduler trait is:

pub trait Scheduler: Clone + Send + 'static {
    #[cfg(not(target_arch = "wasm32"))]
    fn run_domain(
        &self,
        blocks: Vec<Box<dyn Block>>,
        main_channel: &Sender<FlowgraphMessage>,
    ) -> Vec<Task<(BlockId, Box<dyn Block>)>>;

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

run_domain() receives the normal send-capable blocks in a flowgraph. It must spawn each block, call block.run(main_channel).await, and return task handles that yield (BlockId, Box<dyn Block>). The runtime waits for those tasks and restores the finished block objects into the returned flowgraph.

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

Normal vs Local Work

Schedulers handle only the normal scheduling domain. The runtime manages local domains separately for:

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

That separation lets scheduler implementations assume that run_domain() receives send-capable block tasks. Blocking or thread-affine work should be placed in a local domain instead of being hidden inside the normal scheduler.

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 run_domain() that spawns every block and returns its task,
  • 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.