diff --git a/crates/factor-key-value/src/lib.rs b/crates/factor-key-value/src/lib.rs index 1cc94cee52..77a4ce1e8a 100644 --- a/crates/factor-key-value/src/lib.rs +++ b/crates/factor-key-value/src/lib.rs @@ -120,9 +120,17 @@ pub struct AppState { } impl AppState { + /// Returns the [`StoreManager::summary`] for the given store label. pub fn store_summary(&self, label: &str) -> Option { self.store_manager.summary(label) } + + /// Returns true if the given store label is used by any component. + pub fn store_is_used(&self, label: &str) -> bool { + self.component_allowed_stores + .values() + .any(|stores| stores.contains(label)) + } } pub struct InstanceBuilder { diff --git a/crates/factor-sqlite/src/lib.rs b/crates/factor-sqlite/src/lib.rs index bd17432e9b..08f8aa5474 100644 --- a/crates/factor-sqlite/src/lib.rs +++ b/crates/factor-sqlite/src/lib.rs @@ -169,13 +169,17 @@ impl AppState { /// Get a connection for a given database label. /// /// Returns `None` if there is no connection creator for the given label. - pub async fn get_connection( - &self, - label: &str, - ) -> Option, v2::Error>> { + pub fn get_connection(&self, label: &str) -> Option, v2::Error>> { let connection = (self.get_connection_creator)(label)?.create_connection(); Some(connection) } + + /// Returns true if the given database label is used by any component. + pub fn database_is_used(&self, label: &str) -> bool { + self.allowed_databases + .values() + .any(|stores| stores.contains(label)) + } } /// A creator of a connections for a particular SQLite database. @@ -205,4 +209,11 @@ pub trait Connection: Send + Sync { ) -> Result; async fn execute_batch(&self, statements: &str) -> anyhow::Result<()>; + + /// A human-readable summary of the connection's configuration + /// + /// Example: "libSQL at libsql://example.com" + fn summary(&self) -> Option { + None + } } diff --git a/crates/factors-executor/src/lib.rs b/crates/factors-executor/src/lib.rs index 8bddf27b66..3f5e01bfb2 100644 --- a/crates/factors-executor/src/lib.rs +++ b/crates/factors-executor/src/lib.rs @@ -34,10 +34,7 @@ impl FactorsExecutor { } } -impl FactorsExecutor -where - T::AppState: Sync, -{ +impl FactorsExecutor { /// Adds the given [`ExecutorHooks`] to this executor. /// /// Hooks are run in the order they are added. @@ -84,7 +81,6 @@ where pub trait ExecutorHooks: Send + Sync where T: RuntimeFactors, - T::AppState: Sync, { /// Configure app hooks run immediately after [`RuntimeFactors::configure_app`]. async fn configure_app(&mut self, configured_app: &ConfiguredApp) -> anyhow::Result<()> { @@ -143,10 +139,7 @@ impl FactorsExecutorApp { } } -impl FactorsExecutorApp -where - T::AppState: Sync, -{ +impl FactorsExecutorApp { /// Returns an instance builder for the given component ID. pub fn prepare(&self, component_id: &str) -> anyhow::Result> { let app_component = self diff --git a/crates/factors/src/factor.rs b/crates/factors/src/factor.rs index 06e8a996d6..0f1d49d438 100644 --- a/crates/factors/src/factor.rs +++ b/crates/factors/src/factor.rs @@ -17,7 +17,7 @@ pub trait Factor: Any + Sized { /// The application state of this factor. /// /// This state *may* be cached by the runtime across multiple requests. - type AppState; + type AppState: Sync; /// The builder of instance state for this factor. type InstanceBuilder: FactorInstanceBuilder; diff --git a/crates/factors/src/runtime_factors.rs b/crates/factors/src/runtime_factors.rs index e18a66c171..5aa80c8083 100644 --- a/crates/factors/src/runtime_factors.rs +++ b/crates/factors/src/runtime_factors.rs @@ -32,7 +32,7 @@ use crate::{factor::FactorInstanceState, App, ConfiguredApp, Factor}; /// ``` pub trait RuntimeFactors: Sized + 'static { /// The per application state of all the factors. - type AppState; + type AppState: Sync; /// The per instance state of the factors. type InstanceState: RuntimeFactorsInstanceState; /// The collection of all the `InstanceBuilder`s of the factors. diff --git a/crates/sqlite-inproc/src/lib.rs b/crates/sqlite-inproc/src/lib.rs index c972341c88..b4896125cd 100644 --- a/crates/sqlite-inproc/src/lib.rs +++ b/crates/sqlite-inproc/src/lib.rs @@ -112,6 +112,13 @@ impl Connection for InProcConnection { async fn execute_batch(&self, statements: &str) -> anyhow::Result<()> { self.execute_batch(statements).await } + + fn summary(&self) -> Option { + Some(match &self.location { + InProcDatabaseLocation::InMemory => "a temporary in-memory database".to_string(), + InProcDatabaseLocation::Path(path) => format!("\"{}\"", path.display()), + }) + } } fn execute_query( diff --git a/crates/sqlite/src/lib.rs b/crates/sqlite/src/lib.rs index a7d347d9c1..b92eda2c15 100644 --- a/crates/sqlite/src/lib.rs +++ b/crates/sqlite/src/lib.rs @@ -166,6 +166,10 @@ impl Connection for LibSqlConnection { let client = self.get_client().await?; client.execute_batch(statements).await } + + fn summary(&self) -> Option { + Some(format!("libSQL at {}", self.url)) + } } /// Configuration for a local SQLite database. diff --git a/crates/trigger/src/cli.rs b/crates/trigger/src/cli.rs index 5ae714b958..d2d25ce99b 100644 --- a/crates/trigger/src/cli.rs +++ b/crates/trigger/src/cli.rs @@ -15,7 +15,7 @@ use spin_core::async_trait; use spin_factors_executor::{ComponentLoader, FactorsExecutor}; use spin_runtime_config::{ResolvedRuntimeConfig, UserProvidedPath}; use sqlite_statements::SqlStatementExecutorHook; -use summary::KeyValueDefaultStoreSummaryHook; +use summary::{KeyValueDefaultStoreSummaryHook, SqliteDefaultStoreSummaryHook}; use crate::factors::{TriggerFactors, TriggerFactorsRuntimeConfig}; use crate::stdio::{FollowComponents, StdioLoggingExecutorHooks}; @@ -424,8 +424,8 @@ impl TriggerAppBuilder { // TODO: // builder.hooks(SummariseRuntimeConfigHook::new(&self.runtime_config_file)); executor.add_hooks(KeyValueDefaultStoreSummaryHook); + executor.add_hooks(SqliteDefaultStoreSummaryHook); executor.add_hooks(SqlStatementExecutorHook::new(options.sqlite_statements)); - // builder.hooks(SqlitePersistenceMessageHook); let configured_app = { let _sloth_guard = warn_if_wasm_build_slothful(); diff --git a/crates/trigger/src/cli/sqlite_statements.rs b/crates/trigger/src/cli/sqlite_statements.rs index 54108f58ff..c00737cde6 100644 --- a/crates/trigger/src/cli/sqlite_statements.rs +++ b/crates/trigger/src/cli/sqlite_statements.rs @@ -31,7 +31,6 @@ impl SqlStatementExecutorHook { let get_database = |label| async move { sqlite .get_connection(label) - .await .transpose() .with_context(|| format!("failed connect to database with label '{label}'")) }; @@ -69,7 +68,6 @@ impl SqlStatementExecutorHook { impl ExecutorHooks for SqlStatementExecutorHook where F: RuntimeFactors, - F::AppState: Sync, { async fn configure_app( &mut self, diff --git a/crates/trigger/src/cli/summary.rs b/crates/trigger/src/cli/summary.rs index a62faba680..50906daf5f 100644 --- a/crates/trigger/src/cli/summary.rs +++ b/crates/trigger/src/cli/summary.rs @@ -1,9 +1,11 @@ use spin_core::async_trait; use spin_factor_key_value::KeyValueFactor; +use spin_factor_sqlite::SqliteFactor; use spin_factors_executor::ExecutorHooks; use crate::factors::TriggerFactors; +/// An [`ExecutorHooks`] that prints information about the default KV store. pub struct KeyValueDefaultStoreSummaryHook; #[async_trait] @@ -12,13 +14,43 @@ impl ExecutorHooks for KeyValueDefaultStoreSummaryHook { &mut self, configured_app: &spin_factors::ConfiguredApp, ) -> anyhow::Result<()> { - if let Some(default_store_summary) = configured_app - .app_state::() - .ok() - .and_then(|kv_state| kv_state.store_summary("default")) - { + let Ok(kv_app_state) = configured_app.app_state::() else { + return Ok(()); + }; + if !kv_app_state.store_is_used("default") { + // We don't talk about unused default stores + return Ok(()); + } + if let Some(default_store_summary) = kv_app_state.store_summary("default") { println!("Storing default key-value data to {default_store_summary}."); } Ok(()) } } + +/// An [`ExecutorHooks`] that prints information about the default KV store. +pub struct SqliteDefaultStoreSummaryHook; + +#[async_trait] +impl ExecutorHooks for SqliteDefaultStoreSummaryHook { + async fn configure_app( + &mut self, + configured_app: &spin_factors::ConfiguredApp, + ) -> anyhow::Result<()> { + let Ok(sqlite_app_state) = configured_app.app_state::() else { + return Ok(()); + }; + if !sqlite_app_state.database_is_used("default") { + // We don't talk about unused default databases + return Ok(()); + } + if let Some(default_database_summary) = sqlite_app_state + .get_connection("default") + .and_then(Result::ok) + .and_then(|conn| conn.summary()) + { + println!("Storing default SQLite data to {default_database_summary}."); + } + Ok(()) + } +}