Compare commits

..

6 Commits

Author SHA1 Message Date
25a9005375 bump version 2026-05-26 02:52:34 -04:00
386eb59f84 add example 2026-05-26 02:52:27 -04:00
513cbc19a3 Update README.md 2026-05-26 02:44:08 -04:00
995f3cc61a add docs 2026-05-26 02:14:41 -04:00
a5b42ab55a Run shutdown hooks sequentially in reverse 2026-05-26 01:58:56 -04:00
65007f6cd2 AdHocPlugin on_init and on_setup accept capturing closures 2026-05-26 01:47:53 -04:00
5 changed files with 424 additions and 129 deletions

145
Cargo.lock generated
View File

@@ -55,6 +55,8 @@ dependencies = [
"axum",
"axum-app-wrapper-macros",
"futures",
"tokio",
"tracing",
"type-map",
]
@@ -92,6 +94,16 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -287,9 +299,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.177"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "log"
@@ -317,13 +329,13 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mio"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
@@ -445,6 +457,16 @@ dependencies = [
"serde",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "slab"
version = "0.4.11"
@@ -459,12 +481,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys",
]
[[package]]
@@ -486,23 +508,24 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
[[package]]
name = "tokio"
version = "1.48.0"
version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
"libc",
"mio",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
@@ -539,20 +562,32 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
version = "0.1.41"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.34"
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
]
@@ -584,15 +619,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -601,68 +627,3 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"

View File

@@ -1,15 +1,23 @@
[workspace]
members = [".", "crates/*"]
resolver = "2"
[package]
name = "axum-app-wrapper"
version = "0.1.1"
version = "0.1.2"
edition = "2024"
[workspace]
resolver = "2"
members = [".", "crates/*"]
[dependencies]
anyhow = "1.0"
axum = "0.8"
axum-app-wrapper-macros = { path = "crates/axum-app-wrapper-macros" }
futures = "0.3"
type-map = "0.5"
axum-app-wrapper-macros = { path = "crates/axum-app-wrapper-macros" }
[dev-dependencies]
tokio = {
version = "1.52.3",
default-features = false,
features = ["net", "rt", "rt-multi-thread", "signal"]
}
tracing = "0.1.44"

69
README.md Normal file
View File

@@ -0,0 +1,69 @@
# axum-app-wrapper
A small plugin layer for `axum` applications, inspired by [fastify](https://fastify.dev/) from the Node/JS ecosystem.
Plugins can:
- add startup state before the final axum state type exists
- add routes, services, and middleware after state is initialized
- run graceful shutdown work in reverse registration order
This crate is intentionally thin. It does not replace axum's router, extractors, or middleware. Rather, it organizes your server setup and teardown into plugins.
## Lifecycle
`App::init()` runs plugins in this order:
1. `on_init` in registration order, passing a `TypeMap` to build state.
2. `S::try_from(TypeMap)` to build the final typed app state.
3. `on_setup` in registration order, passing `Router<S>` and `&S`.
4. `on_shutdown` consecutively in reverse registration order: the last registered plugin shuts down first, and each hook finishes before the next one starts.
## Example
See the `examples` folder for full examples.
For `on_setup` closures that access typed state, construct the plugin as
`AdHocPlugin::<AppState>::new()`. That gives Rust enough context to infer the `state` parameter:
```rust
let plugin = AdHocPlugin::<AppState>::new()
.on_setup(|router, state| {
let config = Arc::clone(&state.config);
Ok(router.layer(axum::Extension(config)))
});
```
## Reusable Plugins
Implement `AppPlugin<S>` directly when setup should be reusable across apps:
```rust
use axum::Router;
use axum_app_wrapper::{AppPlugin, TypeMap};
use futures::{future::BoxFuture, FutureExt};
struct ConfigPlugin {
config: Config,
}
impl AppPlugin<AppState> for ConfigPlugin {
fn on_init(&mut self, mut state: TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> {
let config = self.config.clone();
async move {
state.insert(config);
Ok(state)
}
.boxed()
}
fn on_setup(
&mut self,
router: Router<AppState>,
_state: &AppState,
) -> anyhow::Result<Router<AppState>> {
Ok(router.route("/health", axum::routing::get(health)))
}
}
```

92
examples/app.rs Normal file
View File

@@ -0,0 +1,92 @@
use std::{net::SocketAddr, sync::Arc};
use axum::{Extension, extract::State, routing::get};
use axum_app_wrapper::{AdHocPlugin, App, AppState};
#[derive(Clone, AppState)]
struct AppState {
config: Arc<Config>,
metrics: Arc<Metrics>,
}
#[derive(Clone)]
struct Config {
service_name: String,
}
struct Metrics;
impl Metrics {
fn new() -> Self {
Self
}
async fn flush(&self) {
tracing::info!("flushed metrics");
}
}
async fn health(State(state): State<AppState>) -> String {
format!("{}:ok", state.config.service_name)
}
async fn metrics_handler(Extension(metrics): Extension<Arc<Metrics>>) -> &'static str {
let _metrics = metrics;
"metrics:ok"
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = Arc::new(Config {
service_name: "api".to_owned(),
});
let metrics_registry = Arc::new(Metrics::new());
let config_plugin = AdHocPlugin::<AppState>::new()
.on_init(async move |mut state| {
state.insert(config);
Ok(state)
})
.on_setup(|router, state| {
tracing::info!(service = %state.config.service_name, "configuring routes");
Ok(router.route("/health", get(health)))
});
let metrics_plugin = AdHocPlugin::<AppState>::new()
.on_init(async move |mut state| {
state.insert(metrics_registry);
Ok(state)
})
.on_setup(|router, state| {
Ok(router
.route("/metrics", get(metrics_handler))
.layer(Extension(Arc::clone(&state.metrics))))
})
.on_shutdown(|state| {
let metrics = Arc::clone(&state.metrics);
async move {
metrics.flush().await;
}
});
let app = App::<AppState>::new()
.register(config_plugin)
.register(metrics_plugin);
let (router, state, on_shutdown) = app.init().await?;
let router = router.with_state(state);
let addr: SocketAddr = "127.0.0.1:3000".parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, router)
.with_graceful_shutdown(async {
tokio::signal::ctrl_c()
.await
.expect("failed to listen for ctrl-c");
on_shutdown.await;
})
.await?;
Ok(())
}

