🦊 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.1"
tokio = { version = "1", features = ["full"] }
anyhow = "1"
async-trait = "0.1"
Quick Start
use foxtive_supervisor::{SupervisedTask, spawn_supervised};
struct MyWorker;
#[async_trait::async_trait]
impl SupervisedTask for MyWorker {
fn name(&self) -> String {
"my-worker".to_string()
}
async fn run(&self) -> anyhow::Result<()> {
process_messages().await?;
Ok(())
}
}
#[tokio::main]
async fn main() {
let handle = spawn_supervised(MyWorker);
handle.await.unwrap();
}
That's it. Your task will automatically restart if it fails.
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::Constant(Duration::from_secs(5))
BackoffStrategy::Exponential {
initial: Duration::from_secs(1),
max: Duration::from_secs(60),
}
}
}
Lifecycle Hooks
Get notified about what's happening:
#[async_trait::async_trait]
impl SupervisedTask for MyTask {
async fn setup(&self) -> anyhow::Result<()> {
println!("Setting up connections...");
Ok(())
}
async fn cleanup(&self) {
println!("Closing connections...");
}
async fn on_restart(&self, attempt: usize) {
println!("Restarting (attempt #{})", attempt);
}
async fn on_error(&self, error: &str, attempt: usize) {
eprintln!("Task failed: {} (attempt #{})", error, attempt);
}
async fn on_panic(&self, msg: &str, attempt: usize) {
eprintln!("Task panicked: {} (attempt #{})", msg, attempt);
}
async fn should_restart(&self, attempt: usize, error: &str) -> bool {
!error.contains("Unauthorized")
}
async fn on_shutdown(&self) {
println!("Shutting down gracefully...");
}
}
Managing Multiple Tasks
Use TaskRuntime to supervise multiple tasks:
use foxtive_supervisor::TaskRuntime;
let mut runtime = TaskRuntime::new();
runtime
.register(DatabaseWorker)
.register(ApiServer)
.register(BackgroundJob);
runtime.start_all().await?;
let result = runtime.wait_any().await;
println!("Task '{}' stopped: {:?}", result.task_name, result.final_status);
let results = runtime.wait_all().await;
Graceful Shutdown
Handle SIGTERM/SIGINT properly:
use tokio::signal;
let mut runtime = TaskRuntime::new();
runtime.register(MyTask);
runtime.start_all().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 KafkaConsumer {
topic: String,
}
#[async_trait::async_trait]
impl SupervisedTask for KafkaConsumer {
fn name(&self) -> String {
format!("kafka-consumer-{}", self.topic)
}
fn restart_policy(&self) -> RestartPolicy {
RestartPolicy::Always }
fn backoff_strategy(&self) -> BackoffStrategy {
BackoffStrategy::Exponential {
initial: Duration::from_secs(1),
max: Duration::from_secs(30),
}
}
async fn setup(&self) -> anyhow::Result<()> {
Ok(())
}
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 = attempt,
error = %error,
"Consumer failed"
);
}
async fn cleanup(&self) {
}
}
Supervision Results
Every task returns a SupervisionResult:
pub struct SupervisionResult {
pub task_name: String,
pub total_attempts: usize, pub final_status: SupervisionStatus,
}
pub enum SupervisionStatus {
CompletedNormally, MaxAttemptsReached, RestartPrevented, SetupFailed, 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.