Skip to content
Taskvisor 0.8source v0.8.3

Final outcomes and lifecycle events

Choose the result path

Taskvisor has two result paths with different contracts:

PathContractUse it for
TaskWaiter and TaskOutcomeOne direct final result, outside the event bus.Application decisions.
Subscribe and EventBest-effort bounded delivery.Logs, metrics, traces, and live status.

A watched outcome is independent of event loss while the process and runtime remain alive. It is not durable storage. TaskWaiter::wait can return OutcomeUnavailable if its completion channel closes unexpectedly.

Separate API errors from task outcomes

An API error means that the current call did not return its documented success value. It does not by itself prove that no command or state transition was committed; the contract of the specific method defines that boundary. A TaskOutcome reports how watched work finally ended.

text
add_and_watch(spec).await
├── Err(RuntimeError)       no watched registration was returned
└── Ok((_id, waiter))
         └── waiter.wait().await
              ├── Err(RuntimeError::OutcomeUnavailable)  result channel failed
              └── Ok(TaskOutcome)                        work reached a final state
BoundaryType
Checked configuration constructors and settersConfigError
BackoffPolicy::newBackoffError
SupervisorBuilder::try_buildBuildError
Runtime lifecycle, management, wait, and shutdownRuntimeError
Controller preparation and command intakeControllerError
One task attemptTaskError
Final result of watched workTaskOutcome
Application code that combines runtime and controller callsError

SupervisorBuilder::build and Supervisor::new panic when checked construction would fail. Use try_build when the application must report or recover from construction failure.

Ok(TaskOutcome::Failed { .. }) means outcome delivery succeeded and the task ended in failure. It is not an API error. For controller work, submit_and_watch().await? confirms command intake; the waiter can later deliver TaskOutcome::Rejected after slot or registry admission fails.

Handle final outcomes

Final outcomes distinguish:

OutcomeMeaning
CompletedThe final attempt succeeded and the restart policy stopped the task.
FailedRetryable failure stopped under policy or retry limit.
FatalThe task reported a permanent failure.
CanceledCancellation was requested or reported.
ForceAbortedTaskvisor stopped waiting before cooperative termination completed.
PanickedThe actor or protected attempt-owned cleanup panicked before terminal outcome delivery.
RejectedAdmission rejected the work, or queued controller work was removed.

Use stable outcome and rejection kinds for branching, metrics, and alerts. Treat reason strings as diagnostic text. A panic while polling task code becomes a retryable task failure instead of Panicked. Removing watched controller work before it runs produces Rejected with RejectionKind::RemovedFromQueue, not Canceled.

Taskvisor delivers the terminal outcome before deferred cleanup destroys the retained task object and physical result. That later destruction can block or panic, but it cannot revise an outcome already delivered through TaskWaiter. Destructor failures on that later path are runtime diagnostics rather than TaskOutcome::Panicked.

Branch on a final outcome

Match typed variants for application decisions and use stable labels for telemetry. Keep a fallback arm because TaskOutcome is non-exhaustive.

rust
use taskvisor::{TaskOutcome, TaskWaiter};

async fn report(waiter: TaskWaiter) -> Result<(), Box<dyn std::error::Error>> {
    let outcome = waiter.wait().await?;

    match &outcome {
        TaskOutcome::Completed => println!("completed"),
        TaskOutcome::Failed { reason, .. } => println!("failed: {reason}"),
        TaskOutcome::Fatal { reason, .. } => println!("fatal: {reason}"),
        TaskOutcome::Rejected { kind, .. } => println!("rejected: {kind:?}"),
        other => println!("ended: {}", other.as_label()),
    }

    Ok(())
}

Reason strings are diagnostic text. Do not parse them as a classification API.

Understand ForceAborted

ForceAborted normally follows the configured grace period. Last-owner fallback and signal-setup failure cleanup cannot wait for that period. The physical attempt can remain active until synchronous task code returns control to Tokio.

Treat events as observability

Choose the interface that answers the operational question:

NeedInterface
Application decisionTaskWaiter and TaskOutcome
Readable demo or small-tool logsLogWriter with the logging feature
Structured service telemetryTracingBridge with the tracing feature
Application-owned metricsA custom Subscribe implementation
Registry membershiplist
Physical attempt activityalive_snapshot
Retained values and cleanup pressureownership_snapshot
Per-key admission statecontroller_snapshot

The shared event bus and every subscriber queue are bounded. Events can be lost at the shared bus or in an individual subscriber queue. When the shared bus is full, it drops the oldest event and retains the newest one. When one subscriber queue is full, Taskvisor drops the incoming event for that subscriber. Each subscriber receives callbacks serially in its own order, but two different subscribers can run at the same time.

Subscriber callbacks are synchronous and run outside Tokio worker threads. Keep them short. Forward async or long blocking work to an application-owned queue. Overflow and shutdown deadlines can drop events; overflow diagnostics report loss when possible.

See outcomes.rs, custom_subscriber.rs, logging.rs (requires logging), tracing.rs (requires tracing), and metrics.rs.

Open-source task execution components.