396 lines
15 KiB
Rust
396 lines
15 KiB
Rust
// file: crates/ksp-store-lib/src/settings.rs
|
|
// version: 3
|
|
|
|
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 {
|
|
auto_migrate: bool,
|
|
migration_lock_timeout: std::time::Duration,
|
|
migration_timeout: std::time::Duration,
|
|
}
|
|
|
|
impl PostgresBootstrapSettings {
|
|
/// Creates explicit bootstrap behavior and migration deadlines.
|
|
#[must_use]
|
|
pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self {
|
|
return Self { auto_migrate, migration_lock_timeout, migration_timeout };
|
|
}
|
|
|
|
/// Returns whether pending KSP-owned migrations may be applied during Store opening.
|
|
#[must_use]
|
|
pub const fn auto_migrate(&self) -> bool {
|
|
return self.auto_migrate;
|
|
}
|
|
|
|
/// 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::new(
|
|
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<std::string::String>,
|
|
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", &"<redacted>")
|
|
.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;
|