runtime-rs
Tokio-native lifecycle and service composition for Rust applications.
runtime-rs gives your app one place to register services, boot them,
reload them, run their background tasks, resolve them by Rust type, and shut
them down gracefully:
- register typed services once
- boot / validate / reload / shutdown them in deterministic dependency order
- resolve them later by concrete Rust type
- remove the need for ad-hoc runtime injection
- spawn long-running async runnable providers
- drain accepted work through graceful shutdown gates
- publish typed in-process lifecycle events with the
eventsfeature
The core idea:
Registry<State>
├─ DbService -> Provider + Reloadable
├─ CacheService -> Provider + Runnable
└─ YourService -> Provider
Runtime<State>
└─ spawns every provider that exposes Runnable
Features
Default features are:
= ["registry"]
= ["registry", "support", "events"]
registryaddsregistry_ref()toSharedStatefor states that carryRegistry<Self>. TheRegistrytype itself is always available.eventsenablesLifecycleBus, addsevents()toSharedState, and enables thedashmapdependency.supportenablesGate,Permit,GuardGroup, andGuard.
Your State
The crate does not own your app state. Your state only needs to implement
SharedState. If you want registry.reload_all(&state).await, implement
ReloadState too.
use async_trait;
use Arc;
use CancellationToken;
use LifecycleBus;
use ;
;
The Registry
Registry<S> is the main piece. It stores Arc<T> values by TypeId, while
also treating them as lifecycle providers.
That means one object can be all of these at once:
- a concrete service you can resolve later:
registry.resolve::<DbService>() - a lifecycle participant:
boot,validate,shutdown - a hot-reload participant:
Reloadable - a long-running background task:
Runnable
use Arc;
let state = new;
state
.registry_ref
.insert
.insert;
state.registry_ref.boot_all.await?;
state.registry_ref.validate_all?;
let db = state.registry_ref..expect;
Register once. Boot, reload, run, and resolve by type.
Providers
A provider is any service that wants to join the application lifecycle.
use async_trait;
use ;
;
Lifecycle Ordering
Lifecycle order is deterministic and type-aware. Providers can express concrete
dependencies with ProviderOrder::before::<T>() / ProviderOrder::after::<T>()
instead of relying on registration order or magic priority numbers.
The same lifecycle plan is used for:
validate_allboot_allreload_allshutdown_all, in reverse order
Reload skips providers that are not Reloadable, but it keeps the same relative
dependency order as boot. If a dependency cycle is introduced, the registry
returns an error before running the lifecycle phase.
use ;
;
;
Registration order can now stay ergonomic:
use Arc;
state.registry_ref.insert;
state.registry_ref.insert;
let names = state.registry_ref.lifecycle_names?;
println!;
state.registry_ref.boot_all.await?;
// DbService boots before CacheService because CacheService says:
// ProviderOrder::new().after::<DbService>()
boot_priority() and Reloadable::priority() are still available as coarse
tie-breakers among otherwise-ready providers. Prefer ProviderOrder for real
dependencies. run_priority() controls only runtime task spawn order.
Runnable Providers
Long-running loops live in Runnable::run(), not in boot().
Runnable::run() is an async trait method and receives self: Arc<Self>, so
providers can be spawned without returning a boxed task future.
use Arc;
use async_trait;
use ;
Then the runtime starts every runnable provider:
use ;
let state = new;
let mut runtime = default;
runtime.spawn_all;
state.initiate_shutdown;
runtime.wait_until_shutdown.await?;
runtime.drain.await?;
# Ok::
Reloadable Providers
Reload is a capability, not a second registry.
use async_trait;
use ;
Use registry.reload_one("db", &state).await for targeted reloads or
registry.reload_all(&state).await for full reloads.
reload_all first calls ReloadState::reload() on your state, then walks the
same lifecycle order used by boot_all and calls Reloadable::reload() on
reloadable providers.
Gates
The support feature enables optional runtime support tools. They are not
required by Registry or Runtime, but they are useful when implementing
servers, connection loops, and request handlers.
Gate is a graceful shutdown admission/drain tool. It is inspired by axum's
server graceful-shutdown pattern, but lifted out of HTTP.
It can:
- reject new work after graceful shutdown starts
- track accepted in-flight work with
Permit - wait until all permits drop
- force shutdown after a grace period
- optionally apply simple max-in-flight admission control
use Duration;
use Gate;
let gate = new;
let permit = gate.enter.await?;
gate.graceful_shutdown;
gate.wait_all_done.await;
# Ok::
GuardGroup is a smaller RAII in-flight counter for places where you only
need “count active work and wait until zero” without admission control.
Lifecycle Events
The events feature enables LifecycleBus, a typed process-local event bus.
Use it when services need a loose in-process signal without depending on each
other directly.
use LifecycleBus;
;
let bus = new;
let mut rx = bus.;
bus.emit;
Examples
The library crate keeps small single-file examples:
examples/minimal.rs
examples/axum.rs
Run them with:
examples/minimal.rs demonstrates type-based boot order by registering
IdleService before DbProvider while DbProvider declares
ProviderOrder::new().before::<IdleService>(); the example prints
Planned boot order: db -> idle -> control before boot starts, then
Boot order: db -> idle -> control after boot completes.
axum is a dev-dependency only. It is there to show integration style; it is
not part of runtime-rs for library users.
Dependencies
The dependency set is intentionally small and canonical for Tokio-based services:
= "0.1"
= "6"
= { = "1", = ["macros", "rt", "sync", "time"] }
= "0.7"
= "0.1"
dashmap is only required when the events feature is enabled.
License
Licensed under either of:
- MIT license (LICENSE-MIT)
- Apache License, Version 2.0 (LICENSE-APACHE)