diff --git a/Cargo.toml b/Cargo.toml index 7d4c8be..50814bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 340 +# version: 341 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.3.2-pre.6" +version = "0.3.2-pre.7" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-store-lib/src/error.rs b/crates/ksp-store-lib/src/error.rs index 4d47e3f..f27f6fb 100644 --- a/crates/ksp-store-lib/src/error.rs +++ b/crates/ksp-store-lib/src/error.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/error.rs -// version: 3 +// version: 4 /// Error code reserved for operations attempted after a Store backend has entered its closed state. pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_closed"); @@ -11,6 +11,8 @@ pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_a pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid"); /// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details. pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed"); +/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely. +pub const ERROR_CODE_POSTGRES_HEALTH_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_health_failed"); /// Error code used when PostgreSQL migration/bootstrap execution fails without exposing server text or SQL. pub const ERROR_CODE_POSTGRES_MIGRATION_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_migration_failed"); /// Error code used when persisted PostgreSQL migration history diverges from the embedded immutable KSP history. diff --git a/crates/ksp-store-lib/src/health.rs b/crates/ksp-store-lib/src/health.rs new file mode 100644 index 0000000..8584125 --- /dev/null +++ b/crates/ksp-store-lib/src/health.rs @@ -0,0 +1,132 @@ +// file: crates/ksp-store-lib/src/health.rs +// version: 1 + +/// Portable Store health state independent from the selected physical backend. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub enum StoreHealthState { + /// The selected Store backend answered the bounded readiness probe and its migration foundation is current. + Ready, + /// The Store instance exists but its latest bounded readiness probe could not prove readiness. + NotReady, +} + +/// Safe synchronous Store runtime snapshot without performing backend I/O. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StoreRuntimeSnapshot { + backend_kind: crate::StoreBackendKind, + network: ksp_store_api::RawNetworkId, + pool_available: u32, + pool_capacity: u32, + pool_size: u32, + pool_waiting: u32, +} + +impl StoreRuntimeSnapshot { + /// Creates one portable runtime projection from backend-owned safe counters. + #[must_use] + pub(crate) fn new( + backend_kind: crate::StoreBackendKind, + network: ksp_store_api::RawNetworkId, + pool_capacity: u32, + pool_size: u32, + pool_available: u32, + pool_waiting: u32, + ) -> Self { + return Self { backend_kind, network, pool_available, pool_capacity, pool_size, pool_waiting }; + } + + /// Returns the selected backend identity. + #[must_use] + pub const fn backend_kind(&self) -> crate::StoreBackendKind { + return self.backend_kind; + } + + /// Returns the one logical network bound to this Store instance. + #[must_use] + pub const fn network(&self) -> &ksp_store_api::RawNetworkId { + return &self.network; + } + + /// Returns the number of currently available pooled backend objects. + #[must_use] + pub const fn pool_available(&self) -> u32 { + return self.pool_available; + } + + /// Returns the configured maximum pooled backend object count. + #[must_use] + pub const fn pool_capacity(&self) -> u32 { + return self.pool_capacity; + } + + /// Returns the current pooled backend object count. + #[must_use] + pub const fn pool_size(&self) -> u32 { + return self.pool_size; + } + + /// Returns the number of tasks currently waiting for a pooled backend object. + #[must_use] + pub const fn pool_waiting(&self) -> u32 { + return self.pool_waiting; + } +} + +/// Portable Store readiness projection containing only safe runtime and migration diagnostics. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StoreHealthSnapshot { + last_error_code: std::option::Option, + migration_version: std::option::Option, + pending_migration_count: u32, + runtime: StoreRuntimeSnapshot, + state: StoreHealthState, +} + +impl StoreHealthSnapshot { + /// Creates one safe health projection from already classified backend diagnostics. + #[must_use] + pub(crate) fn new( + state: StoreHealthState, + runtime: StoreRuntimeSnapshot, + migration_version: std::option::Option, + pending_migration_count: u32, + last_error_code: std::option::Option, + ) -> Self { + return Self { last_error_code, migration_version, pending_migration_count, runtime, state }; + } + + /// Returns the latest safe error code when readiness could not be proven. + #[must_use] + pub const fn last_error_code(&self) -> std::option::Option { + return self.last_error_code; + } + + /// Returns the migration version observed by the readiness probe when available. + #[must_use] + pub const fn migration_version(&self) -> std::option::Option { + return self.migration_version; + } + + /// Returns the number of embedded migrations newer than the observed applied version. + #[must_use] + pub const fn pending_migration_count(&self) -> u32 { + return self.pending_migration_count; + } + + /// Returns the safe synchronous runtime projection captured for this health probe. + #[must_use] + pub const fn runtime(&self) -> &StoreRuntimeSnapshot { + return &self.runtime; + } + + /// Returns whether this probe proved the Store ready. + #[must_use] + pub const fn state(&self) -> StoreHealthState { + return self.state; + } +} + +#[cfg(test)] +#[path = "../unit_tests/health.rs"] +mod tests; diff --git a/crates/ksp-store-lib/src/lib.rs b/crates/ksp-store-lib/src/lib.rs index f7d6e17..d4302ef 100644 --- a/crates/ksp-store-lib/src/lib.rs +++ b/crates/ksp-store-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/lib.rs -// version: 5 +// version: 6 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -7,9 +7,10 @@ //! Common backend-neutral Store runtime facade for KSP. //! -//! `0.3.2-pre.006` materializes the physical PostgreSQL runtime plus its private -//! migration/bootstrap foundation: bounded pool, explicit TLS, versioned SHA-256 -//! history and no business persistence schema. +//! `0.3.2-pre.007` closes the physical PostgreSQL runtime composition with a +//! portable safe runtime snapshot and lightweight health/readiness projection, +//! while retaining the private migration/bootstrap foundation and no business +//! persistence schema. //! //! The default `postgres` feature compiles the official PostgreSQL backend as //! an optional implementation dependency. No backend implementation type is @@ -17,6 +18,7 @@ mod constants; mod error; +mod health; mod settings; mod store; @@ -30,6 +32,8 @@ pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED; pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID; /// Error code used when PostgreSQL physical connection establishment fails. pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED; +/// Error code used when a lightweight PostgreSQL health/readiness probe fails safely. +pub use self::error::ERROR_CODE_POSTGRES_HEALTH_FAILED; /// Error code used when PostgreSQL migration/bootstrap execution fails safely. pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED; /// Error code used when PostgreSQL migration history diverges from the embedded immutable KSP history. @@ -44,6 +48,12 @@ pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED; pub use self::error::ERROR_CODE_SETTINGS_INVALID; /// Error code used when explicit Store shutdown exceeds its configured deadline. pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT; +/// Portable Store health/readiness projection containing only safe diagnostics. +pub use self::health::StoreHealthSnapshot; +/// Portable Store health state independent from physical backend types. +pub use self::health::StoreHealthState; +/// Safe synchronous Store runtime snapshot containing backend-neutral pool counters. +pub use self::health::StoreRuntimeSnapshot; /// Bounded PostgreSQL bootstrap and migration settings owned by the Store facade. pub use self::settings::PostgresBootstrapSettings; /// Bounded PostgreSQL connection-pool settings owned by the Store facade. diff --git a/crates/ksp-store-lib/src/store.rs b/crates/ksp-store-lib/src/store.rs index 852d8f7..17327f4 100644 --- a/crates/ksp-store-lib/src/store.rs +++ b/crates/ksp-store-lib/src/store.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/src/store.rs -// version: 3 +// version: 4 /// Opaque common Store runtime facade. /// @@ -37,6 +37,44 @@ impl Store { }; } + /// Returns a safe synchronous runtime snapshot without performing backend I/O. + #[must_use] + pub fn runtime_snapshot(&self) -> crate::StoreRuntimeSnapshot { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => map_postgres_runtime_snapshot(backend.runtime_snapshot(), self.backend_kind, self.network.clone()), + }; + } + #[cfg(not(feature = "postgres"))] + { + return crate::StoreRuntimeSnapshot::new(self.backend_kind, self.network.clone(), 0, 0, 0, 0); + } + } + + /// Runs the selected backend's lightweight bounded readiness probe and returns only portable redacted diagnostics. + pub async fn health(&self) -> crate::StoreHealthSnapshot { + #[cfg(feature = "postgres")] + { + return match &self.runtime { + StoreRuntime::Postgres(backend) => { + let snapshot = backend.health().await; + map_postgres_health_snapshot(snapshot, self.backend_kind, self.network.clone()) + }, + }; + } + #[cfg(not(feature = "postgres"))] + { + return crate::StoreHealthSnapshot::new( + crate::StoreHealthState::NotReady, + self.runtime_snapshot(), + std::option::Option::None, + 0, + std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED), + ); + } + } + /// Explicitly closes the Store runtime, consumes its facade handle and applies the configured bounded shutdown deadline. pub async fn close(self) -> ksp_store_api::Result<()> { let backend_kind = self.backend_kind; @@ -136,9 +174,47 @@ async fn open_postgres( #[cfg(feature = "postgres")] fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error { - let code = match error.kind() { + let code = postgres_error_code(error.kind()); + return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed") + .with_context("backend", backend_kind.code()) + .with_context("network", network) + .with_context("phase", error.phase()); +} + +#[cfg(feature = "postgres")] +fn map_postgres_runtime_snapshot( + snapshot: ksp_store_postgres_lib::PostgresBackendRuntimeSnapshot, + backend_kind: crate::StoreBackendKind, + network: ksp_store_api::RawNetworkId, +) -> crate::StoreRuntimeSnapshot { + return crate::StoreRuntimeSnapshot::new( + backend_kind, + network, + snapshot.pool_capacity(), + snapshot.pool_size(), + snapshot.pool_available(), + snapshot.pool_waiting(), + ); +} + +#[cfg(feature = "postgres")] +fn map_postgres_health_snapshot( + snapshot: ksp_store_postgres_lib::PostgresBackendHealthSnapshot, + backend_kind: crate::StoreBackendKind, + network: ksp_store_api::RawNetworkId, +) -> crate::StoreHealthSnapshot { + let state = if snapshot.is_ready() { crate::StoreHealthState::Ready } else { crate::StoreHealthState::NotReady }; + let error_code = snapshot.error_kind().map(|kind| return postgres_error_code(kind)); + let runtime = map_postgres_runtime_snapshot(snapshot.runtime().clone(), backend_kind, network); + return crate::StoreHealthSnapshot::new(state, runtime, snapshot.migration_version(), snapshot.pending_migration_count(), error_code); +} + +#[cfg(feature = "postgres")] +fn postgres_error_code(kind: ksp_store_postgres_lib::PostgresBackendErrorKind) -> ksp_store_api::ErrorCode { + return match kind { ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID, ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED, + ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => crate::ERROR_CODE_POSTGRES_HEALTH_FAILED, ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT, ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED, ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH, @@ -147,10 +223,6 @@ fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backe ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED, _ => crate::ERROR_CODE_BACKEND_OPEN_FAILED, }; - return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed") - .with_context("backend", backend_kind.code()) - .with_context("network", network) - .with_context("phase", error.phase()); } #[cfg(not(feature = "postgres"))] diff --git a/crates/ksp-store-lib/tests/dependency_boundary.rs b/crates/ksp-store-lib/tests/dependency_boundary.rs index ddd0129..26955c6 100644 --- a/crates/ksp-store-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/dependency_boundary.rs -// version: 5 +// version: 6 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -34,6 +34,8 @@ fn pre_005_manifest_keeps_backend_physical_dependencies_out_of_facade() { fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() { let crate_root = include_str!("../src/lib.rs"); assert!(crate_root.contains("pub use self::settings::StoreSettings;")); + assert!(crate_root.contains("pub use self::health::StoreHealthSnapshot;")); + assert!(crate_root.contains("pub use self::health::StoreRuntimeSnapshot;")); assert!(crate_root.contains("pub use self::store::Store;")); assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;")); assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;")); @@ -50,7 +52,7 @@ fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() { ] { assert!(!crate_root.contains(forbidden), "forbidden physical backend facade surface detected: {forbidden}"); } - let production = format!("{}\n{}", include_str!("../src/settings.rs"), include_str!("../src/store.rs")); + let production = format!("{}\n{}\n{}", include_str!("../src/health.rs"), include_str!("../src/settings.rs"), include_str!("../src/store.rs")); for forbidden in ["ksp_config_lib", "std::env", "dotenv", "PGHOST", "PGPORT", "PGUSER", "PGPASSWORD", ".pgpass", "tokio_postgres", "deadpool_postgres"] { assert!(!production.contains(forbidden), "forbidden Store facade ownership bypass detected: {forbidden}"); } diff --git a/crates/ksp-store-lib/tests/public_api.rs b/crates/ksp-store-lib/tests/public_api.rs index c58a900..58b2bd8 100644 --- a/crates/ksp-store-lib/tests/public_api.rs +++ b/crates/ksp-store-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-lib/tests/public_api.rs -// version: 4 +// version: 5 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -25,6 +25,8 @@ fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() { assert!(settings.validate().is_ok()); let _open = ksp_store_lib::Store::open; let _close = ksp_store_lib::Store::close; + let _runtime_snapshot = ksp_store_lib::Store::runtime_snapshot; + let _health = ksp_store_lib::Store::health; return; } @@ -36,6 +38,7 @@ fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() { assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED.code(), "backend_open_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID.code(), "postgres_config_invalid"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_CONNECT_FAILED.code(), "postgres_connect_failed"); + assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_HEALTH_FAILED.code(), "postgres_health_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_FAILED.code(), "postgres_migration_failed"); assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH.code(), "postgres_migration_mismatch"); @@ -55,3 +58,11 @@ fn pre_003_facade_reexports_backend_agnostic_store_api_types() { let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(()); return; } + +#[test] +fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts() { + let _state = std::mem::size_of::>(); + let _health = std::mem::size_of::>(); + let _runtime = std::mem::size_of::>(); + return; +} diff --git a/crates/ksp-store-lib/unit_tests/health.rs b/crates/ksp-store-lib/unit_tests/health.rs new file mode 100644 index 0000000..303449e --- /dev/null +++ b/crates/ksp-store-lib/unit_tests/health.rs @@ -0,0 +1,45 @@ +// file: crates/ksp-store-lib/unit_tests/health.rs +// version: 1 + +fn network() -> ksp_store_api::RawNetworkId { + return match ksp_store_api::RawNetworkId::new("devnet") { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("valid Store health test network rejected: {error:?}"), + }; +} + +#[test] +fn runtime_snapshot_is_backend_neutral_and_contains_only_safe_counts() { + let runtime = crate::StoreRuntimeSnapshot::new(crate::StoreBackendKind::Postgres, network(), 8, 3, 2, 1); + assert_eq!(runtime.backend_kind(), crate::StoreBackendKind::Postgres); + assert_eq!(runtime.network().as_str(), "devnet"); + assert_eq!(runtime.pool_capacity(), 8); + assert_eq!(runtime.pool_size(), 3); + assert_eq!(runtime.pool_available(), 2); + assert_eq!(runtime.pool_waiting(), 1); + let rendered = format!("{runtime:?}"); + for forbidden in ["postgresql://", "password", "username", "database", "SELECT ", "ksp_store_schema_migrations"] { + assert!(!rendered.contains(forbidden), "unsafe runtime snapshot material detected: {forbidden}"); + } + return; +} + +#[test] +fn health_snapshot_carries_only_safe_state_migration_and_error_code() { + let runtime = crate::StoreRuntimeSnapshot::new(crate::StoreBackendKind::Postgres, network(), 8, 1, 1, 0); + let ready = crate::StoreHealthSnapshot::new(crate::StoreHealthState::Ready, runtime.clone(), std::option::Option::Some(0), 0, std::option::Option::None); + assert_eq!(ready.state(), crate::StoreHealthState::Ready); + assert_eq!(ready.migration_version(), std::option::Option::Some(0)); + assert_eq!(ready.pending_migration_count(), 0); + assert_eq!(ready.last_error_code(), std::option::Option::None); + let not_ready = crate::StoreHealthSnapshot::new( + crate::StoreHealthState::NotReady, + runtime, + std::option::Option::None, + 0, + std::option::Option::Some(crate::ERROR_CODE_POSTGRES_HEALTH_FAILED), + ); + assert_eq!(not_ready.state(), crate::StoreHealthState::NotReady); + assert_eq!(not_ready.last_error_code(), std::option::Option::Some(crate::ERROR_CODE_POSTGRES_HEALTH_FAILED)); + return; +} diff --git a/crates/ksp-store-postgres-lib/src/error.rs b/crates/ksp-store-postgres-lib/src/error.rs index 052f05f..4a267bb 100644 --- a/crates/ksp-store-postgres-lib/src/error.rs +++ b/crates/ksp-store-postgres-lib/src/error.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/src/error.rs -// version: 2 +// version: 3 /// Safe backend-local classification used by the Store facade for stable error mapping. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -11,6 +11,8 @@ pub enum PostgresBackendErrorKind { ConnectFailed, /// A bounded pool wait, create or recycle operation reached its deadline. PoolTimeout, + /// A lightweight PostgreSQL health/readiness probe failed without exposing server text or SQL. + HealthFailed, /// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL. MigrationFailed, /// Applied PostgreSQL migration history diverges from the embedded immutable KSP history. diff --git a/crates/ksp-store-postgres-lib/src/health.rs b/crates/ksp-store-postgres-lib/src/health.rs new file mode 100644 index 0000000..52cd763 --- /dev/null +++ b/crates/ksp-store-postgres-lib/src/health.rs @@ -0,0 +1,101 @@ +// file: crates/ksp-store-postgres-lib/src/health.rs +// version: 1 + +const DEFAULT_HEALTH_TIMEOUT_MS: u64 = 5_000; +const MIGRATION_VERSION_SQL: &str = "SELECT COALESCE(MAX(version), -1)::BIGINT FROM ksp_store_schema_migrations"; +const READINESS_SQL: &str = "SELECT 1::BIGINT"; + +/// Runs one bounded lightweight readiness probe and returns only safe classified diagnostics. +pub(crate) async fn probe_health(pool: &deadpool_postgres::Pool) -> crate::PostgresBackendHealthSnapshot { + let runtime = crate::runtime_snapshot_from_status(pool.status()); + let timeouts = pool.timeouts(); + let timeout = match timeouts.wait { + std::option::Option::Some(value) => value, + std::option::Option::None => std::time::Duration::from_millis(DEFAULT_HEALTH_TIMEOUT_MS), + }; + let bounded = tokio::time::timeout(timeout, probe_health_inner(pool, runtime.clone())).await; + return match bounded { + std::result::Result::Ok(snapshot) => snapshot, + std::result::Result::Err(_) => { + crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed) + }, + }; +} + +async fn probe_health_inner(pool: &deadpool_postgres::Pool, runtime: crate::PostgresBackendRuntimeSnapshot) -> crate::PostgresBackendHealthSnapshot { + let client_result = pool.get().await; + let client = match client_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + let classified = crate::map_pool_error(error); + return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, classified.kind()); + }, + }; + let readiness_result = client.query_one(READINESS_SQL, &[]).await; + let readiness_row = match readiness_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed); + }, + }; + let readiness_value = match readiness_row.try_get::(0) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed); + }, + }; + if readiness_value != 1 { + return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed); + } + let migration_result = client.query_one(MIGRATION_VERSION_SQL, &[]).await; + let migration_row = match migration_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed); + }, + }; + let migration_value = migration_row.try_get::(0); + let migration_version = match migration_value { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return crate::PostgresBackendHealthSnapshot::not_ready(runtime, std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed); + }, + }; + let expected = crate::current_migration_version(); + if migration_version < 0 || migration_version < expected { + let observed = nonnegative_version(migration_version); + let pending = pending_migration_count(migration_version, expected); + return crate::PostgresBackendHealthSnapshot::not_ready(runtime, observed, pending, crate::PostgresBackendErrorKind::MigrationMismatch); + } + if migration_version > expected { + return crate::PostgresBackendHealthSnapshot::not_ready( + runtime, + nonnegative_version(migration_version), + 0, + crate::PostgresBackendErrorKind::SchemaNewer, + ); + } + return crate::PostgresBackendHealthSnapshot::ready(runtime, migration_version as u64, 0); +} + +fn nonnegative_version(value: i64) -> std::option::Option { + if value < 0 { + return std::option::Option::None; + } + return std::option::Option::Some(value as u64); +} + +fn pending_migration_count(observed: i64, expected: i64) -> u32 { + if observed >= expected { + return 0; + } + let delta = expected.saturating_sub(observed); + if delta > i64::from(u32::MAX) { + return u32::MAX; + } + return delta as u32; +} + +#[cfg(test)] +#[path = "../unit_tests/health.rs"] +mod tests; diff --git a/crates/ksp-store-postgres-lib/src/lib.rs b/crates/ksp-store-postgres-lib/src/lib.rs index 17bd5f6..dbd3aa0 100644 --- a/crates/ksp-store-postgres-lib/src/lib.rs +++ b/crates/ksp-store-postgres-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/src/lib.rs -// version: 4 +// version: 5 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -7,9 +7,10 @@ //! Official PostgreSQL backend implementation for KSP Store. //! -//! `0.3.2-pre.006` owns the physical `tokio-postgres` connection, bounded -//! Deadpool pool, explicit Rustls TLS policy and private KSP migration/bootstrap -//! engine. Business persistence remains absent from this foundation release. +//! `0.3.2-pre.007` owns the physical `tokio-postgres` connection, bounded +//! Deadpool pool, explicit Rustls TLS policy, private KSP migration/bootstrap +//! engine and safe lightweight health/readiness probe. Business persistence +//! remains absent from this foundation release. //! //! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The //! common facade consumes only this crate's narrow backend bridge and never @@ -17,6 +18,7 @@ mod constants; mod error; +mod health; mod migration; mod runtime; @@ -26,6 +28,10 @@ pub use self::error::PostgresBackendError; pub use self::error::PostgresBackendErrorKind; /// Opaque physical PostgreSQL backend owning its connection pool. pub use self::runtime::PostgresBackend; +/// Safe PostgreSQL readiness projection returned through the backend bridge. +pub use self::runtime::PostgresBackendHealthSnapshot; +/// Safe PostgreSQL pool counter projection returned through the backend bridge. +pub use self::runtime::PostgresBackendRuntimeSnapshot; /// Physical PostgreSQL settings bridge consumed only by the backend crate. pub use self::runtime::PostgresBackendSettings; /// TLS mode accepted by the physical PostgreSQL settings bridge. @@ -33,7 +39,15 @@ pub use self::runtime::PostgresBackendTlsMode; /// Crate-owned tracing target for PostgreSQL backend behavior. pub(crate) use self::constants::TRACING_TARGET; +/// Private bounded health probe consumed by the physical backend runtime. +pub(crate) use self::health::probe_health; /// Private migration/bootstrap runner consumed by the physical backend runtime. pub(crate) use self::migration::bootstrap; +/// Current embedded migration version consumed by the private health probe. +pub(crate) use self::migration::current_migration_version; +/// Private Deadpool error mapper shared with the health probe. +pub(crate) use self::runtime::map_pool_error; +/// Private Deadpool status projector shared with the health probe. +pub(crate) use self::runtime::runtime_snapshot_from_status; const _: &str = crate::TRACING_TARGET; diff --git a/crates/ksp-store-postgres-lib/src/migration.rs b/crates/ksp-store-postgres-lib/src/migration.rs index 5ee7ef5..308cad4 100644 --- a/crates/ksp-store-postgres-lib/src/migration.rs +++ b/crates/ksp-store-postgres-lib/src/migration.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/src/migration.rs -// version: 1 +// version: 2 use sha2::Digest; // rust-rules: trait-import @@ -40,6 +40,12 @@ struct AppliedMigration { version: i64, } +/// Returns the latest migration version embedded by this backend runtime. +#[must_use] +pub(crate) const fn current_migration_version() -> i64 { + return BOOTSTRAP_MIGRATION_VERSION; +} + /// Runs the private bounded PostgreSQL schema bootstrap on one dedicated pooled client. pub(crate) async fn bootstrap( client: &mut deadpool_postgres::Client, diff --git a/crates/ksp-store-postgres-lib/src/runtime.rs b/crates/ksp-store-postgres-lib/src/runtime.rs index 389ff44..64b6c3d 100644 --- a/crates/ksp-store-postgres-lib/src/runtime.rs +++ b/crates/ksp-store-postgres-lib/src/runtime.rs @@ -1,10 +1,122 @@ // file: crates/ksp-store-postgres-lib/src/runtime.rs -// version: 2 +// version: 3 const APPLICATION_NAME: &str = "ksp-store"; const MAX_CONNECTION_URI_BYTES: usize = 4_096; const SHUTDOWN_POLL_INTERVAL_MS: u64 = 10; +/// Safe PostgreSQL runtime counters exported only through the narrow backend bridge. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PostgresBackendRuntimeSnapshot { + pool_available: u32, + pool_capacity: u32, + pool_size: u32, + pool_waiting: u32, +} + +impl PostgresBackendRuntimeSnapshot { + /// Creates a safe pool-counter projection from already bounded values. + #[must_use] + pub(crate) const fn new(pool_capacity: u32, pool_size: u32, pool_available: u32, pool_waiting: u32) -> Self { + return Self { pool_available, pool_capacity, pool_size, pool_waiting }; + } + + /// Returns the number of currently available pooled PostgreSQL clients. + #[must_use] + pub const fn pool_available(&self) -> u32 { + return self.pool_available; + } + + /// Returns the configured maximum pooled PostgreSQL client count. + #[must_use] + pub const fn pool_capacity(&self) -> u32 { + return self.pool_capacity; + } + + /// Returns the current pooled PostgreSQL client count. + #[must_use] + pub const fn pool_size(&self) -> u32 { + return self.pool_size; + } + + /// Returns the number of tasks currently waiting for a pooled PostgreSQL client. + #[must_use] + pub const fn pool_waiting(&self) -> u32 { + return self.pool_waiting; + } +} + +/// Safe PostgreSQL readiness projection returned to the common Store facade. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PostgresBackendHealthSnapshot { + error_kind: std::option::Option, + migration_version: std::option::Option, + pending_migration_count: u32, + ready: bool, + runtime: PostgresBackendRuntimeSnapshot, +} + +impl PostgresBackendHealthSnapshot { + /// Creates one successful safe readiness projection. + #[must_use] + pub(crate) const fn ready(runtime: PostgresBackendRuntimeSnapshot, migration_version: u64, pending_migration_count: u32) -> Self { + return Self { + error_kind: std::option::Option::None, + migration_version: std::option::Option::Some(migration_version), + pending_migration_count, + ready: true, + runtime, + }; + } + + /// Creates one failed safe readiness projection from a classified backend error. + #[must_use] + pub(crate) const fn not_ready( + runtime: PostgresBackendRuntimeSnapshot, + migration_version: std::option::Option, + pending_migration_count: u32, + error_kind: crate::PostgresBackendErrorKind, + ) -> Self { + return Self { + error_kind: std::option::Option::Some(error_kind), + migration_version, + pending_migration_count, + ready: false, + runtime, + }; + } + + /// Returns the safe backend error classification when readiness could not be proven. + #[must_use] + pub const fn error_kind(&self) -> std::option::Option { + return self.error_kind; + } + + /// Returns whether the latest bounded PostgreSQL probe proved readiness. + #[must_use] + pub const fn is_ready(&self) -> bool { + return self.ready; + } + + /// Returns the migration version observed by the readiness probe when available. + #[must_use] + pub const fn migration_version(&self) -> std::option::Option { + return self.migration_version; + } + + /// Returns the number of embedded migrations newer than the observed applied version. + #[must_use] + pub const fn pending_migration_count(&self) -> u32 { + return self.pending_migration_count; + } + + /// Returns the safe PostgreSQL pool counters captured for this probe. + #[must_use] + pub const fn runtime(&self) -> &PostgresBackendRuntimeSnapshot { + return &self.runtime; + } +} + /// TLS mode accepted by the physical PostgreSQL backend bridge. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[non_exhaustive] @@ -165,6 +277,17 @@ impl PostgresBackend { return std::result::Result::Ok(Self { network: settings.network, pool }); } + /// Returns a safe synchronous snapshot of bounded pool counters without performing PostgreSQL I/O. + #[must_use] + pub fn runtime_snapshot(&self) -> PostgresBackendRuntimeSnapshot { + return runtime_snapshot_from_status(self.pool.status()); + } + + /// Runs a bounded lightweight PostgreSQL readiness probe and returns only safe classified diagnostics. + pub async fn health(&self) -> PostgresBackendHealthSnapshot { + return crate::probe_health(&self.pool).await; + } + /// Explicitly closes the pool and waits for all owned pooled objects to drain inside the supplied bound. pub async fn close(self, timeout: std::time::Duration) -> std::result::Result<(), crate::PostgresBackendError> { self.pool.close(); @@ -304,7 +427,8 @@ fn build_verified_tls() -> std::result::Result crate::PostgresBackendError { +/// Maps one Deadpool acquisition error into a redacted backend classification. +pub(crate) fn map_pool_error(error: deadpool_postgres::PoolError) -> crate::PostgresBackendError { return match error { deadpool_postgres::PoolError::Timeout(timeout_type) => crate::PostgresBackendError::new( crate::PostgresBackendErrorKind::PoolTimeout, @@ -321,6 +445,23 @@ fn map_pool_error(error: deadpool_postgres::PoolError) -> crate::PostgresBackend }; } +/// Converts Deadpool status into a bounded safe backend runtime projection. +pub(crate) fn runtime_snapshot_from_status(status: deadpool_postgres::Status) -> PostgresBackendRuntimeSnapshot { + return PostgresBackendRuntimeSnapshot::new( + bounded_count(status.max_size), + bounded_count(status.size), + bounded_count(status.available), + bounded_count(status.waiting), + ); +} + +fn bounded_count(value: usize) -> u32 { + if value > u32::MAX as usize { + return u32::MAX; + } + return value as u32; +} + #[cfg(test)] #[path = "../unit_tests/runtime.rs"] mod tests; diff --git a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs index eb00354..fbdcdc3 100644 --- a/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs +++ b/crates/ksp-store-postgres-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs -// version: 4 +// version: 5 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -33,6 +33,7 @@ fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_faca fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() { let crate_root = include_str!("../src/lib.rs"); assert!(crate_root.contains("mod error;")); + assert!(crate_root.contains("mod health;")); assert!(crate_root.contains("mod migration;")); assert!(crate_root.contains("mod runtime;")); assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;")); @@ -68,3 +69,16 @@ fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() } return; } + +#[test] +fn pre_007_health_probe_remains_foundation_only_and_private_sql() { + let health = include_str!("../src/health.rs"); + assert!(health.contains("SELECT 1::BIGINT")); + assert!(health.contains("ksp_store_schema_migrations")); + for forbidden in + ["RawTransaction", "RawAccountState", "raw_transaction", "raw_account", "CORE", "DECODE", "SPECIALIZED", "std::env", "dotenv", "KSP_SECRET_"] + { + assert!(!health.contains(forbidden), "forbidden health ownership/scope content detected: {forbidden}"); + } + return; +} diff --git a/crates/ksp-store-postgres-lib/tests/public_api.rs b/crates/ksp-store-postgres-lib/tests/public_api.rs index 751ddf4..73678f0 100644 --- a/crates/ksp-store-postgres-lib/tests/public_api.rs +++ b/crates/ksp-store-postgres-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-store-postgres-lib/tests/public_api.rs -// version: 2 +// version: 3 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -39,12 +39,22 @@ fn pre_005_backend_error_projection_is_safe_and_static() { ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid, ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed, ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout, + ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed, ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed, ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch, ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer, ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout, ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed, ]; - assert_eq!(kinds.len(), 8); + assert_eq!(kinds.len(), 9); + return; +} + +#[test] +fn pre_007_backend_health_bridge_exposes_only_safe_snapshot_types() { + let _runtime = std::mem::size_of::>(); + let _health = std::mem::size_of::>(); + let _runtime_snapshot = ksp_store_postgres_lib::PostgresBackend::runtime_snapshot; + let _health_probe = ksp_store_postgres_lib::PostgresBackend::health; return; } diff --git a/crates/ksp-store-postgres-lib/unit_tests/health.rs b/crates/ksp-store-postgres-lib/unit_tests/health.rs new file mode 100644 index 0000000..c597e9d --- /dev/null +++ b/crates/ksp-store-postgres-lib/unit_tests/health.rs @@ -0,0 +1,42 @@ +// file: crates/ksp-store-postgres-lib/unit_tests/health.rs +// version: 1 + +#[test] +fn pool_status_projection_is_bounded_and_contains_no_physical_handle() { + let status = deadpool_postgres::Status { max_size: 8, size: 3, available: 2, waiting: 1 }; + let snapshot = crate::runtime_snapshot_from_status(status); + assert_eq!(snapshot.pool_capacity(), 8); + assert_eq!(snapshot.pool_size(), 3); + assert_eq!(snapshot.pool_available(), 2); + assert_eq!(snapshot.pool_waiting(), 1); + return; +} + +#[test] +fn health_snapshot_distinguishes_ready_and_safe_failure_without_server_text() { + let status = deadpool_postgres::Status { max_size: 8, size: 1, available: 1, waiting: 0 }; + let runtime = crate::runtime_snapshot_from_status(status); + let ready = crate::PostgresBackendHealthSnapshot::ready(runtime, 0, 0); + assert!(ready.is_ready()); + assert_eq!(ready.migration_version(), std::option::Option::Some(0)); + assert_eq!(ready.error_kind(), std::option::Option::None); + let failed = + crate::PostgresBackendHealthSnapshot::not_ready(ready.runtime().clone(), std::option::Option::None, 0, crate::PostgresBackendErrorKind::HealthFailed); + assert!(!failed.is_ready()); + assert_eq!(failed.error_kind(), std::option::Option::Some(crate::PostgresBackendErrorKind::HealthFailed)); + let rendered = format!("{failed:?}"); + for forbidden in ["postgresql://", "SELECT ", "ksp_store_schema_migrations", "password", "server error"] { + assert!(!rendered.contains(forbidden), "unsafe backend health material detected: {forbidden}"); + } + return; +} + +#[test] +fn migration_health_helpers_keep_unknown_and_pending_counts_safe() { + assert_eq!(super::nonnegative_version(-1), std::option::Option::None); + assert_eq!(super::nonnegative_version(0), std::option::Option::Some(0)); + assert_eq!(super::pending_migration_count(-1, 0), 1); + assert_eq!(super::pending_migration_count(0, 0), 0); + assert_eq!(super::pending_migration_count(1, 0), 0); + return; +} diff --git a/deltas/0.3.2/pre.007.md b/deltas/0.3.2/pre.007.md new file mode 100644 index 0000000..11f8099 --- /dev/null +++ b/deltas/0.3.2/pre.007.md @@ -0,0 +1,165 @@ + + + +# Delta `0.3.2-pre.007` — composition runtime et health/readiness portable + +## 1. Base + +Base exacte : `0.3.2-pre.006`. + +Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-postgres-lib`, tests `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et graphes directs passent. + +## 2. Objet + +Fermer la composition runtime de la fondation PostgreSQL sans persistence métier : + +```text +StoreSettings + -> Store::open + -> PostgresBackend + -> connexion/pool/TLS + -> bootstrap/history + -> Store prêt + +Store::runtime_snapshot() + -> projection synchrone sans I/O + +Store::health().await + -> readiness PostgreSQL légère et bornée + -> projection portable/redacted +``` + +## 3. Surface façade + +`ksp-store-lib` ajoute : + +```text +StoreHealthState::{Ready, NotReady} +StoreRuntimeSnapshot +StoreHealthSnapshot +Store::runtime_snapshot() +Store::health().await +store.postgres_health_failed +``` + +`StoreRuntimeSnapshot` expose uniquement : + +```text +backend kind +network logique +pool capacity +pool size +pool available +pool waiting +``` + +`StoreHealthSnapshot` ajoute : + +```text +state +migration version optionnelle +pending migration count +last safe ErrorCode optionnel +``` + +La façade atteint 84 exports crate-root : 60 réexports `ksp-store-api` et 24 éléments runtime Store. + +## 4. Bridge backend + +`ksp-store-postgres-lib` ajoute uniquement les projections safe nécessaires au bridge : + +```text +PostgresBackendRuntimeSnapshot +PostgresBackendHealthSnapshot +``` + +Aucun `Pool`, `Client`, `Row`, `Statement`, URI, host, user, database, SQL ou texte serveur n'est exposé par ces snapshots. + +Les compteurs Deadpool `max_size/size/available/waiting` sont saturés en `u32` avant de traverser le bridge. + +## 5. Probe health + +Le probe PostgreSQL reste privé au backend et exécute uniquement : + +```text +acquisition Deadpool bornée +SELECT 1 +lecture de MAX(version) dans la metadata privée +``` + +La deadline globale réutilise le `wait_timeout` configuré du pool ; un fallback interne de 5 secondes n'est utilisé que si ce timeout n'est pas présent dans le pool, état qui ne doit pas être produit par la construction KSP normale. + +Classification : + +```text +probe/query/decode failure -> HealthFailed +version absente/inférieure -> MigrationMismatch +version supérieure -> SchemaNewer +version attendue -> Ready +``` + +La vérification complète nom/checksum reste celle de `Store::open`/bootstrap. Le health est volontairement un diagnostic léger, pas un second moteur de migration. + +## 6. Sémantique readiness + +`Store::open` ne change pas de sémantique : une instance n'est rendue qu'après connexion physique et bootstrap/history validés. + +Le health sert ensuite à observer l'état courant : une panne transitoire produit `NotReady` avec un code KSP sûr, pas une erreur contenant du texte PostgreSQL. + +Une instance reste attachée à exactement un réseau/target Store ; aucun routage ou multiplexage multi-réseaux n'est ajouté. + +## 7. Scope négatif + +Toujours absent de `0.3.2-pre.007` : + +```text +RawTransaction PostgreSQL +RawAccountState PostgreSQL +repository RAW +SQL métier +CORE/DECODE/SPECIALIZED +health HTTP/service global +scheduler/polling de health +``` + +## 8. Tests/canaris + +Les tests sans serveur couvrent : + +```text +projection pool safe/bornée +Ready vs NotReady +migration version/pending safe +absence d'URI/SQL/credential dans les snapshots +surface façade health crate-root +bridge backend health crate-root +frontières dépendances et absence de SQL métier +``` + +La preuve réelle `Ready`, panne/mismatch et close reste au smoke PostgreSQL opt-in de `pre.008`. + +## 9. Version + +```text +workspace.package.version = 0.3.2-pre.7 +``` + +## 10. Gate opérateur + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas/0.3.2 +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-store-postgres-lib +cargo test -p ksp-store-lib +cargo test -p ksp-store-lib --no-default-features +cargo check -p ksp-store-lib --no-default-features +cargo tree -p ksp-store-postgres-lib --edges normal +cargo tree -p ksp-store-lib --edges normal +``` + +## 11. Suite + +Après gate propre, `pre.008` ajoute le test PostgreSQL réel opt-in non destructif couvrant bootstrap initial/idempotent/concurrent, mismatch, rollback/failure, health Ready et close borné. diff --git a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md index b90bb38..9c05f6d 100644 --- a/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md +++ b/docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.3.2` — Store/PostgreSQL runtime foundation @@ -563,7 +563,7 @@ ops futures ont besoin d'un diagnostic safe sans pool leak close doit être observable sans exposer le backend ``` -Surface candidate dans `ksp-store-lib` : +Surface matérialisée dans `ksp-store-lib` par `pre.007` : ```text StoreHealthState @@ -574,17 +574,24 @@ StoreRuntimeSnapshot Projection sûre seulement : ```text -backend kind -state -pool size/available sous forme de compteurs bornés -schema/migration version courante -pending migration count -last safe error code éventuel +StoreRuntimeSnapshot + backend kind + network logique + pool capacity/size/available/waiting bornés + +StoreHealthSnapshot + Ready | NotReady + runtime snapshot + migration version observée éventuelle + pending migration count + last safe ErrorCode éventuel ``` -Interdits : URI, host, username, database name si sensible, SQL, nom physique de relation, server error string, pool/client handles. +`Store::runtime_snapshot()` est synchrone et ne réalise aucun I/O. `Store::health().await` est un probe borné : acquisition Deadpool sous deadline, `SELECT 1`, puis lecture interne de la version maximale de `ksp_store_schema_migrations`. Un échec ne renvoie jamais le texte PostgreSQL ; il produit `NotReady` et un code KSP déjà classifié. La deadline du probe réutilise le `wait_timeout` du pool, avec un fallback interne borné uniquement si le pool ne fournit pas ce paramètre. -Le backend PostgreSQL peut utiliser `SELECT 1` et une introspection minimale interne, puis mapper son résultat vers la projection portable. +Interdits : URI, host, username, database name si sensible, SQL, nom physique de relation dans la projection publique, server error string, pool/client handles. + +Le probe ne vérifie pas toute l'intégrité checksum à chaque appel : `Store::open` reste propriétaire de la vérification complète bootstrap/history avant de rendre une instance. Le health relit la disponibilité et la version de schéma comme diagnostic léger. ## 12. Config `std.store` @@ -984,7 +991,7 @@ Le gate opérateur de `pre.005-fix.001` est vert : audits, workspace check/Clipp ### `pre.006` — Migration/bootstrap foundation -Statut : matérialisé par `0.3.2-pre.006`, gate Cargo opérateur à exécuter. +Statut : matérialisé par `0.3.2-pre.006`, gate opérateur vert. La tranche introduit un moteur privé `ksp-store-postgres-lib` sans crate de migration externe : @@ -1011,7 +1018,21 @@ Avec les quatre codes PostgreSQL ajoutés en `pre.005` puis les trois codes migr ### `pre.007` — Composition end-to-end + health -Fermer `StoreSettings -> Store -> PostgresBackend`, health/readiness portable, close et mapping diagnostics. +Statut : matérialisé par `0.3.2-pre.007`, gate Cargo opérateur requis. + +La façade ferme la projection runtime/health sans exposer de type physique : + +```text +Store::runtime_snapshot() -> StoreRuntimeSnapshot +Store::health().await -> StoreHealthSnapshot + +StoreHealthState = Ready | NotReady +ERROR_CODE_POSTGRES_HEALTH_FAILED = store.postgres_health_failed +``` + +Le backend ajoute uniquement deux DTO bridge safe (`PostgresBackendRuntimeSnapshot`, `PostgresBackendHealthSnapshot`). Les compteurs `max_size/size/available/waiting` sont saturés en `u32`. Le probe est borné, exécute seulement une disponibilité légère et la lecture de version de migration, puis mappe tout échec vers une classification backend statique sans source externe. `Store::open` ne change pas de sémantique : il ne rend une instance qu'après connexion physique et bootstrap/history vérifiés. Aucun SQL métier RAW n'est introduit. + +Avec `StoreHealthState`, `StoreRuntimeSnapshot`, `StoreHealthSnapshot` et `ERROR_CODE_POSTGRES_HEALTH_FAILED`, la façade atteint désormais 84 exports crate-root : 60 réexports `ksp-store-api` et 24 éléments runtime Store. ### `pre.008` — PostgreSQL integration réelle diff --git a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md index 4a76081..c145bed 100644 --- a/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md +++ b/docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md @@ -1,5 +1,5 @@ - + # Validation `0.3.2` — Store/PostgreSQL runtime foundation @@ -207,7 +207,7 @@ Drop best-effort seulement `pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await`. `pre.005` remplace le stop de staging PostgreSQL par l'ouverture physique : un succès exige `pool.get().await` après construction du pool, puis `close(self)` ferme et draine ce pool sous la deadline configurée. Aucun pool/client n'est exposé par la façade. -Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `PASS pre.005-fix.001 opérateur` pour l'ouverture et le shutdown physiques / `TODO pre.007` pour la composition health. +Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `PASS pre.005-fix.001 opérateur` pour l'ouverture et le shutdown physiques / `TODO gate opérateur pre.007` pour la composition health. ## 5. Config ownership @@ -331,7 +331,7 @@ aucune tâche volontairement laissée orpheline `pre.005` matérialise déjà `Pool::close()` et un drain borné par `shutdown_timeout`; `Drop` ne fait qu'un `close()` best-effort. La preuve end-to-end avec backend réel reste réservée à `pre.007/pre.008`. -Statut : `PASS pre.005-fix.001 opérateur / TODO pre.007/pre.008`. +Statut : `PASS pre.005-fix.001 opérateur / TODO gate pre.007 / TODO pre.008 live`. ## 7. TLS @@ -384,7 +384,7 @@ et aucune table métier RAW/CORE/DECODE/SPECIALIZED. `pre.006` embarque exactement `migrations/V000__bootstrap.sql`, dont le seul DDL de production crée `ksp_store_schema_migrations`. Le canari source interdit les identifiants métier RAW/CORE/DECODE/SPECIALIZED dans le moteur et la ressource SQL. -Statut : `TODO gate opérateur pre.006 / TODO pre.008 live`. +Statut : `PASS pre.006 opérateur / TODO pre.008 live`. ### V32-MIG-002 — Version/checksum @@ -400,7 +400,7 @@ mismatch historique terminal `pre.006` calcule explicitement SHA-256 sur les octets exacts du SQL embarqué puis encode les 32 octets en 64 caractères hex minuscules. Le sentinel `(0, bootstrap, checksum)` est inséré dans la même transaction que la création metadata. Les tests unitaires figent le checksum du SQL committed et couvrent sentinel valide, missing, nom/checksum divergents et historique plus récent. -Statut : `TODO gate opérateur pre.006 / TODO pre.008 live`. +Statut : `PASS pre.006 opérateur / TODO pre.008 live`. ### V32-MIG-003 — Concurrence @@ -408,7 +408,7 @@ Critère : deux runners concurrents sont sérialisés par advisory transaction l `pre.006` utilise une clé KSP fixe et `pg_try_advisory_xact_lock($1)` dans une boucle bornée par `migration_lock_timeout`, avec polling de 25 ms maximum. Aucun lock bloquant illimité n'est utilisé. -Statut : `TODO gate opérateur pre.006 / TODO pre.008 concurrence réelle`. +Statut : `PASS pre.006 opérateur / TODO pre.008 concurrence réelle`. ### V32-MIG-004 — Atomicité/recovery @@ -416,7 +416,7 @@ Critère : échec d'une migration du run courant rollback DDL + history de ce ru `pre.006` place lock, metadata DDL, sentinel et validation dans une transaction unique ; toute sortie d'erreur avant `commit()` droppe la transaction et PostgreSQL rollback le run courant. Un timeout externe borne également l'ensemble du bootstrap. L'injection d'échec et la preuve physique du rollback restent au smoke réel. -Statut : `TODO gate opérateur pre.006 / TODO pre.008 rollback réel`. +Statut : `PASS pre.006 opérateur / TODO pre.008 rollback réel`. ### V32-MIG-005 — Newer runtime guard @@ -424,7 +424,7 @@ Critère : migration appliquée inconnue/supérieure à la liste embarquée prod `pre.006` connaît uniquement la version `0`; toute history `> 0` est classée `SchemaNewer` par le backend puis `store.postgres_schema_newer` par la façade. Aucun chemin de down migration n'existe. -Statut : `TODO gate opérateur pre.006`. +Statut : `PASS pre.006 opérateur`. ### V32-MIG-006 — SQL injection @@ -438,7 +438,7 @@ aucun identifier physique user-configurable en 0.3.2 `pre.006` garde le DDL versionné sous `include_str!` et toutes les values variables des requêtes de contrôle (`lock key`, `statement_timeout`, history values) passent par paramètres. Les noms physiques sont des constantes KSP, jamais des settings. -Statut : `TODO gate opérateur pre.006 / TODO pre.009 hardening`. +Statut : `PASS pre.006 opérateur / TODO pre.009 hardening`. ### V32-MIG-007 — No business capability @@ -456,21 +456,27 @@ Statut : `TODO pre.009/gate final`. ### V32-HEALTH-001 — Projection portable -Critère : façade expose uniquement état/backend/counts/migration safe, jamais URI/SQL/pool/client. +Critère : façade expose uniquement état/backend/network/counts/migration safe, jamais URI/SQL/pool/client. -Statut : `TODO pre.007`. +`pre.007` matérialise `StoreRuntimeSnapshot`, `StoreHealthState::{Ready, NotReady}` et `StoreHealthSnapshot`. Les compteurs pool sont bornés en `u32`; la version migration est optionnelle si le probe ne peut pas la lire. + +Statut : `TODO gate opérateur pre.007`. ### V32-HEALTH-002 — Readiness réelle -Critère : `Store::open` ne retourne Ready qu'après connect + bootstrap/verify selon settings. +Critère : `Store::open` ne retourne Ready qu'après connect + bootstrap/verify selon settings, et `Store::health()` sonde ensuite la disponibilité physique sous deadline. -Statut : `TODO pre.007/pre.008`. +`pre.007` conserve l'ouverture stricte acquise en `pre.006` et ajoute un probe borné par le `wait_timeout` du pool : acquisition, `SELECT 1`, lecture de version migration. Le smoke réel reste nécessaire pour prouver ce chemin contre PostgreSQL. + +Statut : `TODO gate opérateur pre.007 / TODO pre.008 live`. ### V32-HEALTH-003 — Error redaction Critère : un health failure n'expose pas server error string, query text ou credential. -Statut : `TODO pre.007/pre.009`. +Le backend ne conserve que `PostgresBackendErrorKind`; la façade mappe vers un `ErrorCode` KSP optionnel. Le snapshot public ne contient aucune string serveur, URI, SQL, host, user, database ou handle. + +Statut : `TODO gate opérateur pre.007 / TODO pre.009 hardening`. ## 10. PostgreSQL integration réelle