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:
SmolScheduleris a compact general-purpose scheduler backed byasync_executor.FlowSchedulershows 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.