View File

@@ -1,13 +1,37 @@
#![allow(clippy::type_complexity)]
//! A small plugin layer for [`axum`](https://docs.rs/axum/latest/axum/) applications.
//!
//! `axum-app-wrapper` lets plugins contribute startup state, router setup, and shutdown work
//! around an `axum::Router`.
//!
//! Lifecycle:
//!
//! 1. [`AppPlugin::on_init`] runs in registration order and builds a [`TypeMap`].
//! 2. The `TypeMap` is converted into the app state `S` with `TryFrom<TypeMap>`.
//! 3. [`AppPlugin::on_setup`] runs in registration order and receives `Router<S>` plus `&S`.
//! 4. The returned shutdown future runs [`AppPlugin::on_shutdown`] consecutively in reverse
//! registration order.
//!
//! For one-off plugins, use [`AdHocPlugin`]. If the setup closure needs access to typed state,
//! spell the state type at construction time so Rust can infer the closure parameters:
//!
//! ```ignore
//! let plugin = AdHocPlugin::<AppState>::new()
//! .on_init(async |mut state| {
//! state.insert(config);
//! Ok(state)
//! })
//! .on_setup(|router, state| {
//! let config = state.config.clone();
//! Ok(router.layer(axum::Extension(config)))
//! });
//! ```
use std::{fmt::Display, sync::Arc};
use anyhow::anyhow;
use axum::Router;
use futures::{
FutureExt,
future::{BoxFuture, join_all},
};
use futures::{FutureExt, future::BoxFuture};
// State extraction utilities
@@ -25,7 +49,7 @@ pub fn extract_type_field<T: Send + Sync + 'static>(map: &mut TypeMap) -> anyhow
// Plugin system
/// A lightweight wrapper around axum that enables building plugins around the router
/// An axum app builder with plugin-managed state, router setup, and shutdown hooks.
pub struct App<S = Arc<TypeMap>> {
base_router: Router<S>,
plugins: Vec<Box<dyn AppPlugin<S>>>,
@@ -36,7 +60,7 @@ impl<S> App<S>
where
S: TryFrom<TypeMap> + Clone + Send + Sync + 'static,
{
/// Create a new server with the given routes.
/// Create an empty app.
pub fn new() -> Self {
Self {
base_router: Router::new(),
@@ -45,8 +69,10 @@ where
}
}
/// Register a plugin. This can be considered analogous to axum/tower's `layer` function
/// and follows the same ordering (each plugin "wraps" the previous one).
/// Register a plugin.
///
/// `on_init` and `on_setup` run in registration order. Shutdown runs in reverse registration
/// order, awaiting each hook before starting the next.
pub fn register(mut self, plugin: impl AppPlugin<S> + 'static) -> Self {
self.plugins.push(Box::new(plugin));
self
@@ -68,8 +94,9 @@ where
// self
// }
/// Build and initialize the server. Returns the base router, finalized state, and a future to run
/// on graceful shutdown.
/// Build the router, finalized state, and graceful shutdown future.
///
/// The returned shutdown future must be awaited by the caller when the server is shutting down.
pub async fn init(mut self) -> anyhow::Result<(Router<S>, S, impl Future + Send)>
where
S::Error: Display,
@@ -86,44 +113,59 @@ where
router = plugin.on_setup(router, &state)?;
}
let shutdown_fns = self
let shutdown_fns: Vec<_> = self
.plugins
.into_iter()
.filter_map(|mut p| p.on_shutdown(&state));
let on_shutdown = join_all(shutdown_fns);
.rev()
.filter_map(|mut p| p.on_shutdown(&state))
.collect();
let on_shutdown = async move {
for shutdown_fn in shutdown_fns {
shutdown_fn.await;
}
};
Ok((router, state, on_shutdown))
}
}
/// Trait for a plugin that can be attached to the server
/// A plugin that can participate in app initialization, router setup, and shutdown.
#[allow(unused_variables, reason = "trait functions with default no-op")]
pub trait AppPlugin<S = Arc<TypeMap>> {
/// Init function that will run on server startup. Can add and manipulate state.
/// Run during startup before typed state exists.
///
/// Use this hook to insert or transform values in the shared [`TypeMap`].
fn on_init(&mut self, app_state: TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> {
async { Ok(app_state) }.boxed()
}
/// Setup function that will run _after_ state is initialized (e.g. routes, middleware,
/// and services should be added here)
/// Run after typed state has been created.
///
/// Use this hook to add routes, services, middleware, or router state.
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
Ok(router)
}
/// Teardown function that will run on server shutdown
/// Return shutdown work for this plugin.
///
/// Shutdown hooks are awaited consecutively in reverse registration order.
fn on_shutdown(&mut self, state: &S) -> Option<BoxFuture<'static, ()>> {
None
}
}
/// Utility to build a plugin on the fly
/// A closure-based plugin for application-local setup.
///
/// `on_init` and `on_setup` accept capturing closures. If `on_setup` uses typed state, prefer
/// `AdHocPlugin::<State>::new()` so the closure parameter type can be inferred.
pub struct AdHocPlugin<S = Arc<TypeMap>> {
on_init: Option<Box<dyn FnOnce(TypeMap) -> BoxFuture<'static, anyhow::Result<TypeMap>> + Send>>,
on_setup: Option<fn(router: Router<S>, state: &S) -> anyhow::Result<Router<S>>>,
on_setup: Option<Box<dyn FnOnce(Router<S>, &S) -> anyhow::Result<Router<S>> + Send>>,
on_shutdown: Option<Box<dyn FnOnce(&S) -> BoxFuture<'static, ()>>>,
}
impl<S: 'static> AdHocPlugin<S> {
/// Create an empty ad-hoc plugin.
pub fn new() -> Self {
Self {
on_init: None,
@@ -132,26 +174,26 @@ impl<S: 'static> AdHocPlugin<S> {
}
}
/// Init function that will run on server startup. Can add and manipulate state.
pub fn on_init<T>(mut self, on_init: fn(app_state: TypeMap) -> T) -> Self
/// Set startup state initialization for this plugin.
pub fn on_init<F, T>(mut self, on_init: F) -> Self
where
F: FnOnce(TypeMap) -> T + Send + 'static,
T: Future<Output = anyhow::Result<TypeMap>> + Send + 'static,
{
self.on_init = Some(Box::new(move |s| Box::pin(on_init(s))));
self
}
/// Setup function that will run _after_ state is initialized. (e.g. routes, middleware,
/// and services should be added here)
pub fn on_setup(
mut self,
on_setup: fn(router: Router<S>, state: &S) -> anyhow::Result<Router<S>>,
) -> Self {
self.on_setup = Some(on_setup);
/// Set router setup for this plugin.
pub fn on_setup<F>(mut self, on_setup: F) -> Self
where
F: FnOnce(Router<S>, &S) -> anyhow::Result<Router<S>> + Send + 'static,
{
self.on_setup = Some(Box::new(on_setup));
self
}
/// Teardown function that will run on server shutdown
/// Set shutdown work for this plugin.
pub fn on_shutdown<T>(mut self, on_shutdown: fn(state: &S) -> T) -> Self
where
T: Future<Output = ()> + Send + 'static,
@@ -170,7 +212,7 @@ impl<S> AppPlugin<S> for AdHocPlugin<S> {
}
fn on_setup(&mut self, router: Router<S>, state: &S) -> anyhow::Result<Router<S>> {
match self.on_setup {
match self.on_setup.take() {
Some(setup_fn) => setup_fn(router, state),
None => Ok(router),
}
@@ -183,3 +225,126 @@ impl<S> AppPlugin<S> for AdHocPlugin<S> {
}
}
}
#[cfg(test)]
mod tests {
use std::{
convert::Infallible,
sync::{
Mutex,
atomic::{AtomicUsize, Ordering},
},
task::Poll,
};
use super::*;
#[derive(Clone)]
struct TestState {
value: Arc<String>,
}
impl TryFrom<TypeMap> for TestState {
type Error = Infallible;
fn try_from(mut map: TypeMap) -> Result<Self, Self::Error> {
Ok(Self {
value: map.remove::<Arc<String>>().expect("missing state value"),
})
}
}
#[test]
fn adhoc_plugin_with_basic_state() {
let init_value = Arc::new(String::from("ready"));
let setup_value = Arc::clone(&init_value);
let plugin = AdHocPlugin::<TestState>::new()
.on_init(async |mut state| {
state.insert(init_value);
Ok(state)
})
.on_setup(move |router, state| {
assert_eq!(state.value.as_str(), setup_value.as_str());
Ok(router)
});
let app = App::<TestState>::new().register(plugin);
let (_router, state, _shutdown) =
futures::executor::block_on(app.init()).expect("app should initialize");
assert_eq!(state.value.as_str(), "ready");
}
struct ShutdownOrderPlugin {
name: &'static str,
events: Arc<Mutex<Vec<String>>>,
active_shutdowns: Arc<AtomicUsize>,
}
impl AppPlugin<TestState> for ShutdownOrderPlugin {
fn on_shutdown(&mut self, _state: &TestState) -> Option<BoxFuture<'static, ()>> {
let name = self.name;
let events = Arc::clone(&self.events);
let active_shutdowns = Arc::clone(&self.active_shutdowns);
let mut yielded = false;
Some(Box::pin(futures::future::poll_fn(move |cx| {
if !yielded {
yielded = true;
let previously_active = active_shutdowns.fetch_add(1, Ordering::SeqCst);
assert_eq!(previously_active, 0, "shutdown hooks ran concurrently");
events
.lock()
.expect("events lock poisoned")
.push(format!("{name}:start"));
cx.waker().wake_by_ref();
return Poll::Pending;
}
events
.lock()
.expect("events lock poisoned")
.push(format!("{name}:finish"));
active_shutdowns.fetch_sub(1, Ordering::SeqCst);
Poll::Ready(())
})))
}
}
#[test]
fn shutdown_hooks_order() {
let events = Arc::new(Mutex::new(Vec::new()));
let active_shutdowns = Arc::new(AtomicUsize::new(0));
let app = App::<TestState>::new()
.register(AdHocPlugin::<TestState>::new().on_init(async |mut state| {
state.insert(Arc::new(String::from("ready")));
Ok(state)
}))
.register(ShutdownOrderPlugin {
name: "first",
events: Arc::clone(&events),
active_shutdowns: Arc::clone(&active_shutdowns),
})
.register(ShutdownOrderPlugin {
name: "second",
events: Arc::clone(&events),
active_shutdowns,
});
let (_router, _state, on_shutdown) =
futures::executor::block_on(app.init()).expect("app should initialize");
futures::executor::block_on(on_shutdown);
assert_eq!(
*events.lock().expect("events lock poisoned"),
[
"second:start",
"second:finish",
"first:start",
"first:finish"
]
);
}
}