timer-lib
timer-lib is a Tokio-based timer crate for one-shot and recurring async work.
It is built around a small set of handle types:
Timer starts, pauses, resumes, stops, cancels, and joins runs.
TimerBuilder reduces setup boilerplate for common configurations.
TimerRegistry tracks timers by ID and provides bulk operations.
TimerEvents exposes lossy lifecycle broadcasts.
TimerCompletion exposes lossless completed-run delivery.
Features
- One-shot and recurring timers
- Deadline-based one-shot scheduling
- Optional initial delay for recurring timers
- Optional recurring jitter
- Pause, resume, graceful stop, and immediate cancel
- Dynamic interval adjustment for live runs
- Per-callback timeout support
- Retry policy and retry backoff support for failed callbacks
- Run outcomes and execution statistics
- Broadcast lifecycle events plus lossless completion waiting
- Labels, metadata tags, timer snapshots, and registry listing/filtering helpers
- Registry helpers for managing many timers, including bulk pause/resume
- Closure-first API with optional trait-based callbacks
- Optional
test-util feature for deterministic mocked time
Installation
[dependencies]
timer-lib = "0.4.0"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
Quick Start
use std::time::Duration;
use timer_lib::{RecurringSchedule, Timer, TimerFinishReason};
#[tokio::main]
async fn main() {
let timer = Timer::new();
timer
.start_once(Duration::from_millis(250), || async {
println!("ran once");
Ok(())
})
.await
.unwrap();
let outcome = timer.join().await.unwrap();
assert_eq!(outcome.reason, TimerFinishReason::Completed);
}
Recurring Timers
use std::time::Duration;
use timer_lib::{RecurringSchedule, Timer, TimerFinishReason};
#[tokio::main]
async fn main() {
let timer = Timer::recurring(
RecurringSchedule::new(Duration::from_secs(1)).with_expiration_count(3),
)
.start(|| async {
println!("tick");
Ok(())
})
.await
.unwrap();
let outcome = timer.join().await.unwrap();
assert_eq!(outcome.reason, TimerFinishReason::Completed);
assert_eq!(outcome.statistics.execution_count, 3);
}
Retry And Timeout Controls
use std::time::Duration;
use timer_lib::{Timer, TimerError};
#[tokio::main]
async fn main() {
let timer = Timer::once(Duration::from_secs(1))
.callback_timeout(Duration::from_secs(2))
.max_retries(1)
.start(|| async {
Ok::<(), TimerError>(())
})
.await
.unwrap();
let outcome = timer.join().await.unwrap();
println!("attempted: {}", outcome.statistics.execution_count);
}
Deadlines, Jitter, And Backoff
use std::time::Duration;
use timer_lib::{RecurringSchedule, Timer, TimerError};
use tokio::time::Instant;
#[tokio::main]
async fn main() {
let deadline = Instant::now() + Duration::from_secs(1);
let timer = Timer::at(deadline)
.start(|| async { Ok::<(), TimerError>(()) })
.await
.unwrap();
let recurring = Timer::recurring(
RecurringSchedule::new(Duration::from_secs(5))
.with_jitter(Duration::from_secs(1))
.with_expiration_count(3),
)
.max_retries(2)
.exponential_backoff(Duration::from_millis(250))
.start(|| async { Ok::<(), TimerError>(()) })
.await
.unwrap();
let _ = timer.join().await.unwrap();
let _ = recurring.join().await.unwrap();
}
Events And Completion
Use subscribe() when you want a best-effort event stream and completion() when you need to reliably observe the final outcome of a run.
use std::time::Duration;
use timer_lib::{Timer, TimerEvent};
#[tokio::main]
async fn main() {
let timer = Timer::new();
let mut events = timer.subscribe();
let mut completion = timer.completion();
let run_id = timer
.start_once(Duration::from_millis(100), || async { Ok(()) })
.await
.unwrap();
if let Some(TimerEvent::Started { run_id: seen, .. }) = events.wait_started().await {
assert_eq!(seen, run_id);
}
let outcome = completion.wait_for_run(run_id).await.unwrap();
println!("finished: {:?}", outcome.reason);
}
Managing Many Timers
use std::time::Duration;
use timer_lib::TimerRegistry;
#[tokio::main]
async fn main() {
let registry = TimerRegistry::new();
let (timer_id, timer) = registry
.start_once(Duration::from_secs(1), || async { Ok(()) })
.await
.unwrap();
assert!(registry.contains(timer_id).await);
let _ = timer.join().await.unwrap();
let completed = registry.join_all().await;
assert!(!completed.is_empty());
}
Labels And Snapshots
use std::time::Duration;
use timer_lib::Timer;
#[tokio::main]
async fn main() {
let timer = Timer::once(Duration::from_secs(1))
.label("billing")
.tag("tenant", "acme")
.start(|| async { Ok::<(), timer_lib::TimerError>(()) })
.await
.unwrap();
let snapshot = timer.snapshot().await;
assert_eq!(snapshot.metadata.label.as_deref(), Some("billing"));
}
Callback Styles
The simplest API uses closures:
# use std::time::Duration;
# use timer_lib::Timer;
# async fn demo() {
let timer = Timer::new();
let _ = timer
.start_once(Duration::from_secs(1), || async { Ok(()) })
.await;
# }
If you need a reusable callback type, implement TimerCallback:
use async_trait::async_trait;
use timer_lib::{TimerCallback, TimerError};
struct MyCallback;
#[async_trait]
impl TimerCallback for MyCallback {
async fn execute(&self) -> Result<(), TimerError> {
Ok(())
}
}
Current Scope
timer-lib currently targets Tokio runtimes. It does not provide cron scheduling or async-std support.
For deterministic test control, enable the test-util feature and use Timer::new_mocked() or TimerRegistry::new_mocked().
The next major documentation pass should live on docs.rs. For now, the most complete usage sample is examples/feature_showcase.rs.