v0.3.2-pre.007
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 340
|
# version: 341
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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"]
|
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]
|
[workspace.package]
|
||||||
version = "0.3.2-pre.6"
|
version = "0.3.2-pre.7"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/error.rs
|
// 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.
|
/// 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");
|
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");
|
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.
|
/// 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");
|
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.
|
/// 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");
|
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.
|
/// Error code used when persisted PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||||
|
|||||||
132
crates/ksp-store-lib/src/health.rs
Normal file
132
crates/ksp-store-lib/src/health.rs
Normal file
@@ -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<ksp_store_api::ErrorCode>,
|
||||||
|
migration_version: std::option::Option<u64>,
|
||||||
|
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<u64>,
|
||||||
|
pending_migration_count: u32,
|
||||||
|
last_error_code: std::option::Option<ksp_store_api::ErrorCode>,
|
||||||
|
) -> 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<ksp_store_api::ErrorCode> {
|
||||||
|
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<u64> {
|
||||||
|
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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/lib.rs
|
// file: crates/ksp-store-lib/src/lib.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -7,9 +7,10 @@
|
|||||||
|
|
||||||
//! Common backend-neutral Store runtime facade for KSP.
|
//! Common backend-neutral Store runtime facade for KSP.
|
||||||
//!
|
//!
|
||||||
//! `0.3.2-pre.006` materializes the physical PostgreSQL runtime plus its private
|
//! `0.3.2-pre.007` closes the physical PostgreSQL runtime composition with a
|
||||||
//! migration/bootstrap foundation: bounded pool, explicit TLS, versioned SHA-256
|
//! portable safe runtime snapshot and lightweight health/readiness projection,
|
||||||
//! history and no business persistence schema.
|
//! while retaining the private migration/bootstrap foundation and no business
|
||||||
|
//! persistence schema.
|
||||||
//!
|
//!
|
||||||
//! The default `postgres` feature compiles the official PostgreSQL backend as
|
//! The default `postgres` feature compiles the official PostgreSQL backend as
|
||||||
//! an optional implementation dependency. No backend implementation type is
|
//! an optional implementation dependency. No backend implementation type is
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod health;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod store;
|
mod store;
|
||||||
|
|
||||||
@@ -30,6 +32,8 @@ pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
|
|||||||
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
|
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
|
||||||
/// Error code used when PostgreSQL physical connection establishment fails.
|
/// Error code used when PostgreSQL physical connection establishment fails.
|
||||||
pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED;
|
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.
|
/// Error code used when PostgreSQL migration/bootstrap execution fails safely.
|
||||||
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED;
|
pub use self::error::ERROR_CODE_POSTGRES_MIGRATION_FAILED;
|
||||||
/// Error code used when PostgreSQL migration history diverges from the embedded immutable KSP history.
|
/// 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;
|
pub use self::error::ERROR_CODE_SETTINGS_INVALID;
|
||||||
/// Error code used when explicit Store shutdown exceeds its configured deadline.
|
/// Error code used when explicit Store shutdown exceeds its configured deadline.
|
||||||
pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT;
|
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.
|
/// Bounded PostgreSQL bootstrap and migration settings owned by the Store facade.
|
||||||
pub use self::settings::PostgresBootstrapSettings;
|
pub use self::settings::PostgresBootstrapSettings;
|
||||||
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
|
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/store.rs
|
// file: crates/ksp-store-lib/src/store.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
/// Opaque common Store runtime facade.
|
/// 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.
|
/// 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<()> {
|
pub async fn close(self) -> ksp_store_api::Result<()> {
|
||||||
let backend_kind = self.backend_kind;
|
let backend_kind = self.backend_kind;
|
||||||
@@ -136,9 +174,47 @@ async fn open_postgres(
|
|||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error {
|
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::ConfigInvalid => crate::ERROR_CODE_POSTGRES_CONFIG_INVALID,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed => crate::ERROR_CODE_POSTGRES_CONNECT_FAILED,
|
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::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => crate::ERROR_CODE_POSTGRES_MIGRATION_FAILED,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => crate::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH,
|
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,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED,
|
||||||
_ => crate::ERROR_CODE_BACKEND_OPEN_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"))]
|
#[cfg(not(feature = "postgres"))]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![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() {
|
fn pre_005_facade_exposes_no_physical_postgres_types_or_environment_bypass() {
|
||||||
let crate_root = include_str!("../src/lib.rs");
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
assert!(crate_root.contains("pub use self::settings::StoreSettings;"));
|
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 self::store::Store;"));
|
||||||
assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;"));
|
assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;"));
|
||||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
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}");
|
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"] {
|
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}");
|
assert!(!production.contains(forbidden), "forbidden Store facade ownership bypass detected: {forbidden}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/tests/public_api.rs
|
// file: crates/ksp-store-lib/tests/public_api.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -25,6 +25,8 @@ fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() {
|
|||||||
assert!(settings.validate().is_ok());
|
assert!(settings.validate().is_ok());
|
||||||
let _open = ksp_store_lib::Store::open;
|
let _open = ksp_store_lib::Store::open;
|
||||||
let _close = ksp_store_lib::Store::close;
|
let _close = ksp_store_lib::Store::close;
|
||||||
|
let _runtime_snapshot = ksp_store_lib::Store::runtime_snapshot;
|
||||||
|
let _health = ksp_store_lib::Store::health;
|
||||||
return;
|
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_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_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_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_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_FAILED.code(), "postgres_migration_failed");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_MIGRATION_MISMATCH.code(), "postgres_migration_mismatch");
|
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(());
|
let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_007_health_and_runtime_snapshot_types_are_portable_crate_root_contracts() {
|
||||||
|
let _state = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreHealthState>>();
|
||||||
|
let _health = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreHealthSnapshot>>();
|
||||||
|
let _runtime = std::mem::size_of::<std::option::Option<ksp_store_lib::StoreRuntimeSnapshot>>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|||||||
45
crates/ksp-store-lib/unit_tests/health.rs
Normal file
45
crates/ksp-store-lib/unit_tests/health.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/error.rs
|
// 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.
|
/// Safe backend-local classification used by the Store facade for stable error mapping.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
@@ -11,6 +11,8 @@ pub enum PostgresBackendErrorKind {
|
|||||||
ConnectFailed,
|
ConnectFailed,
|
||||||
/// A bounded pool wait, create or recycle operation reached its deadline.
|
/// A bounded pool wait, create or recycle operation reached its deadline.
|
||||||
PoolTimeout,
|
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.
|
/// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL.
|
||||||
MigrationFailed,
|
MigrationFailed,
|
||||||
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
|
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||||
|
|||||||
101
crates/ksp-store-postgres-lib/src/health.rs
Normal file
101
crates/ksp-store-postgres-lib/src/health.rs
Normal file
@@ -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::<usize, i64>(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::<usize, i64>(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<u64> {
|
||||||
|
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;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -7,9 +7,10 @@
|
|||||||
|
|
||||||
//! Official PostgreSQL backend implementation for KSP Store.
|
//! Official PostgreSQL backend implementation for KSP Store.
|
||||||
//!
|
//!
|
||||||
//! `0.3.2-pre.006` owns the physical `tokio-postgres` connection, bounded
|
//! `0.3.2-pre.007` owns the physical `tokio-postgres` connection, bounded
|
||||||
//! Deadpool pool, explicit Rustls TLS policy and private KSP migration/bootstrap
|
//! Deadpool pool, explicit Rustls TLS policy, private KSP migration/bootstrap
|
||||||
//! engine. Business persistence remains absent from this foundation release.
|
//! 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
|
//! 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
|
//! common facade consumes only this crate's narrow backend bridge and never
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod health;
|
||||||
mod migration;
|
mod migration;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
|
||||||
@@ -26,6 +28,10 @@ pub use self::error::PostgresBackendError;
|
|||||||
pub use self::error::PostgresBackendErrorKind;
|
pub use self::error::PostgresBackendErrorKind;
|
||||||
/// Opaque physical PostgreSQL backend owning its connection pool.
|
/// Opaque physical PostgreSQL backend owning its connection pool.
|
||||||
pub use self::runtime::PostgresBackend;
|
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.
|
/// Physical PostgreSQL settings bridge consumed only by the backend crate.
|
||||||
pub use self::runtime::PostgresBackendSettings;
|
pub use self::runtime::PostgresBackendSettings;
|
||||||
/// TLS mode accepted by the physical PostgreSQL settings bridge.
|
/// 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.
|
/// Crate-owned tracing target for PostgreSQL backend behavior.
|
||||||
pub(crate) use self::constants::TRACING_TARGET;
|
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.
|
/// Private migration/bootstrap runner consumed by the physical backend runtime.
|
||||||
pub(crate) use self::migration::bootstrap;
|
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;
|
const _: &str = crate::TRACING_TARGET;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/migration.rs
|
// file: crates/ksp-store-postgres-lib/src/migration.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
use sha2::Digest; // rust-rules: trait-import
|
use sha2::Digest; // rust-rules: trait-import
|
||||||
|
|
||||||
@@ -40,6 +40,12 @@ struct AppliedMigration {
|
|||||||
version: i64,
|
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.
|
/// Runs the private bounded PostgreSQL schema bootstrap on one dedicated pooled client.
|
||||||
pub(crate) async fn bootstrap(
|
pub(crate) async fn bootstrap(
|
||||||
client: &mut deadpool_postgres::Client,
|
client: &mut deadpool_postgres::Client,
|
||||||
|
|||||||
@@ -1,10 +1,122 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
const APPLICATION_NAME: &str = "ksp-store";
|
const APPLICATION_NAME: &str = "ksp-store";
|
||||||
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||||
const SHUTDOWN_POLL_INTERVAL_MS: u64 = 10;
|
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<crate::PostgresBackendErrorKind>,
|
||||||
|
migration_version: std::option::Option<u64>,
|
||||||
|
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<u64>,
|
||||||
|
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<crate::PostgresBackendErrorKind> {
|
||||||
|
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<u64> {
|
||||||
|
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.
|
/// TLS mode accepted by the physical PostgreSQL backend bridge.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@@ -165,6 +277,17 @@ impl PostgresBackend {
|
|||||||
return std::result::Result::Ok(Self { network: settings.network, pool });
|
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.
|
/// 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> {
|
pub async fn close(self, timeout: std::time::Duration) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||||
self.pool.close();
|
self.pool.close();
|
||||||
@@ -304,7 +427,8 @@ fn build_verified_tls() -> std::result::Result<tokio_postgres_rustls::MakeRustls
|
|||||||
return std::result::Result::Ok(tokio_postgres_rustls::MakeRustlsConnect::new(client_config));
|
return std::result::Result::Ok(tokio_postgres_rustls::MakeRustlsConnect::new(client_config));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_pool_error(error: deadpool_postgres::PoolError) -> 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 {
|
return match error {
|
||||||
deadpool_postgres::PoolError::Timeout(timeout_type) => crate::PostgresBackendError::new(
|
deadpool_postgres::PoolError::Timeout(timeout_type) => crate::PostgresBackendError::new(
|
||||||
crate::PostgresBackendErrorKind::PoolTimeout,
|
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)]
|
#[cfg(test)]
|
||||||
#[path = "../unit_tests/runtime.rs"]
|
#[path = "../unit_tests/runtime.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![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() {
|
fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
|
||||||
let crate_root = include_str!("../src/lib.rs");
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
assert!(crate_root.contains("mod error;"));
|
assert!(crate_root.contains("mod error;"));
|
||||||
|
assert!(crate_root.contains("mod health;"));
|
||||||
assert!(crate_root.contains("mod migration;"));
|
assert!(crate_root.contains("mod migration;"));
|
||||||
assert!(crate_root.contains("mod runtime;"));
|
assert!(crate_root.contains("mod runtime;"));
|
||||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
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;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![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::ConfigInvalid,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
|
||||||
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
|
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::<std::option::Option<ksp_store_postgres_lib::PostgresBackendRuntimeSnapshot>>();
|
||||||
|
let _health = std::mem::size_of::<std::option::Option<ksp_store_postgres_lib::PostgresBackendHealthSnapshot>>();
|
||||||
|
let _runtime_snapshot = ksp_store_postgres_lib::PostgresBackend::runtime_snapshot;
|
||||||
|
let _health_probe = ksp_store_postgres_lib::PostgresBackend::health;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
42
crates/ksp-store-postgres-lib/unit_tests/health.rs
Normal file
42
crates/ksp-store-postgres-lib/unit_tests/health.rs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
165
deltas/0.3.2/pre.007.md
Normal file
165
deltas/0.3.2/pre.007.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<!-- file: deltas/0.3.2/pre.007.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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é.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
|
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Plan `0.3.2` — Store/PostgreSQL runtime foundation
|
# 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
|
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
|
```text
|
||||||
StoreHealthState
|
StoreHealthState
|
||||||
@@ -574,17 +574,24 @@ StoreRuntimeSnapshot
|
|||||||
Projection sûre seulement :
|
Projection sûre seulement :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
backend kind
|
StoreRuntimeSnapshot
|
||||||
state
|
backend kind
|
||||||
pool size/available sous forme de compteurs bornés
|
network logique
|
||||||
schema/migration version courante
|
pool capacity/size/available/waiting bornés
|
||||||
pending migration count
|
|
||||||
last safe error code éventuel
|
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`
|
## 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
|
### `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 :
|
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
|
### `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
|
### `pre.008` — PostgreSQL integration réelle
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
|
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
|
||||||
<!-- version: 8 -->
|
<!-- version: 9 -->
|
||||||
|
|
||||||
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
|
# 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.
|
`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
|
## 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`.
|
`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
|
## 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.
|
`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
|
### 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.
|
`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
|
### 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é.
|
`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
|
### 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.
|
`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
|
### 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.
|
`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
|
### 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.
|
`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
|
### V32-MIG-007 — No business capability
|
||||||
|
|
||||||
@@ -456,21 +456,27 @@ Statut : `TODO pre.009/gate final`.
|
|||||||
|
|
||||||
### V32-HEALTH-001 — Projection portable
|
### 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
|
### 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
|
### V32-HEALTH-003 — Error redaction
|
||||||
|
|
||||||
Critère : un health failure n'expose pas server error string, query text ou credential.
|
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
|
## 10. PostgreSQL integration réelle
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user