From c6520a28e0e2bcfe602f01311801218d74f95263 Mon Sep 17 00:00:00 2001 From: Spencer Ferris <3319370+spencewenski@users.noreply.github.com> Date: Tue, 12 Nov 2024 00:44:18 -0800 Subject: [PATCH] feat: Add support for TestContainers (pgsql + redis modules) Add support for TestContainers to make it easy to set up independent test environments to allow running tests in parallel without risk of cross-contamination even if they touch the DB. TestContainers support is behind the `test-containers` flag. When the flag is enabled, the consumer can enable TestContainers for the DB and Sidekiq Redis in the `AppConfig`, in which case a TestContainer instance will be create when building the `AppContext`, and the URI in the `AppConfig` will be updated to match that of the TestContainer instance. Additionally, a new method was added to `AppConfig` to allow consumers to create it themselves. This would allow consumers more control over the config, including manually creating a TestContainer instance and updating the URI of a field in the `AppConfig` before loading the application. Finally, a new `app::init_state` method was added to allow getting access to a real `AppState` instance in tests. This method performs most of the setup of the app without actually running the app. This is similar but slightly different to the existing `app::prepare` method, and is intended to only be used in tests. Closes https://github.com/roadster-rs/roadster/issues/500 --- Cargo.toml | 5 +- src/api/core/health.rs | 16 +- src/app/context.rs | 105 +++++++++- src/app/mod.rs | 236 +++++++++++++++++++---- src/config/database/mod.rs | 16 ++ src/config/mod.rs | 41 +++- src/config/service/worker/sidekiq/mod.rs | 9 + src/service/runner.rs | 7 +- 8 files changed, 380 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ba584e64..e9954fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,8 @@ jwt-openid = ["jwt"] cli = ["dep:clap"] otel = ["dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-otlp", "dep:tracing-opentelemetry", "dep:prost"] grpc = ["dep:tonic"] -testing = ["dep:insta", "dep:rstest"] +testing = ["dep:insta", "dep:rstest", "dep:testcontainers-modules"] +test-containers = ["testing", "dep:testcontainers-modules"] config-yml = ["config/yaml"] [dependencies] @@ -94,6 +95,7 @@ tonic = { workspace = true, optional = true } # Testing insta = { workspace = true, optional = true } rstest = { workspace = true, optional = true } +testcontainers-modules = { workspace = true, features = ["postgres", "redis"], optional = true } # Others anyhow = { workspace = true } @@ -171,6 +173,7 @@ rusty-sidekiq = { version = "0.11.0", default-features = false } # Testing insta = { version = "1.39.0", features = ["toml", "filters"] } rstest = { version = "0.23.0", default-features = false } +testcontainers-modules = { version = "0.11.3" } # Others # Todo: minimize tokio features included in `roadster` diff --git a/src/api/core/health.rs b/src/api/core/health.rs index 44b5e3ff..bf743fd1 100644 --- a/src/api/core/health.rs +++ b/src/api/core/health.rs @@ -37,6 +37,19 @@ pub async fn health_check( state: &S, duration: Option, ) -> RoadsterResult +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, +{ + let context = AppContext::from_ref(state); + health_check_with_checks(context.health_checks(), duration).await +} + +#[instrument(skip_all)] +pub(crate) async fn health_check_with_checks( + checks: Vec>, + duration: Option, +) -> RoadsterResult where S: Clone + Send + Sync + 'static, AppContext: FromRef, @@ -49,10 +62,9 @@ where } else { info!("Running checks"); } - let context = AppContext::from_ref(state); let timer = Instant::now(); - let check_futures = context.health_checks().into_iter().map(|check| { + let check_futures = checks.into_iter().map(|check| { Box::pin(async move { let name = check.name(); info!(%name, "Running check"); diff --git a/src/app/context.rs b/src/app/context.rs index 512706c1..4e051a50 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -25,7 +25,10 @@ impl AppContext { #[cfg_attr(test, allow(dead_code))] pub(crate) async fn new( #[allow(unused_variables)] app: &A, - config: AppConfig, + #[cfg(not(feature = "test-containers"))] config: AppConfig, + #[cfg(feature = "test-containers")] + #[allow(unused_mut)] + mut config: AppConfig, metadata: AppMetadata, ) -> RoadsterResult where @@ -40,6 +43,11 @@ impl AppContext { #[cfg(not(test))] let context = { + #[cfg(all(feature = "db-sql", feature = "test-containers"))] + let db_test_container = db_test_container(&mut config).await?; + #[cfg(all(feature = "sidekiq", feature = "test-containers"))] + let sidekiq_redis_test_container = sidekiq_redis_test_container(&mut config).await?; + #[cfg(feature = "db-sql")] let db = sea_orm::Database::connect(app.db_connection_options(&config)?).await?; @@ -89,10 +97,14 @@ impl AppContext { health_checks: OnceLock::new(), #[cfg(feature = "db-sql")] db, + #[cfg(all(feature = "db-sql", feature = "test-containers"))] + db_test_container, #[cfg(feature = "sidekiq")] redis_enqueue, #[cfg(feature = "sidekiq")] redis_fetch, + #[cfg(all(feature = "sidekiq", feature = "test-containers"))] + sidekiq_redis_test_container, #[cfg(feature = "email-smtp")] smtp, #[cfg(feature = "email-sendgrid")] @@ -184,12 +196,96 @@ impl AppContext { } } +#[cfg(all(feature = "db-sql", feature = "test-containers"))] +pub async fn db_test_container( + config: &mut AppConfig, +) -> RoadsterResult< + Option< + testcontainers_modules::testcontainers::ContainerAsync< + testcontainers_modules::postgres::Postgres, + >, + >, +> { + use testcontainers_modules::testcontainers::runners::AsyncRunner; + use testcontainers_modules::testcontainers::ImageExt; + + let container = if let Some(test_container) = config.database.test_container.as_ref() { + let container = testcontainers_modules::postgres::Postgres::default() + .with_tag(test_container.tag.to_string()) + .start() + .await + .map_err(|err| anyhow!("{err}"))?; + Some(container) + } else { + None + }; + + if let Some(container) = container.as_ref() { + let host_ip = container.get_host().await.map_err(|err| anyhow!("{err}"))?; + + let host_port = container + .get_host_port_ipv4(5432) + .await + .map_err(|err| anyhow!("{err}"))?; + + config.database.uri = + format!("postgres://postgres:postgres@{host_ip}:{host_port}/postgres").parse()?; + } + Ok(container) +} + +#[cfg(all(feature = "sidekiq", feature = "test-containers"))] +pub async fn sidekiq_redis_test_container( + config: &mut AppConfig, +) -> RoadsterResult< + Option< + testcontainers_modules::testcontainers::ContainerAsync< + testcontainers_modules::redis::Redis, + >, + >, +> { + use testcontainers_modules::testcontainers::runners::AsyncRunner; + use testcontainers_modules::testcontainers::ImageExt; + + let container = + if let Some(test_container) = config.service.sidekiq.custom.redis.test_container.as_ref() { + let container = testcontainers_modules::redis::Redis::default() + .with_tag(test_container.tag.to_string()) + .start() + .await + .map_err(|err| anyhow!("{err}"))?; + Some(container) + } else { + None + }; + + if let Some(container) = container.as_ref() { + let host_ip = container.get_host().await.map_err(|err| anyhow!("{err}"))?; + + let host_port = container + .get_host_port_ipv4(testcontainers_modules::redis::REDIS_PORT) + .await + .map_err(|err| anyhow!("{err}"))?; + + config.service.sidekiq.custom.redis.uri = + format!("redis://{host_ip}:{host_port}").parse()?; + } + Ok(container) +} + struct AppContextInner { config: AppConfig, metadata: AppMetadata, health_checks: OnceLock, #[cfg(feature = "db-sql")] db: DatabaseConnection, + #[cfg(all(feature = "db-sql", feature = "test-containers"))] + #[allow(dead_code)] + db_test_container: Option< + testcontainers_modules::testcontainers::ContainerAsync< + testcontainers_modules::postgres::Postgres, + >, + >, #[cfg(feature = "sidekiq")] redis_enqueue: sidekiq::RedisPool, /// The Redis connection pool used by [sidekiq::Processor] to fetch Sidekiq jobs from Redis. @@ -197,6 +293,13 @@ struct AppContextInner { /// config is set to zero, in which case the [sidekiq::Processor] would also not be started. #[cfg(feature = "sidekiq")] redis_fetch: Option, + #[cfg(all(feature = "sidekiq", feature = "test-containers"))] + #[allow(dead_code)] + sidekiq_redis_test_container: Option< + testcontainers_modules::testcontainers::ContainerAsync< + testcontainers_modules::redis::Redis, + >, + >, #[cfg(feature = "email-smtp")] smtp: lettre::SmtpTransport, #[cfg(feature = "email-sendgrid")] diff --git a/src/app/mod.rs b/src/app/mod.rs index b202f6d1..3ab3748e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -31,7 +31,7 @@ use crate::api::cli::RunCommand; use crate::app::metadata::AppMetadata; #[cfg(not(feature = "cli"))] use crate::config::environment::Environment; -use crate::config::AppConfig; +use crate::config::{AppConfig, AppConfigOptions}; use crate::error::RoadsterResult; use crate::health_check::registry::HealthCheckRegistry; use crate::lifecycle::registry::LifecycleHandlerRegistry; @@ -76,7 +76,13 @@ where } } - run_prepared_without_app_cli(prepare_from_cli_and_state(cli_and_state).await?).await + let prepared = prepare_from_cli_and_state(cli_and_state).await?; + + if run_prepared_service_cli(&prepared).await? { + return Ok(()); + } + + run_prepared_without_cli(prepared).await } #[non_exhaustive] @@ -113,7 +119,12 @@ where #[cfg(not(feature = "cli"))] let config_dir: Option = None; - let config = AppConfig::new_with_config_dir(environment, config_dir)?; + let config = AppConfig::new_with_options( + AppConfigOptions::builder() + .environment_opt(environment) + .config_dir_opt(config_dir) + .build(), + )?; app.init_tracing(&config)?; @@ -122,17 +133,7 @@ where #[cfg(feature = "cli")] config.validate(!roadster_cli.skip_validate_config)?; - #[cfg(not(test))] - let metadata = app.metadata(&config)?; - - // The `config.clone()` here is technically not necessary. However, without it, RustRover - // is giving a "value used after move" error when creating an actual `AppContext` below. - #[cfg(test)] - let context = AppContext::test(Some(config.clone()), None, None)?; - #[cfg(not(test))] - let context = AppContext::new::(&app, config, metadata).await?; - - let state = app.provide_state(context.clone()).await?; + let state = build_state(&app, config).await?; Ok(CliAndState { app, @@ -144,6 +145,26 @@ where }) } +/// Utility method to build the app's state object. +async fn build_state(app: &A, config: AppConfig) -> RoadsterResult +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, + A: App + Send + Sync + 'static, +{ + #[cfg(not(test))] + let metadata = app.metadata(&config)?; + + // The `config.clone()` here is technically not necessary. However, without it, RustRover + // is giving a "value used after move" error when creating an actual `AppContext` below. + #[cfg(test)] + let context = AppContext::test(Some(config.clone()), None, None)?; + #[cfg(not(test))] + let context = AppContext::new::(app, config, metadata).await?; + + app.provide_state(context).await +} + /// Contains all the objects needed to run the [`App`]. Useful if a consumer needs access to some /// of the prepared state before running the app. /// @@ -161,15 +182,30 @@ where #[cfg(feature = "cli")] pub app_cli: A::Cli, pub state: S, + pub health_check_registry: HealthCheckRegistry, pub service_registry: ServiceRegistry, pub lifecycle_handler_registry: LifecycleHandlerRegistry, } -/// Prepare the app. Does everything to prepare the app short of starting the app. Specifically, -/// the following are skipped: +#[non_exhaustive] +struct PreparedAppWithoutCli +where + A: App + 'static, + S: Clone + Send + Sync + 'static, + AppContext: FromRef, +{ + health_check_registry: HealthCheckRegistry, + service_registry: ServiceRegistry, + lifecycle_handler_registry: LifecycleHandlerRegistry, +} + +/// Prepare the app. Sets up everything needed to start the app, but does not execute anything. +/// Specifically, the following are skipped: +/// /// 1. Handling CLI commands /// 2. Health checks -/// 3. Starting any services +/// 3. Lifecycle Handlers +/// 4. Starting any services pub async fn prepare(app: A) -> RoadsterResult> where S: Clone + Send + Sync + 'static, @@ -179,6 +215,44 @@ where prepare_from_cli_and_state(build_cli_and_state(app).await?).await } +/// Initialize the app state. Does everything to initialize the app short of starting the app. +/// Similar to [`prepare`], except performs some steps that are skipped in [`prepare`]: +/// 1. Health checks +/// 2. Lifecycle Handlers +/// +/// The following are still skipped: +/// 1. Handling CLI commands +/// 2. Starting any services +/// +/// Additionally, the health checks are not attached to the [`AppContext`] to avoid a reference +/// cycle that prevents the [`AppContext`] from being dropped between tests. +/// +/// This is intended to only be used to get access to the app's fully set up state in tests. +#[cfg(feature = "testing")] +pub async fn init_state(app: &A, config: AppConfig) -> RoadsterResult +where + A: App + 'static, + S: Clone + Send + Sync + 'static, + AppContext: FromRef, +{ + let state = build_state(app, config).await?; + + let PreparedAppWithoutCli { + health_check_registry, + service_registry, + lifecycle_handler_registry, + } = prepare_without_cli(app, &state).await?; + before_app( + &state, + Some(health_check_registry), + &service_registry, + &lifecycle_handler_registry, + ) + .await?; + + Ok(state) +} + async fn prepare_from_cli_and_state( cli_and_state: CliAndState, ) -> RoadsterResult> @@ -195,19 +269,12 @@ where app_cli, state, } = 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?; - context.set_health_checks(health_check_registry)?; - let mut service_registry = ServiceRegistry::new(&state); - app.services(&mut service_registry, &state).await?; + let PreparedAppWithoutCli { + health_check_registry, + service_registry, + lifecycle_handler_registry, + } = prepare_without_cli(&app, &state).await?; Ok(PreparedApp { app, @@ -216,6 +283,40 @@ where #[cfg(feature = "cli")] app_cli, state, + health_check_registry, + service_registry, + lifecycle_handler_registry, + }) +} + +async fn prepare_without_cli( + app: &A, + state: &S, +) -> RoadsterResult> +where + S: Clone + Send + Sync + 'static, + AppContext: FromRef, + A: App + Send + Sync + 'static, +{ + 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?; + // Note that we used to set the health check registry on the `AppContext` here. However, we + // don't do that anymore because it causes a reference cycle between the `AppContext` and the + // `HealthChecks` (at least the ones that contain an `AppContext`). This shouldn't normally + // be a problem, but it causes TestContainers containers to not be cleaned up in tests. + // We may want to re-think some designs to avoid this reference cycle. + + let mut service_registry = ServiceRegistry::new(state); + app.services(&mut service_registry, state).await?; + + Ok(PreparedAppWithoutCli { + health_check_registry, service_registry, lifecycle_handler_registry, }) @@ -242,11 +343,15 @@ where } } - run_prepared_without_app_cli(prepared_app).await + if run_prepared_service_cli(&prepared_app).await? { + return Ok(()); + } + + run_prepared_without_cli(prepared_app).await } /// Run a [PreparedApp] that was previously crated by [prepare] -async fn run_prepared_without_app_cli(prepared_app: PreparedApp) -> RoadsterResult<()> +async fn run_prepared_service_cli(prepared_app: &PreparedApp) -> RoadsterResult where A: App + 'static, S: Clone + Send + Sync + 'static, @@ -254,12 +359,11 @@ where { let state = &prepared_app.state; - let context = AppContext::from_ref(state); let service_registry = &prepared_app.service_registry; if service_registry.services.is_empty() { - warn!("No enabled services were registered, exiting."); - return Ok(()); + warn!("No enabled services were registered."); + return Ok(false); } let lifecycle_handlers = prepared_app.lifecycle_handler_registry.handlers(state); @@ -280,16 +384,43 @@ where if crate::service::runner::handle_cli(roadster_cli, app_cli, service_registry, state) .await? { - return Ok(()); + return Ok(true); } } + Ok(false) +} + +/// Run the app's initialization logic (lifecycle handlers, health checks, etc). +async fn before_app( + state: &S, + health_check_registry: Option, + service_registry: &ServiceRegistry, + lifecycle_handler_registry: &LifecycleHandlerRegistry, +) -> RoadsterResult<()> +where + A: App + 'static, + S: Clone + Send + Sync + 'static, + AppContext: FromRef, +{ + if service_registry.services.is_empty() { + warn!("No enabled services were registered."); + } + + let lifecycle_handlers = lifecycle_handler_registry.handlers(state); + 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?; + let checks = if let Some(health_check_registry) = health_check_registry { + health_check_registry.checks() + } else { + let context = AppContext::from_ref(state); + context.health_checks() + }; + crate::service::runner::health_checks(checks).await?; info!("Running AppLifecycleHandler::before_services"); for handler in lifecycle_handlers.iter() { @@ -297,18 +428,45 @@ where 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; + + Ok(()) +} + +/// Run a [`PreparedApp`] that was previously crated by [`prepare`] without handling CLI commands +/// (they should have been handled already). +async fn run_prepared_without_cli(prepared_app: PreparedApp) -> RoadsterResult<()> +where + A: App + 'static, + S: Clone + Send + Sync + 'static, + AppContext: FromRef, +{ + let PreparedApp { + app, + state, + health_check_registry, + service_registry, + lifecycle_handler_registry, + .. + } = prepared_app; + + let context = AppContext::from_ref(&state); + context.set_health_checks(health_check_registry)?; + + before_app(&state, None, &service_registry, &lifecycle_handler_registry).await?; + + let result = crate::service::runner::run(app, service_registry, &state).await; if let Err(err) = result { error!("An error occurred in the app: {err}"); } info!("Shutting down"); + let lifecycle_handlers = lifecycle_handler_registry.handlers(&state); + 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; + 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}"); } diff --git a/src/config/database/mod.rs b/src/config/database/mod.rs index e823a3af..037205f9 100644 --- a/src/config/database/mod.rs +++ b/src/config/database/mod.rs @@ -13,25 +13,40 @@ use validator::Validate; pub struct Database { /// This can be overridden with an environment variable, e.g. `ROADSTER__DATABASE__URI=postgres://example:example@example:1234/example_app` pub uri: Url, + + /// Options for creating a Test Container instance for the DB. If enabled, the `Database#uri` + /// field will be overridden to be the URI for the Test Container instance that's created when + /// building the app's [`crate::app::context::AppContext`]. + #[cfg(feature = "test-containers")] + #[serde(default)] + pub test_container: Option, + /// Whether to automatically apply migrations during the app's start up. Migrations can also /// be manually performed via the `roadster migration [COMMAND]` CLI command. pub auto_migrate: bool, + #[serde(default = "Database::default_connect_timeout")] #[serde_as(as = "serde_with::DurationMilliSeconds")] pub connect_timeout: Duration, + /// Whether to attempt to connect to the DB immediately upon creating the [`ConnectOptions`]. /// If `true` will wait to connect to the DB until the first DB query is attempted. #[serde(default = "default_true")] pub connect_lazy: bool, + #[serde(default = "Database::default_acquire_timeout")] #[serde_as(as = "serde_with::DurationMilliSeconds")] pub acquire_timeout: Duration, + #[serde_as(as = "Option")] pub idle_timeout: Option, + #[serde_as(as = "Option")] pub max_lifetime: Option, + #[serde(default)] pub min_connections: u32, + pub max_connections: u32, } @@ -115,6 +130,7 @@ mod tests { fn db_config_to_connect_options() { let db = Database { uri: Url::parse("postgres://example:example@example:1234/example_app").unwrap(), + test_container: None, auto_migrate: true, connect_timeout: Duration::from_secs(1), connect_lazy: true, diff --git a/src/config/mod.rs b/src/config/mod.rs index 344ba908..7ab7ddb2 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -21,6 +21,7 @@ use serde_json::Value; use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; +use typed_builder::TypedBuilder; use validator::Validate; pub mod app_config; @@ -85,6 +86,8 @@ pub struct AppConfig { pub const ENV_VAR_PREFIX: &str = "ROADSTER"; pub const ENV_VAR_SEPARATOR: &str = "__"; +const DEFAULT_CONFIG_DIR: &str = "config/"; + cfg_if! { if #[cfg(feature = "config-yml")] { pub const FILE_EXTENSIONS: [&str; 3] = ["toml", "yaml", "yml"]; @@ -93,26 +96,36 @@ cfg_if! { } } +#[derive(TypedBuilder)] +#[non_exhaustive] +pub struct AppConfigOptions { + #[builder(default, setter(strip_option(fallback = environment_opt)))] + pub environment: Option, + #[builder(default, setter(strip_option(fallback = config_dir_opt)))] + pub config_dir: Option, +} + impl AppConfig { #[deprecated( since = "0.6.2", note = "This wasn't intended to be made public and may be removed in a future version." )] pub fn new(environment: Option) -> RoadsterResult { - Self::new_with_config_dir(environment, Some(PathBuf::from("config/"))) + let options = AppConfigOptions::builder() + .environment_opt(environment) + .config_dir(PathBuf::from(DEFAULT_CONFIG_DIR)) + .build(); + Self::new_with_options(options) } // This runs before tracing is initialized, so we need to use `println` in order to // log from this method. #[allow(clippy::disallowed_macros)] - pub(crate) fn new_with_config_dir( - environment: Option, - config_dir: Option, - ) -> RoadsterResult { + pub fn new_with_options(options: AppConfigOptions) -> RoadsterResult { dotenv().ok(); - let environment = if let Some(environment) = environment { - println!("Using environment from CLI args: {environment:?}"); + let environment = if let Some(environment) = options.environment { + println!("Using environment from options: {environment:?}"); environment } else { Environment::new()? @@ -120,8 +133,9 @@ impl AppConfig { let environment_string = environment.clone().to_string(); let environment_str = environment_string.as_str(); - let config_root_dir = config_dir - .unwrap_or_else(|| PathBuf::from("config/")) + let config_root_dir = options + .config_dir + .unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_DIR)) .canonicalize()?; println!("Loading configuration from directory {config_root_dir:?}"); @@ -326,6 +340,15 @@ pub struct App { pub shutdown_on_error: bool, } +#[cfg(feature = "test-containers")] +#[derive(Debug, Default, Validate, Clone, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case")] +#[non_exhaustive] +pub struct TestContainer { + pub enable: bool, + pub tag: String, +} + #[cfg(all( test, feature = "http", diff --git a/src/config/service/worker/sidekiq/mod.rs b/src/config/service/worker/sidekiq/mod.rs index c3ca3e9c..3097fee1 100644 --- a/src/config/service/worker/sidekiq/mod.rs +++ b/src/config/service/worker/sidekiq/mod.rs @@ -84,10 +84,19 @@ pub enum StaleCleanUpBehavior { #[non_exhaustive] pub struct Redis { pub uri: Url, + + /// Options for creating a Test Container instance for the DB. If enabled, the `Redis#uri` + /// field will be overridden to be the URI for the Test Container instance that's created when + /// building the app's [`crate::app::context::AppContext`]. + #[cfg(feature = "test-containers")] + #[serde(default)] + pub test_container: Option, + /// The configuration for the Redis connection pool used for enqueuing Sidekiq jobs in Redis. #[serde(default)] #[validate(nested)] pub enqueue_pool: ConnectionPool, + /// The configuration for the Redis connection pool used by [sidekiq::Processor] to fetch /// Sidekiq jobs from Redis. #[serde(default)] diff --git a/src/service/runner.rs b/src/service/runner.rs index 8daa5177..91489e40 100644 --- a/src/service/runner.rs +++ b/src/service/runner.rs @@ -1,9 +1,10 @@ #[cfg(feature = "cli")] use crate::api::cli::roadster::RoadsterCli; -use crate::api::core::health::health_check; +use crate::api::core::health::health_check_with_checks; use crate::app::context::AppContext; use crate::app::App; use crate::error::RoadsterResult; +use crate::health_check::HealthCheck; use crate::health_check::Status; use crate::service::registry::ServiceRegistry; use anyhow::anyhow; @@ -38,9 +39,9 @@ where } #[instrument(skip_all)] -pub(crate) async fn health_checks(context: &AppContext) -> RoadsterResult<()> { +pub(crate) async fn health_checks(checks: Vec>) -> RoadsterResult<()> { let duration = Duration::from_secs(60); - let response = health_check(context, Some(duration)).await?; + let response = health_check_with_checks(checks, Some(duration)).await?; let error_responses = response .resources