// file: crates/ksp-store-lib/src/settings.rs // version: 4 const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000; const DEFAULT_MAX_CONNECTIONS: u32 = 8; const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS: u64 = 10_000; const DEFAULT_MIGRATION_TIMEOUT_MS: u64 = 30_000; const DEFAULT_POOL_CREATE_TIMEOUT_MS: u64 = 10_000; const DEFAULT_POOL_RECYCLE_TIMEOUT_MS: u64 = 5_000; const DEFAULT_POOL_WAIT_TIMEOUT_MS: u64 = 5_000; const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 5_000; const MAX_CONNECTIONS: u32 = 64; const MAX_CONNECT_TIMEOUT_MS: u64 = 60_000; const MAX_MIGRATION_LOCK_TIMEOUT_MS: u64 = 120_000; const MAX_MIGRATION_TIMEOUT_MS: u64 = 300_000; const MAX_POOL_CREATE_TIMEOUT_MS: u64 = 60_000; const MAX_POOL_RECYCLE_TIMEOUT_MS: u64 = 60_000; const MAX_POOL_WAIT_TIMEOUT_MS: u64 = 60_000; const MAX_SHUTDOWN_TIMEOUT_MS: u64 = 30_000; const MIN_CONNECTIONS: u32 = 1; const MIN_CONNECT_TIMEOUT_MS: u64 = 100; const MIN_MIGRATION_LOCK_TIMEOUT_MS: u64 = 100; const MIN_MIGRATION_TIMEOUT_MS: u64 = 1_000; const MIN_POOL_CREATE_TIMEOUT_MS: u64 = 100; const MIN_POOL_RECYCLE_TIMEOUT_MS: u64 = 100; const MIN_POOL_WAIT_TIMEOUT_MS: u64 = 100; const MIN_SHUTDOWN_TIMEOUT_MS: u64 = 100; /// Backend identity understood by the common Store runtime independently from compiled Cargo features. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum StoreBackendKind { /// Official PostgreSQL Store backend. Postgres, } impl StoreBackendKind { /// Returns the stable safe backend code used in diagnostics and configuration mapping. #[must_use] pub const fn code(&self) -> &'static str { return match self { Self::Postgres => "postgres", }; } } /// TLS policy accepted by the backend-neutral PostgreSQL settings surface. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[non_exhaustive] pub enum PostgresTlsMode { /// Connect without TLS. Disabled, /// Require TLS and verify both the certificate chain and requested server identity. VerifyFull, } /// Bounded PostgreSQL connection-pool settings owned by the Store facade. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct PostgresPoolSettings { connect_timeout: std::time::Duration, create_timeout: std::time::Duration, max_connections: u32, recycle_timeout: std::time::Duration, wait_timeout: std::time::Duration, } impl PostgresPoolSettings { /// Creates explicit PostgreSQL pool bounds without performing any I/O. #[must_use] pub const fn new( max_connections: u32, connect_timeout: std::time::Duration, wait_timeout: std::time::Duration, create_timeout: std::time::Duration, recycle_timeout: std::time::Duration, ) -> Self { return Self { connect_timeout, create_timeout, max_connections, recycle_timeout, wait_timeout }; } /// Returns the timeout for establishing one physical PostgreSQL connection. #[must_use] pub const fn connect_timeout(&self) -> std::time::Duration { return self.connect_timeout; } /// Returns the timeout for creating one pooled PostgreSQL object. #[must_use] pub const fn create_timeout(&self) -> std::time::Duration { return self.create_timeout; } /// Returns the maximum number of physical PostgreSQL connections owned by the pool. #[must_use] pub const fn max_connections(&self) -> u32 { return self.max_connections; } /// Returns the timeout for recycling one pooled PostgreSQL object. #[must_use] pub const fn recycle_timeout(&self) -> std::time::Duration { return self.recycle_timeout; } /// Returns the maximum time one acquisition can wait for pool capacity. #[must_use] pub const fn wait_timeout(&self) -> std::time::Duration { return self.wait_timeout; } /// Validates all pool bounds without opening a connection. pub fn validate(&self) -> ksp_store_api::Result<()> { if self.max_connections < MIN_CONNECTIONS || self.max_connections > MAX_CONNECTIONS { return std::result::Result::Err( ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL pool connection bound is invalid") .with_context("field", "postgres.pool.max_connections") .with_context("minimum", MIN_CONNECTIONS.to_string()) .with_context("maximum", MAX_CONNECTIONS.to_string()), ); } let connect_validation = validate_duration("postgres.pool.connect_timeout", self.connect_timeout, MIN_CONNECT_TIMEOUT_MS, MAX_CONNECT_TIMEOUT_MS); if let std::result::Result::Err(error) = connect_validation { return std::result::Result::Err(error); } let wait_validation = validate_duration("postgres.pool.wait_timeout", self.wait_timeout, MIN_POOL_WAIT_TIMEOUT_MS, MAX_POOL_WAIT_TIMEOUT_MS); if let std::result::Result::Err(error) = wait_validation { return std::result::Result::Err(error); } let create_validation = validate_duration("postgres.pool.create_timeout", self.create_timeout, MIN_POOL_CREATE_TIMEOUT_MS, MAX_POOL_CREATE_TIMEOUT_MS); if let std::result::Result::Err(error) = create_validation { return std::result::Result::Err(error); } let recycle_validation = validate_duration("postgres.pool.recycle_timeout", self.recycle_timeout, MIN_POOL_RECYCLE_TIMEOUT_MS, MAX_POOL_RECYCLE_TIMEOUT_MS); if let std::result::Result::Err(error) = recycle_validation { return std::result::Result::Err(error); } return std::result::Result::Ok(()); } } impl std::default::Default for PostgresPoolSettings { fn default() -> Self { return Self::new( DEFAULT_MAX_CONNECTIONS, std::time::Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS), std::time::Duration::from_millis(DEFAULT_POOL_WAIT_TIMEOUT_MS), std::time::Duration::from_millis(DEFAULT_POOL_CREATE_TIMEOUT_MS), std::time::Duration::from_millis(DEFAULT_POOL_RECYCLE_TIMEOUT_MS), ); } } /// Bounded PostgreSQL bootstrap settings owned by the Store facade. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct PostgresBootstrapSettings { migration_lock_timeout: std::time::Duration, migration_timeout: std::time::Duration, schema_autocreate: bool, schema_autoupdate: bool, } impl PostgresBootstrapSettings { /// Creates bootstrap settings using the legacy single migration switch for source compatibility. /// /// The supplied value is mapped to both schema auto-creation and schema auto-update. New code should prefer /// [`Self::with_schema_policy`] when these policies need to differ. #[must_use] pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self { return Self::with_schema_policy(auto_migrate, auto_migrate, migration_timeout, migration_lock_timeout); } /// Creates explicit schema creation/update policy and migration deadlines. #[must_use] pub const fn with_schema_policy( schema_autocreate: bool, schema_autoupdate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration, ) -> Self { return Self { migration_lock_timeout, migration_timeout, schema_autocreate, schema_autoupdate }; } /// Returns the legacy pending-migration switch, mapped to the schema auto-update policy. #[must_use] pub const fn auto_migrate(&self) -> bool { return self.schema_autoupdate; } /// Returns whether an absent KSP-managed schema may be created or adopted during Store opening. #[must_use] pub const fn schema_autocreate(&self) -> bool { return self.schema_autocreate; } /// Returns whether pending migrations and safe additive schema repairs may be applied during Store opening. #[must_use] pub const fn schema_autoupdate(&self) -> bool { return self.schema_autoupdate; } /// Returns the bounded wait allowed for the private PostgreSQL migration lock. #[must_use] pub const fn migration_lock_timeout(&self) -> std::time::Duration { return self.migration_lock_timeout; } /// Returns the bounded duration allowed for one migration/bootstrap run. #[must_use] pub const fn migration_timeout(&self) -> std::time::Duration { return self.migration_timeout; } /// Validates bootstrap and migration deadlines without contacting PostgreSQL. pub fn validate(&self) -> ksp_store_api::Result<()> { let migration_validation = validate_duration("postgres.bootstrap.migration_timeout", self.migration_timeout, MIN_MIGRATION_TIMEOUT_MS, MAX_MIGRATION_TIMEOUT_MS); if let std::result::Result::Err(error) = migration_validation { return std::result::Result::Err(error); } let lock_validation = validate_duration( "postgres.bootstrap.migration_lock_timeout", self.migration_lock_timeout, MIN_MIGRATION_LOCK_TIMEOUT_MS, MAX_MIGRATION_LOCK_TIMEOUT_MS, ); if let std::result::Result::Err(error) = lock_validation { return std::result::Result::Err(error); } return std::result::Result::Ok(()); } } impl std::default::Default for PostgresBootstrapSettings { fn default() -> Self { return Self::with_schema_policy( true, true, std::time::Duration::from_millis(DEFAULT_MIGRATION_TIMEOUT_MS), std::time::Duration::from_millis(DEFAULT_MIGRATION_LOCK_TIMEOUT_MS), ); } } /// PostgreSQL settings owned by the Store facade and independent from Config or backend implementation types. pub struct PostgresStoreSettings { bootstrap: PostgresBootstrapSettings, connection_uri: std::string::String, pool: PostgresPoolSettings, tls_mode: PostgresTlsMode, } impl PostgresStoreSettings { /// Creates PostgreSQL Store settings from an explicitly supplied sensitive connection URI and typed runtime bounds. #[must_use] pub fn new( connection_uri: impl std::convert::Into, pool: PostgresPoolSettings, tls_mode: PostgresTlsMode, bootstrap: PostgresBootstrapSettings, ) -> Self { return Self { bootstrap, connection_uri: connection_uri.into(), pool, tls_mode }; } /// Returns the PostgreSQL bootstrap settings without exposing the sensitive connection URI. #[must_use] pub const fn bootstrap(&self) -> PostgresBootstrapSettings { return self.bootstrap; } /// Returns the sensitive PostgreSQL connection URI only to the compiled backend bridge. #[cfg(feature = "postgres")] #[must_use] pub(crate) fn connection_uri(&self) -> &str { return self.connection_uri.as_str(); } /// Returns the PostgreSQL pool settings without exposing the sensitive connection URI. #[must_use] pub const fn pool(&self) -> PostgresPoolSettings { return self.pool; } /// Returns the selected PostgreSQL TLS policy without exposing the sensitive connection URI. #[must_use] pub const fn tls_mode(&self) -> PostgresTlsMode { return self.tls_mode; } /// Validates backend-neutral PostgreSQL settings without parsing the URI or performing I/O. pub fn validate(&self) -> ksp_store_api::Result<()> { if self.connection_uri.is_empty() { return std::result::Result::Err( ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL connection URI is required") .with_context("field", "postgres.connection_uri"), ); } let pool_validation = self.pool.validate(); if let std::result::Result::Err(error) = pool_validation { return std::result::Result::Err(error); } let bootstrap_validation = self.bootstrap.validate(); if let std::result::Result::Err(error) = bootstrap_validation { return std::result::Result::Err(error); } return std::result::Result::Ok(()); } } impl std::fmt::Debug for PostgresStoreSettings { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("PostgresStoreSettings") .field("connection_uri", &"") .field("pool", &self.pool) .field("tls_mode", &self.tls_mode) .field("bootstrap", &self.bootstrap) .finish(); } } /// Backend-specific settings selected through the common Store facade. #[derive(Debug)] #[non_exhaustive] pub enum StoreBackendSettings { /// Settings for the known PostgreSQL backend, whether or not its Cargo feature is compiled. Postgres(PostgresStoreSettings), } impl StoreBackendSettings { /// Returns the stable backend identity represented by these settings. #[must_use] pub const fn kind(&self) -> StoreBackendKind { return match self { Self::Postgres(_) => StoreBackendKind::Postgres, }; } /// Validates backend-specific settings without performing I/O. pub fn validate(&self) -> ksp_store_api::Result<()> { return match self { Self::Postgres(settings) => settings.validate(), }; } } /// Complete backend-neutral settings consumed by the common Store runtime facade. #[derive(Debug)] pub struct StoreSettings { backend: StoreBackendSettings, network: ksp_store_api::RawNetworkId, shutdown_timeout: std::time::Duration, } impl StoreSettings { /// Creates complete Store runtime settings for one explicit logical network, backend and shutdown bound. #[must_use] pub fn new(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self { return Self { backend, network, shutdown_timeout }; } /// Returns the selected backend settings. #[must_use] pub const fn backend(&self) -> &StoreBackendSettings { return &self.backend; } /// Returns the selected stable backend identity. #[must_use] pub const fn backend_kind(&self) -> StoreBackendKind { return self.backend.kind(); } /// Returns the single logical network bound to this Store instance. #[must_use] pub const fn network(&self) -> &ksp_store_api::RawNetworkId { return &self.network; } /// Returns the maximum duration allowed for explicit Store shutdown. #[must_use] pub const fn shutdown_timeout(&self) -> std::time::Duration { return self.shutdown_timeout; } /// Validates all backend-neutral Store settings before any backend I/O can start. pub fn validate(&self) -> ksp_store_api::Result<()> { let shutdown_validation = validate_duration("shutdown_timeout", self.shutdown_timeout, MIN_SHUTDOWN_TIMEOUT_MS, MAX_SHUTDOWN_TIMEOUT_MS); if let std::result::Result::Err(error) = shutdown_validation { return std::result::Result::Err(error); } let backend_validation = self.backend.validate(); if let std::result::Result::Err(error) = backend_validation { return std::result::Result::Err(error); } return std::result::Result::Ok(()); } } impl StoreSettings { /// Creates settings using the common default shutdown bound while keeping network and backend construction explicit. #[must_use] pub fn with_default_shutdown(network: ksp_store_api::RawNetworkId, backend: StoreBackendSettings) -> Self { return Self::new(network, backend, std::time::Duration::from_millis(DEFAULT_SHUTDOWN_TIMEOUT_MS)); } } fn validate_duration(field: &'static str, value: std::time::Duration, minimum_ms: u64, maximum_ms: u64) -> ksp_store_api::Result<()> { let minimum = std::time::Duration::from_millis(minimum_ms); let maximum = std::time::Duration::from_millis(maximum_ms); if value < minimum || value > maximum { return std::result::Result::Err( ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "Store runtime duration is outside the supported resource bound") .with_context("field", field) .with_context("minimum_ms", minimum_ms.to_string()) .with_context("maximum_ms", maximum_ms.to_string()), ); } return std::result::Result::Ok(()); } #[cfg(test)] #[path = "../unit_tests/settings.rs"] mod tests;