Skip to content
Taskvisor 0.9source v0.9.0

Run Taskvisor ​

Choose an entry point ​

Choose an entry point based on how tasks are supplied and who requests shutdown:

Entry pointUse it when
Supervisor::runThe initial batch finishes naturally.
Supervisor::run_untilThe application owns the future that requests shutdown.
Supervisor::run_with_os_signalsTaskvisor should install process signal handlers.
Supervisor::serveWork is discovered or managed while the service is running.

Construct and start one runtime ​

Use Supervisor::new for runtime configuration and subscribers. Use Supervisor::builder for task defaults or a controller. The builder's try_build returns typed construction errors. Panics from subscriber metadata methods (execution, name, or queue_capacity) still reach the caller.

Construction does not start Tokio tasks. serve or a static run starts the runtime. First startup needs an active Tokio runtime. Repeated serve calls return handles to the same runtime; workers start only once. Shutdown is terminal. Calling serve again does not restart workers or reopen admission. See the supervisor entry points and startup code.

Choose the application Tokio runtime ​

Taskvisor uses the active application runtime; it does not choose a Tokio runtime flavor for the application. Choose from the workload and deployment constraints, not from task duration alone:

Application boundaryStarting point
The process intentionally uses one Tokio worker, and measured management concurrency and task latency fitcurrent_thread
Multiple independent futures need to be runnable on Tokio workers concurrentlymulti_thread
Task code performs heavy CPU work or blocking callsNeither runtime flavor fixes the task; use a suitable CPU or blocking pool and await its result from the supervised task
The production workload is mixed or its ready-future pattern is not yet knownMeasure the real workload on both runtime flavors

For example, an intentionally single-threaded CLI with non-blocking timers or queue receive loops can start with:

rust
#[tokio::main(flavor = "current_thread")]
async fn main() {
    // Construct and run Taskvisor here.
}

A service that needs several Tokio workers to run independent ready futures can start with:

rust
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() {
    // Construct and run Taskvisor here.
}

Four workers match the benchmark fixture below; they are not a general worker-count recommendation. Taskvisor can also own native cleanup and subscriber threads, so current_thread describes Tokio only.

Two matched benchmark boundaries show why task duration alone is not a runtime-selection rule:

  • Root-driven serialized lifecycle. For 256 instant watched tasks, from the first serialized root-caller admission through all outcomes, the Taskvisor 0.9.0 Linux/aarch64 reference run measured 95.661 K tasks/s on current-thread and 55.467 K tasks/s on multi-thread with four workers. Current-thread was faster for this boundary.
  • Pre-admitted cooperative CPU drain. For 64 already-admitted tasks, each running 16 independent CPU chunks of 4,096 steps and yielding after every chunk, the same run measured 12.446 K tasks/s on current-thread and 29.945 K tasks/s on multi-thread with four workers. Multi-thread was 2.406× faster for this boundary.

The first result does not establish that short tasks are generally faster on current-thread. It includes serialized caller-to-registry round trips and contains no parallel task work. The second result excludes admission and includes user CPU work, so 2.406× is the speedup of that complete synthetic drain boundary, not a measurement of Taskvisor overhead alone. See the throughput and dynamic-management benchmark contracts before comparing results. Keep heavy CPU work off Tokio; cpu_job.rs shows a supervised bridge to Rayon.

Run resident work under application-owned shutdown ​

This flow starts one resident worker and uses an application-owned future to request shutdown. It joins the shared shutdown workflow before returning. Replace the timer with the surrounding server's shutdown future.

rust
use std::time::Duration;
use taskvisor::prelude::*;

async fn application_shutdown() {
    tokio::time::sleep(Duration::from_millis(50)).await;
}

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let worker: TaskRef = TaskFn::arc(|ctx| async move {
        loop {
            ctx.run_until_cancelled(tokio::time::sleep(Duration::from_secs(1)))
                .await?;
            // Poll or process one cancellation-safe unit of work.
        }
    });

    let supervisor = Supervisor::new(SupervisorConfig::default(), vec![]);
    supervisor
        .run_until(
            vec![TaskSpec::restartable("worker", worker)],
            application_shutdown(),
        )
        .await?;

    Ok(())
}

After the initial batch is admitted, run_until waits for any of these conditions:

  • the registry becomes empty;
  • the application shutdown future resolves;
  • another caller starts shared shutdown.

It then joins shared shutdown. When the application future wins, shutdown requests cooperative task cancellation. The method can return before the application future resolves if all registered tasks finish first. The static-run code owns these races.

Ok(()) reports completion of the supervisor lifecycle, not the worker's outcome. The wrapped Tokio sleep is safe to stop by dropping. A real receive, commit, or acknowledgement operation needs its own cancellation-safety review.

Use serve with add(spec).watch().execute() when work arrives after startup or application logic needs each task's final outcome.

Understand the static lifecycle ​

run, run_until, and run_with_os_signals submit one initial batch through all-or-nothing registry admission. The registry accepts every task in that batch or rejects the full batch. After successful admission, all three methods start natural shutdown when the entire registry becomes empty. Tasks already added through serve also keep it non-empty.

The three methods share one static lifecycle. Its commit is separate from registry admission. After a call commits, another static run on the same supervisor returns RuntimeError::AlreadyRunning. Errors before the lifecycle commit leave it available for another static run. A registry rejection consumes the lifecycle, but does not stop tasks added earlier through serve.

run_until can begin shutdown before the batch reaches the registry. run_with_os_signals can enter cleanup before that point if signal-listener setup fails. An Ok(()) return means the bounded supervisor lifecycle and shared cleanup workflow completed. It does not mean every task succeeded or every retained user value has been destroyed.

Dropping a static run future after its lifecycle commits does not stop admitted tasks or start shutdown. A handle returned by serve can still request shutdown.

Own signal handling and shutdown ​

run and run_until do not install operating-system signal handlers. run_with_os_signals is the explicit process-wide opt-in. An embedded application that already owns signals should use run_until or request shutdown through a dynamic handle.

On Unix, dropping Taskvisor's signal listeners does not restore the default signal disposition. The application remains responsible for signal handling after the method returns.

serve does not install signal handlers. Call handle.shutdown().await for the shared shutdown result. Read Cancellation and shutdown before relying on its deadlines or return value.

Run an entry-point example ​

Runnable entry-point examples:

Open-source task execution components.