Skip to content
Taskvisor 0.8source v0.8.3

Outcomes: wait for the final result

Use add_and_watch when application logic must know how one task ended. Its TaskWaiter uses a dedicated terminal channel, separate from best-effort events.

text
add_and_watch ──────► TaskId + TaskWaiter
managed lifecycle ──► terminal outcome ────► TaskWaiter
lifecycle events ───► event bus ───────────► subscribers (best-effort)
Task behaviorFinal outcome
succeedsCompleted
exhausts its retry budgetFailed
times out under onceFailed
returns a permanent errorFatal
observes cancellationCanceled

A configured timeout is retry-eligible. The timeout row is final because once forbids retry. with_max_retries(NonZeroU32::new(2).unwrap()) allows the first failed attempt and two retries before Failed. An admitted result normally arrives after registry membership is removed. Except for ForceAborted, task execution is physically joined first. ForceAborted is final logically while a physical actor may still be active. Controller rejection can resolve without starting the task. Panicked covers an actor or protected cleanup panic.

Expect one printed section for each table row. The example then shuts down and exits. TaskWaiter::wait can return an error if its terminal channel closes unexpectedly.

Run with cargo run --example outcomes.

Complete program

rust
use std::num::NonZeroU32;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;

use taskvisor::prelude::*;
use tokio::sync::Notify;

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let supervisor = Supervisor::new(SupervisorConfig::default(), vec![]);
    let handle = supervisor.serve()?;

    // 1) A one-shot job that succeeds -> Completed.
    println!("=== Completed ===");
    let job: TaskRef = TaskFn::arc(|_ctx| async {
        tokio::time::sleep(Duration::from_millis(100)).await;
        Ok(())
    });
    let (_id, waiter) = handle.add_and_watch(TaskSpec::once("import", job)).await?;
    println!("  import -> {:?}\n", waiter.wait().await?);

    // 2) A task that always fails, with a bounded retry budget -> Failed.
    //    Its reason/exit_code are identical to the typed TaskFinished event.
    println!("=== Failed (retries exhausted) ===");
    let attempts = Arc::new(AtomicU32::new(0));
    let flaky: TaskRef = TaskFn::arc(move |_ctx| {
        let attempts = Arc::clone(&attempts);
        async move {
            let n = attempts.fetch_add(1, Ordering::Relaxed) + 1;
            println!("  sync attempt #{n} failing...");
            Err(TaskError::fail("upstream 503").with_exit_code(75))
        }
    });
    let spec = TaskSpec::restartable("sync", flaky)
        .with_backoff(BackoffPolicy::constant(Duration::from_millis(20)))
        .with_max_retries(NonZeroU32::new(2).unwrap());
    match handle.add_and_watch(spec).await?.1.wait().await? {
        TaskOutcome::Failed {
            reason, exit_code, ..
        } => {
            println!("  sync -> Failed: {reason} (exit_code={exit_code:?})\n");
        }
        other => println!("  sync -> {other:?}\n"),
    }

    // 3) A one-shot task that exceeds its per-attempt deadline -> Failed.
    println!("=== Failed (attempt timed out) ===");
    let slow: TaskRef = TaskFn::arc(|_ctx| async {
        tokio::time::sleep(Duration::from_secs(1)).await;
        Ok(())
    });
    let timed = TaskSpec::once("slow-report", slow).with_timeout(Duration::from_millis(20));
    match handle.add_and_watch(timed).await?.1.wait().await? {
        TaskOutcome::Failed { reason, .. } => {
            println!("  slow-report -> Failed: {reason}\n");
        }
        other => println!("  slow-report -> {other:?}\n"),
    }

    // 4) A permanent error stops even a restartable task -> Fatal.
    println!("=== Fatal (not retryable) ===");
    let permanent: TaskRef = TaskFn::arc(|_ctx| async {
        Err::<(), TaskError>(TaskError::fatal("credentials rejected").with_exit_code(78))
    });
    let fatal = TaskSpec::restartable("credential-check", permanent);
    match handle.add_and_watch(fatal).await?.1.wait().await? {
        TaskOutcome::Fatal {
            reason, exit_code, ..
        } => {
            println!("  credential-check -> Fatal: {reason} (exit_code={exit_code:?})\n");
        }
        other => println!("  credential-check -> {other:?}\n"),
    }

    // 5) A long-running worker we cancel -> Canceled.
    println!("=== Canceled ===");
    let started = Arc::new(Notify::new());
    let worker: TaskRef = TaskFn::arc({
        let started = Arc::clone(&started);
        move |ctx| {
            let started = Arc::clone(&started);
            async move {
                started.notify_one();
                ctx.cancelled().await;
                Err(TaskError::Canceled)
            }
        }
    });
    let (id, waiter) = handle
        .add_and_watch(TaskSpec::restartable("worker", worker))
        .await?;
    // The task body, rather than a timer, confirms that the worker started.
    started.notified().await;
    println!("  cancelling worker...");
    handle.cancel(id).await?;
    println!("  worker -> {:?}\n", waiter.wait().await?);

    handle.shutdown().await?;
    Ok(())
}

Open-source task execution components.