v0.3.2-pre.007
This commit is contained in:
@@ -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.
|
||||
|
||||
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
|
||||
// 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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.
|
||||
#[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<tokio_postgres_rustls::MakeRustls
|
||||
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 {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user