Skip to content

Commit

Permalink
feat: Allow running CLI commands without requiring DB/Redis connections
Browse files Browse the repository at this point in the history
Sea-orm added support for lazy database connections. We provide a config
field to allow toggling lazy database connections on or off. If lazy
connections are enabled, the default `run` method will allow running CLI
commands without requiring the the DB connection to be available (unless
the CLI command itself attempts to execute a DB query).

Closes #235
  • Loading branch information
spencewenski committed Aug 28, 2024
1 parent 32a98c9 commit a1732b0
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 17 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ axum = "0.7.4"
schemars = "0.8.16"

# DB
sea-orm = { version = "1.0.0" }
sea-orm = { version = "1.0.1" }
sea-orm-migration = { version = "1.0.0" }

# CLI
Expand Down
106 changes: 93 additions & 13 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,28 @@ where
AppContext: FromRef<S>,
A: App<S> + Default + Send + Sync + 'static,
{
let prepared = prepare(app).await?;
run_prepared(prepared).await
let cli_and_state = build_cli_and_state(app).await?;

#[cfg(feature = "cli")]
{
let CliAndState {
app,
#[cfg(feature = "cli")]
roadster_cli,
#[cfg(feature = "cli")]
app_cli,
state,
} = &cli_and_state;

if crate::api::cli::handle_cli(app, roadster_cli, app_cli, state).await? {
return Ok(());
}
}

run_prepared_without_app_cli(prepare_from_cli_and_state(cli_and_state).await?).await
}

#[non_exhaustive]
pub struct PreparedApp<A, S>
struct CliAndState<A, S>
where
A: App<S> + 'static,
S: Clone + Send + Sync + 'static,
Expand All @@ -55,15 +71,9 @@ where
#[cfg(feature = "cli")]
pub app_cli: A::Cli,
pub state: S,
pub service_registry: ServiceRegistry<A, S>,
}

/// Prepare the app. Does everything to prepare the app short of starting the app. Specifically,
/// the following are skipped:
/// 1. Handling CLI commands
/// 2. Health checks
/// 3. Starting any services
pub async fn prepare<A, S>(app: A) -> RoadsterResult<PreparedApp<A, S>>
async fn build_cli_and_state<A, S>(app: A) -> RoadsterResult<CliAndState<A, S>>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
Expand Down Expand Up @@ -98,6 +108,64 @@ where

let state = app.provide_state(context.clone()).await?;

Ok(CliAndState {
app,
#[cfg(feature = "cli")]
roadster_cli,
#[cfg(feature = "cli")]
app_cli,
state,
})
}

#[non_exhaustive]
pub struct PreparedApp<A, S>
where
A: App<S> + 'static,
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
pub app: A,
#[cfg(feature = "cli")]
pub roadster_cli: RoadsterCli,
#[cfg(feature = "cli")]
pub app_cli: A::Cli,
pub state: S,
pub service_registry: ServiceRegistry<A, S>,
}

/// Prepare the app. Does everything to prepare the app short of starting the app. Specifically,
/// the following are skipped:
/// 1. Handling CLI commands
/// 2. Health checks
/// 3. Starting any services
pub async fn prepare<A, S>(app: A) -> RoadsterResult<PreparedApp<A, S>>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
A: App<S> + Default + Send + Sync + 'static,
{
prepare_from_cli_and_state(build_cli_and_state(app).await?).await
}

async fn prepare_from_cli_and_state<A, S>(
cli_and_state: CliAndState<A, S>,
) -> RoadsterResult<PreparedApp<A, S>>
where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
A: App<S> + Default + Send + Sync + 'static,
{
let CliAndState {
app,
#[cfg(feature = "cli")]
roadster_cli,
#[cfg(feature = "cli")]
app_cli,
state,
} = cli_and_state;
let context = AppContext::from_ref(&state);

let mut health_check_registry = HealthCheckRegistry::new(&context);
app.health_checks(&mut health_check_registry, &state)
.await?;
Expand All @@ -124,20 +192,32 @@ where
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
let state = &prepared_app.state;

#[cfg(feature = "cli")]
{
let PreparedApp {
app,
roadster_cli,
app_cli,
state,
..
} = &prepared_app;
if crate::api::cli::handle_cli(app, roadster_cli, app_cli, state).await? {
return Ok(());
}
}

run_prepared_without_app_cli(prepared_app).await
}

/// Run a [PreparedApp] that was previously crated by [prepare]
async fn run_prepared_without_app_cli<A, S>(prepared_app: PreparedApp<A, S>) -> RoadsterResult<()>
where
A: App<S> + 'static,
S: Clone + Send + Sync + 'static,
AppContext: FromRef<S>,
{
let state = &prepared_app.state;

let context = AppContext::from_ref(state);
let service_registry = &prepared_app.service_registry;

Expand Down
7 changes: 7 additions & 0 deletions src/config/database/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::util::serde::default_true;
use sea_orm::ConnectOptions;
use serde_derive::{Deserialize, Serialize};
use serde_with::serde_as;
Expand All @@ -18,6 +19,10 @@ pub struct Database {
#[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,
Expand Down Expand Up @@ -51,6 +56,7 @@ impl From<&Database> for ConnectOptions {
let mut options = ConnectOptions::new(database.uri.to_string());
options
.connect_timeout(database.connect_timeout)
.connect_lazy(database.connect_lazy)
.acquire_timeout(database.acquire_timeout)
.min_connections(database.min_connections)
.max_connections(database.max_connections)
Expand Down Expand Up @@ -111,6 +117,7 @@ mod deserialize_tests {
uri: Url::parse("postgres://example:example@example:1234/example_app").unwrap(),
auto_migrate: true,
connect_timeout: Duration::from_secs(1),
connect_lazy: true,
acquire_timeout: Duration::from_secs(2),
idle_timeout: Some(Duration::from_secs(3)),
max_lifetime: Some(Duration::from_secs(4)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ ConnectOptions {
sqlcipher_key: None,
schema_search_path: None,
test_before_acquire: true,
connect_lazy: false,
connect_lazy: true,
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
source: src/config/database.rs
source: src/config/database/mod.rs
expression: database
---
uri = 'https://example.com:1234/'
auto-migrate = true
connect-timeout = 1000
connect-lazy = true
acquire-timeout = 1000
min-connections = 0
max-connections = 1
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
source: src/config/database.rs
source: src/config/database/mod.rs
expression: database
---
uri = 'https://example.com:1234/'
auto-migrate = true
connect-timeout = 1000
connect-lazy = true
acquire-timeout = 2000
idle-timeout = 3000
max-lifetime = 4000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ trace-propagation = true
uri = 'postgres://example:example@invalid_host:5432/example_test'
auto-migrate = true
connect-timeout = 1000
connect-lazy = true
acquire-timeout = 1000
min-connections = 0
max-connections = 10

0 comments on commit a1732b0

Please sign in to comment.