🦊 Foxtive Supervisor
Because tasks crash. We help them get back up.
A Rust supervision library that keeps your async tasks running, even when things go wrong. Think of it as a caring parent for your background jobs.
Why?
In production, things fail:
- Network hiccups
- Temporary resource exhaustion
- Race conditions you didn't catch in testing
- That one edge case from 3am
Foxtive Supervisor automatically restarts your tasks with configurable policies, handles panics gracefully, and gives you hooks to observe what's happening.
Installation
[dependencies]
foxtive-supervisor = "0.2"
tokio = { version = "1", features = ["full"] }
anyhow = "1"
async-trait = "0.1"
Quick Start
use foxtive_supervisor::{Supervisor, SupervisedTask};
struct MyWorker;
#[async_trait::async_trait]
impl SupervisedTask for MyWorker {
fn id(&self) -> &'static str {
"my-worker"
}
async fn run(&self) -> anyhow::Result<()> {
process_messages().await?;
Ok(())
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
Supervisor::new()
.add(MyWorker)
.start_and_wait_any()
.await?;
Ok(())
}
That's it. Your task will automatically restart if it fails.
Task Identity
Every task declares a unique id. This is used for logging, monitoring, and dependency resolution:
impl SupervisedTask for MyTask {
fn id(&self) -> &'static str {
"my-task" }
fn name(&self) -> String {
"My Task (friendly name)".to_string()
}
}
Task Dependencies
Declare which tasks must complete their setup() phase before yours starts:
impl SupervisedTask for KafkaConsumer {
fn id(&self) -> &'static str {
"kafka-consumer"
}
fn dependencies(&self) -> &'static [&'static str] {
&["database", "redis"] }
async fn run(&self) -> anyhow::Result<()> {
Ok(())
}
}
The supervisor validates the dependency graph at startup — unknown IDs and circular dependencies are caught immediately with a clear error before any task spawns.
Prerequisites
Need to wait for something external before any task starts? Use prerequisites:
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel();
Supervisor::new()
.require("http-server-bound", async move {
ready_rx.await.map_err(|_| anyhow::anyhow!("server never signalled ready"))
})
.add(MyConsumer)
.start_and_wait_any()
.await?;
Prerequisites run sequentially before any task is spawned. If one fails, startup is aborted immediately.
Restart Policies
Control when tasks restart:
impl SupervisedTask for MyTask {
fn restart_policy(&self) -> RestartPolicy {
RestartPolicy::Never RestartPolicy::MaxAttempts(5) RestartPolicy::Always }
}
Backoff Strategies
Control how long to wait between restarts:
use std::time::Duration;
impl SupervisedTask for MyTask {
fn backoff_strategy(&self) -> BackoffStrategy {
BackoffStrategy::fixed(Duration::from_secs(5)) BackoffStrategy::exponential() BackoffStrategy::linear() BackoffStrategy::fibonacci_with_default() BackoffStrategy::custom(|attempt| Duration::from_secs(attempt as u64 * 3))
}
}
Lifecycle Hooks
Get notified about what's happening:
#[async_trait::async_trait]
impl SupervisedTask for MyTask {
async fn setup(&self) -> anyhow::Result<()> {
Ok(())
}
async fn cleanup(&self) {
}
async fn on_restart(&self, attempt: usize) {
}
async fn on_error(&self, error: &str, attempt: usize) {
}
async fn on_panic(&self, msg: &str, attempt: usize) {
}
async fn should_restart(&self, attempt: usize, error: &str) -> bool {
!error.contains("Unauthorized") }
async fn on_shutdown(&self) {
}
}
Managing Multiple Tasks
Use the Supervisor builder or TaskRuntime directly:
Supervisor::new()
.add(DatabaseWorker)
.add(ApiServer)
.add(BackgroundJob)
.start_and_wait_any()
.await?;
Supervisor::new()
.add_boxed(Box::new(TaskA::new()))
.add_boxed(Box::new(TaskB::new()))
.start_and_wait_all()
.await?;
let mut runtime = TaskRuntime::new();
runtime.register(DatabaseWorker);
runtime.register(ApiServer);
runtime.start_all().await?;
let result = runtime.wait_any().await;
println!("Task '{}' stopped: {:?}", result.task_name, result.final_status);
Graceful Shutdown
Handle SIGTERM/SIGINT properly:
use tokio::signal;
let mut runtime = Supervisor::new()
.add(MyTask)
.start()
.await?;
tokio::select! {
_ = signal::ctrl_c() => {
println!("Shutting down...");
runtime.shutdown().await;
}
result = runtime.wait_any() => {
println!("Task terminated: {:?}", result);
}
}
Real-World Example
use foxtive_supervisor::*;
use std::time::Duration;
struct DatabasePool;
struct KafkaConsumer { topic: String }
#[async_trait::async_trait]
impl SupervisedTask for DatabasePool {
fn id(&self) -> &'static str { "database" }
async fn setup(&self) -> anyhow::Result<()> {
Ok(())
}
async fn run(&self) -> anyhow::Result<()> {
Ok(())
}
}
#[async_trait::async_trait]
impl SupervisedTask for KafkaConsumer {
fn id(&self) -> &'static str { "kafka-consumer" }
fn dependencies(&self) -> &'static [&'static str] {
&["database"]
}
fn restart_policy(&self) -> RestartPolicy { RestartPolicy::Always }
fn backoff_strategy(&self) -> BackoffStrategy {
BackoffStrategy::exponential_custom(Duration::from_secs(1), Duration::from_secs(30))
}
async fn run(&self) -> anyhow::Result<()> {
loop {
let message = fetch_message(&self.topic).await?;
process(message).await?;
}
}
async fn on_error(&self, error: &str, attempt: usize) {
tracing::error!(topic = %self.topic, attempt, error, "Consumer failed");
}
async fn cleanup(&self) {
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
Supervisor::new()
.add(DatabasePool)
.add(KafkaConsumer { topic: "orders".into() })
.start_and_wait_any()
.await?;
Ok(())
}
Error Handling
The supervisor provides structured error handling through the SupervisorError enum:
use foxtive_supervisor::{Supervisor, SupervisorError};
async fn start_supervisor() -> Result<(), SupervisorError> {
let mut supervisor = Supervisor::new();
supervisor.start().await?;
Ok(())
}
match start_supervisor().await {
Err(SupervisorError::DependencyValidation { task_id, dependency_id, reason }) => {
eprintln!("Task '{}' has invalid dependency '{}': {:?}", task_id, dependency_id, reason);
}
Err(SupervisorError::CircularDependency { task_id, dependency_id }) => {
eprintln!("Circular dependency detected: '{}' -> '{}'", task_id, dependency_id);
}
Err(e) => {
eprintln!("Supervisor failed: {}", e);
}
Ok(()) => println!("Supervisor started successfully"),
}
Error Categories
- Configuration Errors:
DependencyValidation, CircularDependency, InvalidConfiguration
- Runtime Errors:
PrerequisiteFailed, SetupFailed, DependencySetupFailed
- Execution Errors:
TaskExecutionFailed, TaskPanicked, MaxAttemptsReached, RestartPrevented
- System Errors:
RuntimeFailure, InternalError
Supervision Results
Every task returns a SupervisionResult:
pub struct SupervisionResult {
pub task_name: String,
pub id: String,
pub total_attempts: usize,
pub final_status: SupervisionStatus,
}
pub enum SupervisionStatus {
CompletedNormally, MaxAttemptsReached, RestartPrevented, SetupFailed, DependencyFailed, ManuallyStopped, }
When to Use This
✅ Great for:
- Background workers that should keep running
- Message consumers (Kafka, RabbitMQ, etc.)
- Database connection pools
- Health check loops
- Webhook processors
- Any "run forever" service
❌ Not for:
- HTTP request handlers (use your web framework's middleware)
- One-off scripts
- Tasks that should fail fast
Philosophy
We believe error handling should be:
- Explicit: You decide the restart policy
- Observable: Hooks show you what's happening
- Flexible: Customize behavior per task
- Forgiving: Panics don't crash your app
Tasks fail. That's okay. We've got your back.
License
MIT
Built with 🦊 by the Foxtive team. Made for humans who write Rust.