From 8342a9ef27746fd27cb52b92df0a947e91dbe097 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:42:34 -0700 Subject: [PATCH] feat: Add lifecycle handlers (#360) Add `AppLifecycleHandler` trait with methods that are called at various stages of an app's lifecycle. The initial set of methods are the following: - `before_service_cli` - `before_health_checks` - `before_services` - `on_shutdown` Closes https://github.com/roadster-rs/roadster/issues/350 --- src/app/mod.rs | 61 +++++++++- src/config/app_config.rs | 5 + src/config/default.toml | 3 + src/config/health_check/mod.rs | 6 + src/config/lifecycle/default.toml | 2 + src/config/lifecycle/mod.rs | 115 ++++++++++++++++++ src/config/mod.rs | 1 + ...ster__config__app_config__tests__test.snap | 6 + src/health_check/default.rs | 1 + src/health_check/mod.rs | 16 +-- src/lib.rs | 1 + src/lifecycle/db_migration.rs | 108 ++++++++++++++++ src/lifecycle/default.rs | 25 ++++ src/lifecycle/mod.rs | 85 +++++++++++++ src/lifecycle/registry.rs | 73 +++++++++++ src/service/http/initializer/default.rs | 1 + src/service/http/middleware/default.rs | 1 + src/service/runner.rs | 2 - 18 files changed, 496 insertions(+), 16 deletions(-) create mode 100644 src/config/lifecycle/default.toml create mode 100644 src/config/lifecycle/mod.rs create mode 100644 src/lifecycle/db_migration.rs create mode 100644 src/lifecycle/default.rs create mode 100644 src/lifecycle/mod.rs create mode 100644 src/lifecycle/registry.rs diff --git a/src/app/mod.rs b/src/app/mod.rs index 63103b74..458ddf34 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -15,6 +15,7 @@ use crate::config::app_config::AppConfig; use crate::config::environment::Environment; use crate::error::RoadsterResult; use crate::health_check::registry::HealthCheckRegistry; +use crate::lifecycle::registry::LifecycleHandlerRegistry; use crate::service::registry::ServiceRegistry; use crate::tracing::init_tracing; use async_trait::async_trait; @@ -30,7 +31,7 @@ use sea_orm_migration::MigratorTrait; use std::env; use std::future; use std::sync::Arc; -use tracing::{instrument, warn}; +use tracing::{error, info, instrument, warn}; pub async fn run(app: A) -> RoadsterResult<()> where @@ -59,6 +60,7 @@ where run_prepared_without_app_cli(prepare_from_cli_and_state(cli_and_state).await?).await } +#[non_exhaustive] struct CliAndState where A: App + 'static, @@ -132,6 +134,7 @@ where pub app_cli: A::Cli, pub state: S, pub service_registry: ServiceRegistry, + pub lifecycle_handler_registry: LifecycleHandlerRegistry, } /// Prepare the app. Does everything to prepare the app short of starting the app. Specifically, @@ -166,6 +169,10 @@ where } = cli_and_state; let context = AppContext::from_ref(&state); + let mut lifecycle_handler_registry = LifecycleHandlerRegistry::new(&state); + app.lifecycle_handlers(&mut lifecycle_handler_registry, &state) + .await?; + let mut health_check_registry = HealthCheckRegistry::new(&context); app.health_checks(&mut health_check_registry, &state) .await?; @@ -182,6 +189,7 @@ where app_cli, state, service_registry, + lifecycle_handler_registry, }) } @@ -226,8 +234,16 @@ where return Ok(()); } + let lifecycle_handlers = prepared_app.lifecycle_handler_registry.handlers(state); + #[cfg(feature = "cli")] { + info!("Running AppLifecycleHandler::before_service_cli"); + for handler in lifecycle_handlers.iter() { + info!(name=%handler.name(), "Running AppLifecycleHandler::before_service_cli"); + handler.before_service_cli(state).await?; + } + let PreparedApp { roadster_cli, app_cli, @@ -240,16 +256,37 @@ where } } - #[cfg(feature = "db-sql")] - if context.config().database.auto_migrate { - A::M::up(context.db(), None).await?; + info!("Running AppLifecycleHandler::before_health_checks"); + for handler in lifecycle_handlers.iter() { + info!(name=%handler.name(), "Running AppLifecycleHandler::before_health_checks"); + handler.before_health_checks(state).await?; } - crate::service::runner::health_checks(&context).await?; + info!("Running AppLifecycleHandler::before_services"); + for handler in lifecycle_handlers.iter() { + info!(name=%handler.name(), "Running AppLifecycleHandler::before_services"); + handler.before_services(state).await? + } crate::service::runner::before_run(service_registry, state).await?; + let result = + crate::service::runner::run(prepared_app.app, prepared_app.service_registry, state).await; + if let Err(err) = result { + error!("An error occurred in the app: {err}"); + } - crate::service::runner::run(prepared_app.app, prepared_app.service_registry, state).await?; + info!("Shutting down"); + + info!("Running AppLifecycleHandler::before_shutdown"); + for handler in lifecycle_handlers.iter() { + info!(name=%handler.name(), "Running AppLifecycleHandler::before_shutdown"); + let result = handler.on_shutdown(state).await; + if let Err(err) = result { + error!(name=%handler.name(), "An error occurred when running AppLifecycleHandler::before_shutdown: {err}"); + } + } + + info!("Shutdown complete"); Ok(()) } @@ -288,6 +325,14 @@ where /// See the following for more details regarding [FromRef]: async fn provide_state(&self, context: AppContext) -> RoadsterResult; + async fn lifecycle_handlers( + &self, + _registry: &mut LifecycleHandlerRegistry, + _state: &S, + ) -> RoadsterResult<()> { + Ok(()) + } + /// Provide the [crate::health_check::HealthCheck]s to use throughout the app. async fn health_checks( &self, @@ -315,6 +360,10 @@ where /// Override to provide custom graceful shutdown logic to clean up any resources created by /// the app. Roadster will take care of cleaning up the resources it created. + /// + /// Alternatively, provide a [`crate::lifecycle::AppLifecycleHandler::on_shutdown`] + /// implementation and provide the handler to the [`LifecycleHandlerRegistry`] in + /// [`Self::lifecycle_handlers`]. #[instrument(skip_all)] async fn graceful_shutdown(self: Arc, _state: &S) -> RoadsterResult<()> { Ok(()) diff --git a/src/config/app_config.rs b/src/config/app_config.rs index fb2dcf1e..11318041 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -3,6 +3,7 @@ use crate::config::auth::Auth; use crate::config::database::Database; use crate::config::environment::{Environment, ENVIRONMENT_ENV_VAR_NAME}; use crate::config::health_check::HealthCheck; +use crate::config::lifecycle::LifecycleHandler; use crate::config::service::Service; use crate::config::tracing::Tracing; use crate::error::RoadsterResult; @@ -28,6 +29,8 @@ pub struct AppConfig { #[validate(nested)] pub app: App, #[validate(nested)] + pub lifecycle_handler: LifecycleHandler, + #[validate(nested)] pub health_check: HealthCheck, #[validate(nested)] pub service: Service, @@ -172,6 +175,8 @@ impl AppConfig { #[cfg(feature = "sidekiq")] let config = config.add_source(crate::config::service::worker::sidekiq::default_config()); + let config = config.add_source(crate::config::lifecycle::default_config()); + let config = config.add_source(crate::config::health_check::default_config()); config diff --git a/src/config/default.toml b/src/config/default.toml index 0b87adde..620eb26c 100644 --- a/src/config/default.toml +++ b/src/config/default.toml @@ -1,6 +1,9 @@ [app] shutdown-on-error = true +[lifecycle-handler] +default-enable = true + [service] default-enable = true diff --git a/src/config/health_check/mod.rs b/src/config/health_check/mod.rs index 34520010..1b1a924e 100644 --- a/src/config/health_check/mod.rs +++ b/src/config/health_check/mod.rs @@ -19,11 +19,17 @@ pub fn default_config() -> config::File { pub struct HealthCheck { #[serde(default = "default_true")] pub default_enable: bool, + pub max_duration: MaxDuration, + #[cfg(feature = "db-sql")] + #[validate(nested)] pub database: HealthCheckConfig<()>, + #[cfg(feature = "sidekiq")] + #[validate(nested)] pub sidekiq: HealthCheckConfig<()>, + /// Allows providing configs for custom health checks. Any configs that aren't pre-defined above /// will be collected here. /// diff --git a/src/config/lifecycle/default.toml b/src/config/lifecycle/default.toml new file mode 100644 index 00000000..e3b239f9 --- /dev/null +++ b/src/config/lifecycle/default.toml @@ -0,0 +1,2 @@ +[lifecycle-handler.db-migration] +priority = 0 diff --git a/src/config/lifecycle/mod.rs b/src/config/lifecycle/mod.rs new file mode 100644 index 00000000..6b2a0512 --- /dev/null +++ b/src/config/lifecycle/mod.rs @@ -0,0 +1,115 @@ +use crate::app::context::AppContext; +use crate::config::app_config::CustomConfig; +use crate::util::serde::default_true; +use config::{FileFormat, FileSourceString}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use validator::Validate; + +pub fn default_config() -> config::File { + config::File::from_str(include_str!("default.toml"), FileFormat::Toml) +} + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct LifecycleHandler { + #[serde(default = "default_true")] + pub default_enable: bool, + + #[cfg(feature = "db-sql")] + #[validate(nested)] + pub db_migration: LifecycleHandlerConfig<()>, + + /// Allows providing configs for custom lifecycle handlers. Any configs that aren't pre-defined + /// above will be collected here. + /// + /// # Examples + /// + /// ```toml + /// [lifecycle-handler.foo] + /// enable = true + /// x = "y" + /// ``` + /// + /// This will be parsed as: + /// ```raw + /// LifecycleHandler#custom: { + /// "foo": { + /// LifecycleHandlerConfig#common: { + /// enable: true, + /// priority: 10 + /// }, + /// LifecycleHandlerConfig#custom: { + /// "x": "y" + /// } + /// } + /// } + /// ``` + #[serde(flatten)] + pub custom: BTreeMap>, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case", default)] +#[non_exhaustive] +pub struct CommonConfig { + // Optional so we can tell the difference between a consumer explicitly enabling/disabling + // the lifecycle handler, vs the lifecycle handler being enabled/disabled by default. + // If this is `None`, the value will match the value of `LifecycleHandler#default_enable`. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable: Option, + pub priority: i32, +} + +impl CommonConfig { + pub fn enabled(&self, context: &AppContext) -> bool { + self.enable + .unwrap_or(context.config().lifecycle_handler.default_enable) + } +} + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct LifecycleHandlerConfig { + #[serde(flatten, default)] + pub common: CommonConfig, + #[serde(flatten)] + pub custom: T, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::app_config::AppConfig; + use rstest::rstest; + + #[rstest] + #[case(true, None, true)] + #[case(true, Some(true), true)] + #[case(true, Some(false), false)] + #[case(false, None, false)] + #[case(false, Some(true), true)] + #[case(false, Some(false), false)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn common_config_enabled( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] expected_enabled: bool, + ) { + // Arrange + let mut config = AppConfig::test(None).unwrap(); + config.lifecycle_handler.default_enable = default_enable; + + let context = AppContext::test(Some(config), None, None).unwrap(); + + let common_config = CommonConfig { + enable, + priority: 0, + }; + + // Act/Assert + assert_eq!(common_config.enabled(&context), expected_enabled); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index b5e8a71c..51fe9c57 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,5 +4,6 @@ pub mod auth; pub mod database; pub mod environment; pub mod health_check; +mod lifecycle; pub mod service; pub mod tracing; diff --git a/src/config/snapshots/roadster__config__app_config__tests__test.snap b/src/config/snapshots/roadster__config__app_config__tests__test.snap index 1b636dc9..d0f1c66c 100644 --- a/src/config/snapshots/roadster__config__app_config__tests__test.snap +++ b/src/config/snapshots/roadster__config__app_config__tests__test.snap @@ -8,6 +8,12 @@ environment = 'test' name = 'Test' shutdown-on-error = true +[lifecycle-handler] +default-enable = true + +[lifecycle-handler.db-migration] +priority = 0 + [health-check] default-enable = true diff --git a/src/health_check/default.rs b/src/health_check/default.rs index 96518cd4..636ef909 100644 --- a/src/health_check/default.rs +++ b/src/health_check/default.rs @@ -26,6 +26,7 @@ pub fn default_health_checks( context: context.clone(), }), ]; + health_checks .into_iter() .filter(|check| check.enabled()) diff --git a/src/health_check/mod.rs b/src/health_check/mod.rs index 52e1b078..096168f5 100644 --- a/src/health_check/mod.rs +++ b/src/health_check/mod.rs @@ -61,10 +61,10 @@ pub struct ErrorData { /// Trait used to check the health of the app before its services start up. /// -/// This is a separate trait, vs adding a "health check" method to `AppService`, to allow defining -/// health checks that apply to multiple services. For example, most services would require -/// the DB and Redis connections to be valid, so we would want to perform a check for these -/// resources a single time before starting any service instead of once for every service that +/// This is a separate trait, vs adding a "health check" method to [`crate::service::AppService`], +/// to allow defining health checks that apply to multiple services. For example, most services +/// would require the DB and Redis connections to be valid, so we would want to perform a check for +/// these resources a single time before starting any service instead of once for every service that /// needs the resources. /// /// Another benefit of using a separate trait is, because the health checks are decoupled from @@ -73,14 +73,14 @@ pub struct ErrorData { #[cfg_attr(test, mockall::automock)] #[async_trait] pub trait HealthCheck: Send + Sync { - /// The name of the health check. + /// The name of the [`HealthCheck`]. fn name(&self) -> String; /// Whether the health check is enabled. If the health check is not enabled, Roadster will not - /// run it. However, if a consumer wants, they can certainly create a [HealthCheck] instance - /// and directly call `HealthCheck#check` even if `HealthCheck#enabled` returns `false`. + /// run it. However, if a consumer wants, they can certainly create a [`HealthCheck`] instance + /// and directly call [`HealthCheck::check`] even if [`HealthCheck::enabled`] returns `false`. fn enabled(&self) -> bool; - /// Run the health check. + /// Run the [`HealthCheck`]. async fn check(&self) -> RoadsterResult; } diff --git a/src/lib.rs b/src/lib.rs index 238a02ee..44efe33e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,7 @@ pub mod app; pub mod config; pub mod error; pub mod health_check; +pub mod lifecycle; pub mod middleware; #[cfg(feature = "db-sql")] pub mod migration; diff --git a/src/lifecycle/db_migration.rs b/src/lifecycle/db_migration.rs new file mode 100644 index 00000000..ebacf385 --- /dev/null +++ b/src/lifecycle/db_migration.rs @@ -0,0 +1,108 @@ +//! This [`AppLifecycleHandler`] runs the app's ['up' migration][`MigratorTrait::up`] +//! in [`AppLifecycleHandler::before_services`]. + +use crate::app::context::AppContext; +use crate::app::App; +use crate::error::RoadsterResult; +use crate::lifecycle::AppLifecycleHandler; +use async_trait::async_trait; +use axum::extract::FromRef; +use sea_orm_migration::MigratorTrait; + +pub struct DbMigrationLifecycleHandler; + +#[async_trait] +impl AppLifecycleHandler for DbMigrationLifecycleHandler +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, + A: App + 'static, +{ + fn name(&self) -> String { + "db-migration".to_string() + } + + fn enabled(&self, state: &S) -> bool { + let context = AppContext::from_ref(state); + context.config().database.auto_migrate + && context + .config() + .lifecycle_handler + .db_migration + .common + .enabled(&context) + } + + fn priority(&self, state: &S) -> i32 { + let context = AppContext::from_ref(state); + context + .config() + .lifecycle_handler + .db_migration + .common + .priority + } + + async fn before_services(&self, state: &S) -> RoadsterResult<()> { + let context = AppContext::from_ref(state); + + A::M::up(context.db(), None).await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::MockApp; + use crate::config::app_config::AppConfig; + use rstest::rstest; + + #[rstest] + #[case(false, Some(true), true)] + #[case(false, Some(false), false)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn enabled( + #[case] default_enable: bool, + #[case] enable: Option, + #[case] expected_enabled: bool, + ) { + // Arrange + let mut config = AppConfig::test(None).unwrap(); + config.lifecycle_handler.default_enable = default_enable; + config.lifecycle_handler.db_migration.common.enable = enable; + + let context = AppContext::test(Some(config), None, None).unwrap(); + + let handler = DbMigrationLifecycleHandler; + + // Act/Assert + assert_eq!( + AppLifecycleHandler::, AppContext>::enabled(&handler, &context), + expected_enabled + ); + } + + #[rstest] + #[case(None, 0)] + #[case(Some(1234), 1234)] + #[cfg_attr(coverage_nightly, coverage(off))] + fn priority(#[case] override_priority: Option, #[case] expected_priority: i32) { + // Arrange + let mut config = AppConfig::test(None).unwrap(); + if let Some(priority) = override_priority { + config.lifecycle_handler.db_migration.common.priority = priority; + } + + let context = AppContext::test(Some(config), None, None).unwrap(); + + let handler = DbMigrationLifecycleHandler; + + // Act/Assert + assert_eq!( + AppLifecycleHandler::, AppContext>::priority(&handler, &context), + expected_priority + ); + } +} diff --git a/src/lifecycle/default.rs b/src/lifecycle/default.rs new file mode 100644 index 00000000..54a7bcf6 --- /dev/null +++ b/src/lifecycle/default.rs @@ -0,0 +1,25 @@ +use crate::app::context::AppContext; +use crate::app::App; +use crate::lifecycle::AppLifecycleHandler; +use axum::extract::FromRef; +use std::collections::BTreeMap; + +pub fn default_lifecycle_handlers( + state: &S, +) -> BTreeMap>> +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, + A: App + 'static, +{ + let lifecycle_handlers: Vec>> = vec![ + #[cfg(feature = "db-sql")] + Box::new(crate::lifecycle::db_migration::DbMigrationLifecycleHandler), + ]; + + lifecycle_handlers + .into_iter() + .filter(|handler| handler.enabled(state)) + .map(|handler| (handler.name(), handler)) + .collect() +} diff --git a/src/lifecycle/mod.rs b/src/lifecycle/mod.rs new file mode 100644 index 00000000..875a25ec --- /dev/null +++ b/src/lifecycle/mod.rs @@ -0,0 +1,85 @@ +#[cfg(feature = "db-sql")] +pub mod db_migration; +pub mod default; +pub mod registry; + +use crate::app::context::AppContext; +use crate::app::App; +use crate::error::RoadsterResult; +use async_trait::async_trait; +use axum::extract::FromRef; + +/// Trait used to hook into various stages of the app's lifecycle. +/// +/// The app's lifecycle generally looks something like this: +/// 1. Parse the [`crate::config::app_config::AppConfig`] +/// 2. Initialize tracing to enable logs/traces +/// 3. Build the [`crate::app::context::AppContext`] and the [`crate::app::App`]'s custom state +/// 4. Run the roadster/app CLI command, if one was specified when the app was started +/// 5. Register [`AppLifecycleHandler`]s, [`crate::health_check::HealthCheck`]s, and +/// [`crate::service::AppService`]s +/// 6. Run any CLI commands that are implemented by [`crate::service::AppService::handle_cli`] +/// 7. Run the registered [`crate::health_check::HealthCheck`]s +/// 8. Run the registered [`crate::service::AppService`]s +/// 9. Wait for a shutdown signal, e.g., `Ctrl+c` or a custom signal from +/// [`crate::app::App::graceful_shutdown_signal`], and stop the [`crate::service::AppService`]s +/// when the signal is received. +/// 9. Run Roadster's graceful shutdown logic +/// 10. Run the app's [`crate::app::App::graceful_shutdown`] logic. +#[cfg_attr(test, mockall::automock)] +#[async_trait] +pub trait AppLifecycleHandler: Send + Sync +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, + A: App + 'static, +{ + /// The name of the [`AppLifecycleHandler`]. + fn name(&self) -> String; + + /// Whether the [`AppLifecycleHandler`] is enabled. + fn enabled(&self, _state: &S) -> bool { + true + } + + /// Used to determine the order in which the [`AppLifecycleHandler`] will run when during app + /// startup. Smaller numbers will run before larger numbers. For example, a + /// [`AppLifecycleHandler`] with priority `-10` will run before a [`AppLifecycleHandler`] + /// with priority `10`. + /// + /// If two [`AppLifecycleHandler`]s have the same priority, they are not guaranteed to run + /// in any particular order relative to each other. This may be fine for many + /// [`AppLifecycleHandler`]s . + /// + /// If the order in which your [`AppLifecycleHandler`] runs doesn't particularly matter, it's + /// generally safe to set its priority as `0`. + fn priority(&self, _state: &S) -> i32 { + 0 + } + + /// This method is run right before running any CLI commands implemented by + /// [`crate::service::AppService::handle_cli`]. + #[cfg(feature = "cli")] + async fn before_service_cli(&self, _state: &S) -> RoadsterResult<()> { + Ok(()) + } + + /// This method is run right before the app's [`crate::health_check::HealthCheck`]s during + /// app startup. + async fn before_health_checks(&self, _state: &S) -> RoadsterResult<()> { + Ok(()) + } + + /// This method is run right before the app's [`crate::service::AppService`]s are started. + async fn before_services(&self, _state: &S) -> RoadsterResult<()> { + Ok(()) + } + + /// This method is run after the app's [`crate::service::AppService`]s have stopped. + /// This method is an alternative to implementing [`crate::app::App::graceful_shutdown`]. + /// + /// Note that this method runs after [`crate::app::App::graceful_shutdown`] has completed. + async fn on_shutdown(&self, _state: &S) -> RoadsterResult<()> { + Ok(()) + } +} diff --git a/src/lifecycle/registry.rs b/src/lifecycle/registry.rs new file mode 100644 index 00000000..82b802dc --- /dev/null +++ b/src/lifecycle/registry.rs @@ -0,0 +1,73 @@ +use crate::app::context::AppContext; +use crate::app::App; +use crate::error::RoadsterResult; +use crate::lifecycle::default::default_lifecycle_handlers; +use crate::lifecycle::AppLifecycleHandler; +use anyhow::anyhow; +use axum::extract::FromRef; +use itertools::Itertools; +use std::collections::BTreeMap; +use std::ops::Deref; +use tracing::info; + +/// Registry for the app's [`AppLifecycleHandler`]s. +pub struct LifecycleHandlerRegistry +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, + A: App + ?Sized + 'static, +{ + state: S, + handlers: BTreeMap>>, +} + +impl LifecycleHandlerRegistry +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, + A: App + 'static, +{ + pub(crate) fn new(state: &S) -> Self { + Self { + state: state.clone(), + handlers: default_lifecycle_handlers(state), + } + } + + /// Register a new [`AppLifecycleHandler`]. If the [`AppLifecycleHandler`] is not enabled + /// (e.g., [[`AppLifecycleHandler::enabled`] returns `false`), the [`AppLifecycleHandler`] + /// will not be registered. + pub fn register(&mut self, handler: H) -> RoadsterResult<()> + where + H: AppLifecycleHandler + 'static, + { + let name = handler.name(); + + if !handler.enabled(&self.state) { + info!(name=%name, "Lifecycle handler is not enabled, skipping registration"); + return Ok(()); + } + + info!(name=%name, "Registering lifecycle handler"); + + if self + .handlers + .insert(name.clone(), Box::new(handler)) + .is_some() + { + return Err(anyhow!("Handler `{}` was already registered", name).into()); + } + + Ok(()) + } + + /// Get the registered [`AppLifecycleHandler`]s, ordered by their + /// [`AppLifecycleHandler::priority`]. + pub(crate) fn handlers(&self, state: &S) -> Vec<&dyn AppLifecycleHandler> { + self.handlers + .values() + .sorted_by(|a, b| Ord::cmp(&a.priority(state), &b.priority(state))) + .map(|handler| handler.deref()) + .collect_vec() + } +} diff --git a/src/service/http/initializer/default.rs b/src/service/http/initializer/default.rs index dfe44ee6..0f17be0e 100644 --- a/src/service/http/initializer/default.rs +++ b/src/service/http/initializer/default.rs @@ -10,6 +10,7 @@ where AppContext: FromRef, { let initializers: Vec>> = vec![Box::new(NormalizePathInitializer)]; + initializers .into_iter() .filter(|initializer| initializer.enabled(state)) diff --git a/src/service/http/middleware/default.rs b/src/service/http/middleware/default.rs index bfe5db31..67542544 100644 --- a/src/service/http/middleware/default.rs +++ b/src/service/http/middleware/default.rs @@ -34,6 +34,7 @@ where Box::new(CorsMiddleware), Box::new(RequestResponseLoggingMiddleware), ]; + middleware .into_iter() .filter(|middleware| middleware.enabled(state)) diff --git a/src/service/runner.rs b/src/service/runner.rs index 78a666cb..92c816cb 100644 --- a/src/service/runner.rs +++ b/src/service/runner.rs @@ -160,8 +160,6 @@ where } } - info!("Shutdown complete"); - Ok(()) }