v0.3.2-pre.005
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 336
|
# version: 337
|
||||||
|
|
||||||
[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.4.fix.1"
|
version = "0.3.2-pre.5"
|
||||||
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"
|
||||||
@@ -18,6 +18,7 @@ argon2 = { version = "^0.5", default-features = false }
|
|||||||
base64 = { version = "^0.23" }
|
base64 = { version = "^0.23" }
|
||||||
chacha20poly1305 = { version = "^0.11", default-features = false }
|
chacha20poly1305 = { version = "^0.11", default-features = false }
|
||||||
chrono = { version = "^0.4", default-features = false }
|
chrono = { version = "^0.4", default-features = false }
|
||||||
|
deadpool-postgres = { version = "^0.14", default-features = false }
|
||||||
directories = { version = "^6.0" }
|
directories = { version = "^6.0" }
|
||||||
ed25519-dalek = { version = "^3.0", default-features = false }
|
ed25519-dalek = { version = "^3.0", default-features = false }
|
||||||
fs2 = { version = "^0.4" }
|
fs2 = { version = "^0.4" }
|
||||||
@@ -26,6 +27,8 @@ getrandom = { version = "^0.4", default-features = false }
|
|||||||
http = { version = "^1.5", default-features = false }
|
http = { version = "^1.5", default-features = false }
|
||||||
jsonschema = { version = "^0.51", default-features = false }
|
jsonschema = { version = "^0.51", default-features = false }
|
||||||
reqwest = { version = "^0.13", default-features = false }
|
reqwest = { version = "^0.13", default-features = false }
|
||||||
|
rustls = { version = "^0.23", default-features = false }
|
||||||
|
rustls-native-certs = { version = "^0.8", default-features = false }
|
||||||
serde = { version = "^1.0" }
|
serde = { version = "^1.0" }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
solana-keypair = { version = "^3.1", default-features = false }
|
solana-keypair = { version = "^3.1", default-features = false }
|
||||||
@@ -39,6 +42,8 @@ tracing = { version = "^0.1", default-features = false }
|
|||||||
tracing-subscriber = { version = "^0.3", default-features = false }
|
tracing-subscriber = { version = "^0.3", default-features = false }
|
||||||
tracing-appender = { version = "^0.2", default-features = false }
|
tracing-appender = { version = "^0.2", default-features = false }
|
||||||
tokio = { version = "^1.53", default-features = false }
|
tokio = { version = "^1.53", default-features = false }
|
||||||
|
tokio-postgres = { version = "^0.7", default-features = false }
|
||||||
|
tokio-postgres-rustls = { version = "^0.14", default-features = false }
|
||||||
tokio-tungstenite = { version = "^0.30", default-features = false }
|
tokio-tungstenite = { version = "^0.30", default-features = false }
|
||||||
tonic = { version = "^0.14", default-features = false }
|
tonic = { version = "^0.14", default-features = false }
|
||||||
tonic-prost = { version = "^0.14", default-features = false }
|
tonic-prost = { version = "^0.14", default-features = false }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-store-lib/Cargo.toml
|
# file: crates/ksp-store-lib/Cargo.toml
|
||||||
# version: 1
|
# version: 2
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-store-lib"
|
name = "ksp-store-lib"
|
||||||
@@ -12,6 +12,7 @@ default = ["postgres"]
|
|||||||
postgres = ["dep:ksp-store-postgres-lib"]
|
postgres = ["dep:ksp-store-postgres-lib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
ksp-store-api = { path = "../ksp-store-api" }
|
ksp-store-api = { path = "../ksp-store-api" }
|
||||||
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib", optional = true }
|
ksp-store-postgres-lib = { path = "../ksp-store-postgres-lib", optional = true }
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/error.rs
|
// file: crates/ksp-store-lib/src/error.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
/// 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");
|
||||||
@@ -7,6 +7,14 @@ pub const ERROR_CODE_BACKEND_CLOSED: ksp_store_api::ErrorCode = ksp_store_api::E
|
|||||||
pub const ERROR_CODE_BACKEND_NOT_COMPILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_not_compiled");
|
pub const ERROR_CODE_BACKEND_NOT_COMPILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_not_compiled");
|
||||||
/// Error code used when a compiled Store backend cannot complete its bounded opening lifecycle.
|
/// Error code used when a compiled Store backend cannot complete its bounded opening lifecycle.
|
||||||
pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_open_failed");
|
pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_open_failed");
|
||||||
|
/// Error code used when the PostgreSQL backend rejects or cannot normalize its physical connection configuration.
|
||||||
|
pub const ERROR_CODE_POSTGRES_CONFIG_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_config_invalid");
|
||||||
|
/// Error code used when PostgreSQL physical connection establishment fails without exposing remote or credential details.
|
||||||
|
pub const ERROR_CODE_POSTGRES_CONNECT_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_connect_failed");
|
||||||
|
/// Error code used when a bounded PostgreSQL pool wait, create or recycle operation reaches its deadline.
|
||||||
|
pub const ERROR_CODE_POSTGRES_POOL_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_pool_timeout");
|
||||||
|
/// Error code used when verified PostgreSQL TLS setup or negotiation cannot be completed safely.
|
||||||
|
pub const ERROR_CODE_POSTGRES_TLS_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "postgres_tls_failed");
|
||||||
/// Error code used when backend-neutral Store settings violate runtime bounds or invariants.
|
/// Error code used when backend-neutral Store settings violate runtime bounds or invariants.
|
||||||
pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid");
|
pub const ERROR_CODE_SETTINGS_INVALID: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "settings_invalid");
|
||||||
/// Error code used when a Store cannot complete its explicit shutdown inside the configured bound.
|
/// Error code used when a Store cannot complete its explicit shutdown inside the configured bound.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/lib.rs
|
// file: crates/ksp-store-lib/src/lib.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -7,10 +7,9 @@
|
|||||||
|
|
||||||
//! Common backend-neutral Store runtime facade for KSP.
|
//! Common backend-neutral Store runtime facade for KSP.
|
||||||
//!
|
//!
|
||||||
//! `0.3.2-pre.003` materializes Config-independent settings, stable backend
|
//! `0.3.2-pre.005` materializes the first physical PostgreSQL runtime path:
|
||||||
//! identity, bounded validation and the opaque async Store lifecycle contract.
|
//! one bounded Deadpool pool, one startup connection proof and explicit TLS
|
||||||
//! Physical PostgreSQL connection, pool, TLS and migrations remain private
|
//! policy. SQL migrations remain private future slices of this release.
|
||||||
//! future slices of this release.
|
|
||||||
//!
|
//!
|
||||||
//! 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
|
||||||
@@ -27,6 +26,14 @@ pub use self::error::ERROR_CODE_BACKEND_CLOSED;
|
|||||||
pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED;
|
pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED;
|
||||||
/// Error code used when a compiled Store backend cannot complete opening.
|
/// Error code used when a compiled Store backend cannot complete opening.
|
||||||
pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
|
pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
|
||||||
|
/// Error code used when PostgreSQL physical configuration is malformed or unsupported.
|
||||||
|
pub use self::error::ERROR_CODE_POSTGRES_CONFIG_INVALID;
|
||||||
|
/// Error code used when PostgreSQL physical connection establishment fails.
|
||||||
|
pub use self::error::ERROR_CODE_POSTGRES_CONNECT_FAILED;
|
||||||
|
/// Error code used when a bounded PostgreSQL pool operation reaches its deadline.
|
||||||
|
pub use self::error::ERROR_CODE_POSTGRES_POOL_TIMEOUT;
|
||||||
|
/// Error code used when PostgreSQL verified TLS setup or negotiation fails.
|
||||||
|
pub use self::error::ERROR_CODE_POSTGRES_TLS_FAILED;
|
||||||
/// Error code used when Store settings violate backend-neutral bounds or invariants.
|
/// Error code used when Store settings violate backend-neutral bounds or invariants.
|
||||||
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.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/src/settings.rs
|
// file: crates/ksp-store-lib/src/settings.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
|
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
|
||||||
const DEFAULT_MAX_CONNECTIONS: u32 = 8;
|
const DEFAULT_MAX_CONNECTIONS: u32 = 8;
|
||||||
@@ -239,6 +239,13 @@ impl PostgresStoreSettings {
|
|||||||
return self.bootstrap;
|
return self.bootstrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the sensitive PostgreSQL connection URI only to the compiled backend bridge.
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn connection_uri(&self) -> &str {
|
||||||
|
return self.connection_uri.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the PostgreSQL pool settings without exposing the sensitive connection URI.
|
/// Returns the PostgreSQL pool settings without exposing the sensitive connection URI.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn pool(&self) -> PostgresPoolSettings {
|
pub const fn pool(&self) -> PostgresPoolSettings {
|
||||||
|
|||||||
@@ -1,54 +1,155 @@
|
|||||||
// file: crates/ksp-store-lib/src/store.rs
|
// file: crates/ksp-store-lib/src/store.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
/// Opaque common Store runtime facade.
|
/// Opaque common Store runtime facade.
|
||||||
///
|
///
|
||||||
/// A successful value is returned only after the selected backend has completed its bounded readiness path. `0.3.2-pre.003` establishes the lifecycle
|
/// A successful value is returned only after the selected compiled backend has completed its bounded physical opening path. PostgreSQL pool, client, TLS and
|
||||||
/// contract but intentionally has no successful opening path until the PostgreSQL runtime foundation is materialized by later prereleases.
|
/// driver types remain private to the backend crate.
|
||||||
#[derive(Debug)]
|
pub struct Store {
|
||||||
#[non_exhaustive]
|
backend_kind: crate::StoreBackendKind,
|
||||||
pub struct Store;
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
runtime: StoreRuntime,
|
||||||
|
shutdown_timeout: std::time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
/// Validates settings, selects the requested backend and opens a ready Store runtime.
|
/// Validates settings, selects the requested backend and opens one ready Store instance for exactly one logical network.
|
||||||
///
|
///
|
||||||
/// A known backend whose Cargo feature is absent is rejected before any I/O. During `0.3.2-pre.003`, the compiled PostgreSQL path also stops before I/O
|
/// A known backend whose Cargo feature is absent is rejected before any I/O. A successful PostgreSQL result proves that one physical pooled connection has
|
||||||
/// with [`crate::ERROR_CODE_BACKEND_OPEN_FAILED`] because physical connection ownership is introduced in `pre.005`.
|
/// been established under the typed TLS and timeout policy.
|
||||||
pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result<Self> {
|
pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result<Self> {
|
||||||
let validation = settings.validate();
|
let validation = settings.validate();
|
||||||
if let std::result::Result::Err(error) = validation {
|
if let std::result::Result::Err(error) = validation {
|
||||||
return std::result::Result::Err(error);
|
return std::result::Result::Err(error);
|
||||||
}
|
}
|
||||||
let backend_kind = settings.backend_kind();
|
let backend_kind = settings.backend_kind();
|
||||||
return match backend_kind {
|
let network = settings.network().clone();
|
||||||
crate::StoreBackendKind::Postgres => open_postgres_contract(backend_kind).await,
|
let shutdown_timeout = settings.shutdown_timeout();
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
backend = backend_kind.code(),
|
||||||
|
network = network.as_str(),
|
||||||
|
"opening Store runtime"
|
||||||
|
);
|
||||||
|
return match settings.backend() {
|
||||||
|
crate::StoreBackendSettings::Postgres(postgres) => open_postgres(backend_kind, network, shutdown_timeout, postgres).await,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explicitly closes the Store runtime and consumes its facade handle.
|
/// Explicitly closes the Store runtime, consumes its facade handle and applies the configured bounded shutdown deadline.
|
||||||
///
|
|
||||||
/// `pre.003` cannot yet produce a successful Store instance, so the physical bounded shutdown path remains reserved for backend composition. The consuming
|
|
||||||
/// async signature is fixed here so no pool or backend handle needs to escape later.
|
|
||||||
pub async fn close(self) -> ksp_store_api::Result<()> {
|
pub async fn close(self) -> ksp_store_api::Result<()> {
|
||||||
return std::result::Result::Ok(());
|
let backend_kind = self.backend_kind;
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
{
|
||||||
|
let network = self.network;
|
||||||
|
let shutdown_timeout = self.shutdown_timeout;
|
||||||
|
let result = match self.runtime {
|
||||||
|
StoreRuntime::Postgres(backend) => backend.close(shutdown_timeout).await,
|
||||||
|
};
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
backend = backend_kind.code(),
|
||||||
|
network = network.as_str(),
|
||||||
|
"Store runtime closed"
|
||||||
|
);
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "postgres"))]
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(unavailable_runtime_error(backend_kind));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for Store {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("Store")
|
||||||
|
.field("backend_kind", &self.backend_kind)
|
||||||
|
.field("network", &self.network)
|
||||||
|
.field("shutdown_timeout", &self.shutdown_timeout)
|
||||||
|
.finish_non_exhaustive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> {
|
enum StoreRuntime {
|
||||||
return std::result::Result::Err(
|
Postgres(ksp_store_postgres_lib::PostgresBackend),
|
||||||
ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_OPEN_FAILED, "PostgreSQL Store runtime opening is not materialized in this prerelease")
|
}
|
||||||
.with_context("backend", backend_kind.code())
|
|
||||||
.with_context("stage", "runtime_foundation_pending"),
|
#[cfg(feature = "postgres")]
|
||||||
|
async fn open_postgres(
|
||||||
|
backend_kind: crate::StoreBackendKind,
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
shutdown_timeout: std::time::Duration,
|
||||||
|
settings: &crate::PostgresStoreSettings,
|
||||||
|
) -> ksp_store_api::Result<Store> {
|
||||||
|
let pool = settings.pool();
|
||||||
|
let tls_mode = match settings.tls_mode() {
|
||||||
|
crate::PostgresTlsMode::Disabled => ksp_store_postgres_lib::PostgresBackendTlsMode::Disabled,
|
||||||
|
crate::PostgresTlsMode::VerifyFull => ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
||||||
|
};
|
||||||
|
let backend_settings = ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||||
|
network.clone(),
|
||||||
|
settings.connection_uri(),
|
||||||
|
pool.max_connections(),
|
||||||
|
pool.connect_timeout(),
|
||||||
|
pool.wait_timeout(),
|
||||||
|
pool.create_timeout(),
|
||||||
|
pool.recycle_timeout(),
|
||||||
|
tls_mode,
|
||||||
);
|
);
|
||||||
|
let opened = ksp_store_postgres_lib::PostgresBackend::open(backend_settings).await;
|
||||||
|
return match opened {
|
||||||
|
std::result::Result::Ok(backend) => {
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
backend = backend_kind.code(),
|
||||||
|
network = network.as_str(),
|
||||||
|
"Store backend is physically ready"
|
||||||
|
);
|
||||||
|
std::result::Result::Ok(Store { backend_kind, network, runtime: StoreRuntime::Postgres(backend), shutdown_timeout })
|
||||||
|
},
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(map_postgres_error(error, backend_kind, network.as_str())),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "postgres"))]
|
#[cfg(not(feature = "postgres"))]
|
||||||
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> {
|
async fn open_postgres(
|
||||||
return std::result::Result::Err(
|
backend_kind: crate::StoreBackendKind,
|
||||||
ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled")
|
_network: ksp_store_api::RawNetworkId,
|
||||||
.with_context("backend", backend_kind.code()),
|
_shutdown_timeout: std::time::Duration,
|
||||||
);
|
_settings: &crate::PostgresStoreSettings,
|
||||||
|
) -> ksp_store_api::Result<Store> {
|
||||||
|
return std::result::Result::Err(unavailable_runtime_error(backend_kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "postgres")]
|
||||||
|
fn map_postgres_error(error: ksp_store_postgres_lib::PostgresBackendError, backend_kind: crate::StoreBackendKind, network: &str) -> ksp_store_api::Error {
|
||||||
|
let code = match error.kind() {
|
||||||
|
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::PoolTimeout => crate::ERROR_CODE_POSTGRES_POOL_TIMEOUT,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout => crate::ERROR_CODE_SHUTDOWN_TIMEOUT,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed => crate::ERROR_CODE_POSTGRES_TLS_FAILED,
|
||||||
|
_ => crate::ERROR_CODE_BACKEND_OPEN_FAILED,
|
||||||
|
};
|
||||||
|
return ksp_store_api::Error::new(code, "PostgreSQL Store backend lifecycle operation failed")
|
||||||
|
.with_context("backend", backend_kind.code())
|
||||||
|
.with_context("network", network)
|
||||||
|
.with_context("phase", error.phase());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "postgres"))]
|
||||||
|
fn unavailable_runtime_error(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Error {
|
||||||
|
return ksp_store_api::Error::new(crate::ERROR_CODE_BACKEND_NOT_COMPILED, "Selected Store backend is not compiled")
|
||||||
|
.with_context("backend", backend_kind.code());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -8,10 +8,11 @@
|
|||||||
//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade.
|
//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() {
|
fn pre_005_manifest_keeps_backend_physical_dependencies_out_of_facade() {
|
||||||
let manifest = include_str!("../Cargo.toml");
|
let manifest = include_str!("../Cargo.toml");
|
||||||
assert!(manifest.contains("default = [\"postgres\"]"));
|
assert!(manifest.contains("default = [\"postgres\"]"));
|
||||||
assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]"));
|
assert!(manifest.contains("postgres = [\"dep:ksp-store-postgres-lib\"]"));
|
||||||
|
assert!(manifest.contains("ksp-logging-lib = { path = \"../ksp-logging-lib\" }"));
|
||||||
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
|
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
|
||||||
assert!(manifest.contains("ksp-store-postgres-lib = { path = \"../ksp-store-postgres-lib\", optional = true }"));
|
assert!(manifest.contains("ksp-store-postgres-lib = { path = \"../ksp-store-postgres-lib\", optional = true }"));
|
||||||
for forbidden in [
|
for forbidden in [
|
||||||
@@ -30,11 +31,8 @@ fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_003_facade_adds_only_backend_neutral_settings_lifecycle_and_api_reexports() {
|
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("mod error;"));
|
|
||||||
assert!(crate_root.contains("mod settings;"));
|
|
||||||
assert!(crate_root.contains("mod store;"));
|
|
||||||
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::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;"));
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// file: crates/ksp-store-lib/tests/feature_mismatch.rs
|
// file: crates/ksp-store-lib/tests/feature_mismatch.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
//! Feature-selection canaries for known Store backends.
|
//! Feature-selection and pre-I/O PostgreSQL failure canaries for the common Store facade.
|
||||||
|
|
||||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||||
let mut future = std::boxed::Box::pin(future);
|
let mut future = std::boxed::Box::pin(future);
|
||||||
@@ -13,7 +13,7 @@ fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
|||||||
let mut context = std::task::Context::from_waker(waker);
|
let mut context = std::task::Context::from_waker(waker);
|
||||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||||
std::task::Poll::Ready(value) => value,
|
std::task::Poll::Ready(value) => value,
|
||||||
std::task::Poll::Pending => panic!("pre.003 Store feature-dispatch future unexpectedly became pending before any I/O exists"),
|
std::task::Poll::Pending => panic!("Store pre-I/O feature/config rejection unexpectedly became pending"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,9 +24,9 @@ fn valid_network() -> ksp_store_lib::RawNetworkId {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn settings() -> ksp_store_lib::StoreSettings {
|
fn settings(connection_uri: &str) -> ksp_store_lib::StoreSettings {
|
||||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
||||||
"postgresql://operator-supplied-sensitive-value",
|
connection_uri,
|
||||||
ksp_store_lib::PostgresPoolSettings::default(),
|
ksp_store_lib::PostgresPoolSettings::default(),
|
||||||
ksp_store_lib::PostgresTlsMode::Disabled,
|
ksp_store_lib::PostgresTlsMode::Disabled,
|
||||||
ksp_store_lib::PostgresBootstrapSettings::default(),
|
ksp_store_lib::PostgresBootstrapSettings::default(),
|
||||||
@@ -36,8 +36,8 @@ fn settings() -> ksp_store_lib::StoreSettings {
|
|||||||
|
|
||||||
#[cfg(not(feature = "postgres"))]
|
#[cfg(not(feature = "postgres"))]
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() {
|
fn pre_005_known_postgres_without_feature_returns_stable_error_before_io() {
|
||||||
let result = poll_ready(ksp_store_lib::Store::open(settings()));
|
let result = poll_ready(ksp_store_lib::Store::open(settings("postgresql://operator-supplied-sensitive-value@localhost/ksp")));
|
||||||
let error = result.err();
|
let error = result.err();
|
||||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED));
|
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED));
|
||||||
return;
|
return;
|
||||||
@@ -45,9 +45,11 @@ fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() {
|
|||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_003_compiled_postgres_path_refuses_to_fake_readiness_before_pre_005() {
|
fn pre_005_compiled_postgres_rejects_malformed_uri_without_secret_leak_before_io() {
|
||||||
let result = poll_ready(ksp_store_lib::Store::open(settings()));
|
let secret_canary = "not-a-postgresql-uri-secret-canary";
|
||||||
|
let result = poll_ready(ksp_store_lib::Store::open(settings(secret_canary)));
|
||||||
let error = result.err();
|
let error = result.err();
|
||||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED));
|
assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_POSTGRES_CONFIG_INVALID));
|
||||||
|
assert!(!format!("{error:?}").contains(secret_canary));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/tests/public_api.rs
|
// file: crates/ksp-store-lib/tests/public_api.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -29,11 +29,15 @@ fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_003_common_error_codes_are_stable_and_store_owned() {
|
fn pre_005_common_and_postgres_error_codes_are_stable_and_store_owned() {
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.domain(), "store");
|
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.domain(), "store");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.code(), "settings_invalid");
|
assert_eq!(ksp_store_lib::ERROR_CODE_SETTINGS_INVALID.code(), "settings_invalid");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED.code(), "backend_not_compiled");
|
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED.code(), "backend_not_compiled");
|
||||||
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_CONNECT_FAILED.code(), "postgres_connect_failed");
|
||||||
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_POOL_TIMEOUT.code(), "postgres_pool_timeout");
|
||||||
|
assert_eq!(ksp_store_lib::ERROR_CODE_POSTGRES_TLS_FAILED.code(), "postgres_tls_failed");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
|
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
|
||||||
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
|
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-store-lib/unit_tests/store.rs
|
// file: crates/ksp-store-lib/unit_tests/store.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||||
let mut future = std::boxed::Box::pin(future);
|
let mut future = std::boxed::Box::pin(future);
|
||||||
@@ -7,7 +7,7 @@ fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
|||||||
let mut context = std::task::Context::from_waker(waker);
|
let mut context = std::task::Context::from_waker(waker);
|
||||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||||
std::task::Poll::Ready(value) => value,
|
std::task::Poll::Ready(value) => value,
|
||||||
std::task::Poll::Pending => panic!("pre.003 Store contract future unexpectedly became pending before any I/O exists"),
|
std::task::Poll::Pending => panic!("Store pre-I/O rejection unexpectedly became pending"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,11 +18,11 @@ fn valid_network() -> crate::RawNetworkId {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn valid_store_settings() -> crate::StoreSettings {
|
fn store_settings(connection_uri: &str) -> crate::StoreSettings {
|
||||||
let postgres = crate::PostgresStoreSettings::new(
|
let postgres = crate::PostgresStoreSettings::new(
|
||||||
"postgresql://secret-user:secret-password@db.internal/ksp",
|
connection_uri,
|
||||||
crate::PostgresPoolSettings::default(),
|
crate::PostgresPoolSettings::default(),
|
||||||
crate::PostgresTlsMode::VerifyFull,
|
crate::PostgresTlsMode::Disabled,
|
||||||
crate::PostgresBootstrapSettings::default(),
|
crate::PostgresBootstrapSettings::default(),
|
||||||
);
|
);
|
||||||
return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres));
|
return crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres));
|
||||||
@@ -30,14 +30,7 @@ fn valid_store_settings() -> crate::StoreSettings {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_settings_are_rejected_before_backend_dispatch() {
|
fn invalid_settings_are_rejected_before_backend_dispatch() {
|
||||||
let postgres = crate::PostgresStoreSettings::new(
|
let result = poll_ready(crate::Store::open(store_settings("")));
|
||||||
std::string::String::new(),
|
|
||||||
crate::PostgresPoolSettings::default(),
|
|
||||||
crate::PostgresTlsMode::Disabled,
|
|
||||||
crate::PostgresBootstrapSettings::default(),
|
|
||||||
);
|
|
||||||
let settings = crate::StoreSettings::with_default_shutdown(valid_network(), crate::StoreBackendSettings::Postgres(postgres));
|
|
||||||
let result = poll_ready(crate::Store::open(settings));
|
|
||||||
let error = result.err();
|
let error = result.err();
|
||||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
||||||
return;
|
return;
|
||||||
@@ -45,17 +38,19 @@ fn invalid_settings_are_rejected_before_backend_dispatch() {
|
|||||||
|
|
||||||
#[cfg(feature = "postgres")]
|
#[cfg(feature = "postgres")]
|
||||||
#[test]
|
#[test]
|
||||||
fn compiled_postgres_dispatch_does_not_fake_readiness_before_connection_materialization() {
|
fn compiled_postgres_rejects_malformed_physical_configuration_before_io() {
|
||||||
let result = poll_ready(crate::Store::open(valid_store_settings()));
|
let secret_canary = "not-a-postgresql-uri-secret-canary";
|
||||||
|
let result = poll_ready(crate::Store::open(store_settings(secret_canary)));
|
||||||
let error = result.err();
|
let error = result.err();
|
||||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_OPEN_FAILED));
|
assert_eq!(error.as_ref().map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_POSTGRES_CONFIG_INVALID));
|
||||||
|
assert!(!format!("{error:?}").contains(secret_canary));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "postgres"))]
|
#[cfg(not(feature = "postgres"))]
|
||||||
#[test]
|
#[test]
|
||||||
fn known_postgres_without_feature_is_rejected_before_io() {
|
fn known_postgres_without_feature_is_rejected_before_io() {
|
||||||
let result = poll_ready(crate::Store::open(valid_store_settings()));
|
let result = poll_ready(crate::Store::open(store_settings("postgresql://operator-supplied-sensitive-value@localhost/ksp")));
|
||||||
let error = result.err();
|
let error = result.err();
|
||||||
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
|
assert_eq!(error.map(|value| return value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: crates/ksp-store-postgres-lib/Cargo.toml
|
# file: crates/ksp-store-postgres-lib/Cargo.toml
|
||||||
# version: 1
|
# version: 2
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ksp-store-postgres-lib"
|
name = "ksp-store-postgres-lib"
|
||||||
@@ -8,7 +8,14 @@ edition.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
deadpool-postgres = { workspace = true, features = ["rt_tokio_1"] }
|
||||||
|
ksp-logging-lib = { path = "../ksp-logging-lib" }
|
||||||
ksp-store-api = { path = "../ksp-store-api" }
|
ksp-store-api = { path = "../ksp-store-api" }
|
||||||
|
rustls = { workspace = true, features = ["aws_lc_rs", "std", "tls12"] }
|
||||||
|
rustls-native-certs.workspace = true
|
||||||
|
tokio = { workspace = true, features = ["rt", "time"] }
|
||||||
|
tokio-postgres = { workspace = true, features = ["runtime"] }
|
||||||
|
tokio-postgres-rustls = { workspace = true, features = ["aws-lc-rs"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
45
crates/ksp-store-postgres-lib/src/error.rs
Normal file
45
crates/ksp-store-postgres-lib/src/error.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// file: crates/ksp-store-postgres-lib/src/error.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
/// Safe backend-local classification used by the Store facade for stable error mapping.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum PostgresBackendErrorKind {
|
||||||
|
/// The supplied physical PostgreSQL configuration is unsupported or malformed.
|
||||||
|
ConfigInvalid,
|
||||||
|
/// A physical PostgreSQL connection could not be established.
|
||||||
|
ConnectFailed,
|
||||||
|
/// A bounded pool wait, create or recycle operation reached its deadline.
|
||||||
|
PoolTimeout,
|
||||||
|
/// Explicit backend shutdown did not drain inside the supplied deadline.
|
||||||
|
ShutdownTimeout,
|
||||||
|
/// Verified TLS configuration or negotiation could not be established.
|
||||||
|
TlsFailed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redacted PostgreSQL backend error carrying only a safe classification and static lifecycle phase.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PostgresBackendError {
|
||||||
|
kind: PostgresBackendErrorKind,
|
||||||
|
phase: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresBackendError {
|
||||||
|
/// Creates one backend error without retaining external error text or sensitive connection material.
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) const fn new(kind: PostgresBackendErrorKind, phase: &'static str) -> Self {
|
||||||
|
return Self { kind, phase };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the safe backend-local error classification.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn kind(&self) -> PostgresBackendErrorKind {
|
||||||
|
return self.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the static safe lifecycle phase associated with the failure.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn phase(&self) -> &'static str {
|
||||||
|
return self.phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,36 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
//! Official PostgreSQL backend implementation scaffold for KSP Store.
|
//! Official PostgreSQL backend implementation for KSP Store.
|
||||||
//!
|
//!
|
||||||
//! `0.3.2-pre.002` establishes only the backend crate boundary. PostgreSQL
|
//! `0.3.2-pre.005` owns the physical `tokio-postgres` connection, bounded
|
||||||
//! driver, pooling, TLS, SQL, migrations and health behavior are intentionally
|
//! Deadpool pool and explicit Rustls TLS policy. SQL migrations and business
|
||||||
//! absent until their dedicated prereleases.
|
//! persistence remain absent until their dedicated prereleases.
|
||||||
//!
|
//!
|
||||||
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`, which
|
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
|
||||||
//! keeps backend implementation ownership acyclic and reusable behind the
|
//! common facade consumes only this crate's narrow backend bridge and never
|
||||||
//! common facade.
|
//! exposes PostgreSQL pool, client, row or statement types.
|
||||||
|
|
||||||
mod constants;
|
mod constants;
|
||||||
|
mod error;
|
||||||
|
mod runtime;
|
||||||
|
|
||||||
/// Crate-owned tracing target reserved for later PostgreSQL backend behavior.
|
/// Safe backend-local error returned to the common Store facade.
|
||||||
|
pub use self::error::PostgresBackendError;
|
||||||
|
/// Safe backend-local error classification used by the common Store facade.
|
||||||
|
pub use self::error::PostgresBackendErrorKind;
|
||||||
|
/// Opaque physical PostgreSQL backend owning its connection pool.
|
||||||
|
pub use self::runtime::PostgresBackend;
|
||||||
|
/// Physical PostgreSQL settings bridge consumed only by the backend crate.
|
||||||
|
pub use self::runtime::PostgresBackendSettings;
|
||||||
|
/// TLS mode accepted by the physical PostgreSQL settings bridge.
|
||||||
|
pub use self::runtime::PostgresBackendTlsMode;
|
||||||
|
|
||||||
|
/// Crate-owned tracing target for PostgreSQL backend behavior.
|
||||||
pub(crate) use self::constants::TRACING_TARGET;
|
pub(crate) use self::constants::TRACING_TARGET;
|
||||||
|
|
||||||
// Keep the mandatory crate-owned tracing target part of the compiled scaffold
|
const _: &str = crate::TRACING_TARGET;
|
||||||
// without inventing runtime logging before the first behavioral tranche.
|
|
||||||
const _: &str = TRACING_TARGET;
|
|
||||||
|
|||||||
303
crates/ksp-store-postgres-lib/src/runtime.rs
Normal file
303
crates/ksp-store-postgres-lib/src/runtime.rs
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
const APPLICATION_NAME: &str = "ksp-store";
|
||||||
|
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||||
|
const SHUTDOWN_POLL_INTERVAL_MS: u64 = 10;
|
||||||
|
|
||||||
|
/// TLS mode accepted by the physical PostgreSQL backend bridge.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum PostgresBackendTlsMode {
|
||||||
|
/// Disable TLS for the selected PostgreSQL target.
|
||||||
|
Disabled,
|
||||||
|
/// Require TLS with system-root trust and server-identity verification.
|
||||||
|
VerifyFull,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresBackendTlsMode {
|
||||||
|
/// Returns the stable safe TLS mode code used only in diagnostics.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn code(&self) -> &'static str {
|
||||||
|
return match self {
|
||||||
|
Self::Disabled => "disabled",
|
||||||
|
Self::VerifyFull => "verify_full",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Physical settings consumed only by the PostgreSQL backend crate.
|
||||||
|
pub struct PostgresBackendSettings {
|
||||||
|
connect_timeout: std::time::Duration,
|
||||||
|
connection_uri: std::string::String,
|
||||||
|
create_timeout: std::time::Duration,
|
||||||
|
max_connections: u32,
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
recycle_timeout: std::time::Duration,
|
||||||
|
tls_mode: PostgresBackendTlsMode,
|
||||||
|
wait_timeout: std::time::Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresBackendSettings {
|
||||||
|
/// Creates the physical PostgreSQL settings bridge from already validated facade-owned values.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
connection_uri: impl std::convert::Into<std::string::String>,
|
||||||
|
max_connections: u32,
|
||||||
|
connect_timeout: std::time::Duration,
|
||||||
|
wait_timeout: std::time::Duration,
|
||||||
|
create_timeout: std::time::Duration,
|
||||||
|
recycle_timeout: std::time::Duration,
|
||||||
|
tls_mode: PostgresBackendTlsMode,
|
||||||
|
) -> Self {
|
||||||
|
return Self {
|
||||||
|
connect_timeout,
|
||||||
|
connection_uri: connection_uri.into(),
|
||||||
|
create_timeout,
|
||||||
|
max_connections,
|
||||||
|
network,
|
||||||
|
recycle_timeout,
|
||||||
|
tls_mode,
|
||||||
|
wait_timeout,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the logical network bound to this one backend instance.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn network(&self) -> &ksp_store_api::RawNetworkId {
|
||||||
|
return &self.network;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the selected safe TLS mode.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn tls_mode(&self) -> PostgresBackendTlsMode {
|
||||||
|
return self.tls_mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PostgresBackendSettings {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter
|
||||||
|
.debug_struct("PostgresBackendSettings")
|
||||||
|
.field("network", &self.network)
|
||||||
|
.field("connection_uri", &"<redacted>")
|
||||||
|
.field("max_connections", &self.max_connections)
|
||||||
|
.field("connect_timeout", &self.connect_timeout)
|
||||||
|
.field("wait_timeout", &self.wait_timeout)
|
||||||
|
.field("create_timeout", &self.create_timeout)
|
||||||
|
.field("recycle_timeout", &self.recycle_timeout)
|
||||||
|
.field("tls_mode", &self.tls_mode)
|
||||||
|
.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opaque physical PostgreSQL backend owning the bounded Deadpool connection pool.
|
||||||
|
pub struct PostgresBackend {
|
||||||
|
network: ksp_store_api::RawNetworkId,
|
||||||
|
pool: deadpool_postgres::Pool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresBackend {
|
||||||
|
/// Parses and normalizes one supplied URI, builds a bounded pool and proves one physical connection before returning readiness.
|
||||||
|
pub async fn open(settings: PostgresBackendSettings) -> std::result::Result<Self, crate::PostgresBackendError> {
|
||||||
|
let normalized = normalized_config(&settings);
|
||||||
|
let pg_config = match normalized {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
network = settings.network().as_str(),
|
||||||
|
tls_mode = settings.tls_mode().code(),
|
||||||
|
max_connections = settings.max_connections,
|
||||||
|
"opening PostgreSQL Store backend pool"
|
||||||
|
);
|
||||||
|
let pool_result = match settings.tls_mode {
|
||||||
|
PostgresBackendTlsMode::Disabled => build_pool(pg_config, tokio_postgres::NoTls, &settings),
|
||||||
|
PostgresBackendTlsMode::VerifyFull => {
|
||||||
|
let tls_result = build_verified_tls();
|
||||||
|
let tls = match tls_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
build_pool(pg_config, tls, &settings)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let pool = match pool_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let probe = pool.get().await;
|
||||||
|
match probe {
|
||||||
|
std::result::Result::Ok(client) => drop(client),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(map_pool_error(error)),
|
||||||
|
}
|
||||||
|
ksp_logging_lib::debug!(
|
||||||
|
target: crate::TRACING_TARGET,
|
||||||
|
network = settings.network().as_str(),
|
||||||
|
tls_mode = settings.tls_mode().code(),
|
||||||
|
"PostgreSQL Store backend established initial physical connection"
|
||||||
|
);
|
||||||
|
return std::result::Result::Ok(Self { network: settings.network, pool });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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();
|
||||||
|
let drain = async {
|
||||||
|
loop {
|
||||||
|
if self.pool.status().size == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(SHUTDOWN_POLL_INTERVAL_MS)).await;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result = tokio::time::timeout(timeout, drain).await;
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(()) => {
|
||||||
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, network = self.network.as_str(), "PostgreSQL Store backend pool closed");
|
||||||
|
std::result::Result::Ok(())
|
||||||
|
},
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ShutdownTimeout, "pool_drain"))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for PostgresBackend {
|
||||||
|
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
return formatter.debug_struct("PostgresBackend").field("network", &self.network).field("state", &"open").finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::Drop for PostgresBackend {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.pool.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalized_config(settings: &PostgresBackendSettings) -> std::result::Result<tokio_postgres::Config, crate::PostgresBackendError> {
|
||||||
|
if settings.connection_uri.is_empty() || settings.connection_uri.len() > MAX_CONNECTION_URI_BYTES {
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
|
||||||
|
}
|
||||||
|
let parsed = settings.connection_uri.parse::<tokio_postgres::Config>();
|
||||||
|
let mut config = match parsed {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "connection_uri"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if config.get_options().is_some() {
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "server_options"));
|
||||||
|
}
|
||||||
|
if config.get_hosts().is_empty() && config.get_hostaddrs().is_empty() {
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "host"));
|
||||||
|
}
|
||||||
|
if settings.tls_mode == PostgresBackendTlsMode::VerifyFull {
|
||||||
|
if config.get_hosts().is_empty() {
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
|
||||||
|
}
|
||||||
|
for host in config.get_hosts() {
|
||||||
|
if !is_tcp_host(host) {
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "tls_server_identity"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.application_name(APPLICATION_NAME);
|
||||||
|
config.connect_timeout(settings.connect_timeout);
|
||||||
|
config.ssl_negotiation(tokio_postgres::config::SslNegotiation::Postgres);
|
||||||
|
match settings.tls_mode {
|
||||||
|
PostgresBackendTlsMode::Disabled => {
|
||||||
|
config.ssl_mode(tokio_postgres::config::SslMode::Disable);
|
||||||
|
},
|
||||||
|
PostgresBackendTlsMode::VerifyFull => {
|
||||||
|
config.ssl_mode(tokio_postgres::config::SslMode::Require);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_tcp_host(host: &tokio_postgres::config::Host) -> bool {
|
||||||
|
return match host {
|
||||||
|
tokio_postgres::config::Host::Tcp(_) => true,
|
||||||
|
#[cfg(unix)]
|
||||||
|
tokio_postgres::config::Host::Unix(_) => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_pool<T>(
|
||||||
|
pg_config: tokio_postgres::Config,
|
||||||
|
tls: T,
|
||||||
|
settings: &PostgresBackendSettings,
|
||||||
|
) -> std::result::Result<deadpool_postgres::Pool, crate::PostgresBackendError>
|
||||||
|
where
|
||||||
|
T: tokio_postgres::tls::MakeTlsConnect<tokio_postgres::Socket> + std::clone::Clone + std::marker::Send + std::marker::Sync + 'static,
|
||||||
|
T::Stream: std::marker::Send + std::marker::Sync,
|
||||||
|
T::TlsConnect: std::marker::Send + std::marker::Sync,
|
||||||
|
<T::TlsConnect as tokio_postgres::tls::TlsConnect<tokio_postgres::Socket>>::Future: std::marker::Send,
|
||||||
|
{
|
||||||
|
let manager_config = deadpool_postgres::ManagerConfig { recycling_method: deadpool_postgres::RecyclingMethod::Verified };
|
||||||
|
let manager = deadpool_postgres::Manager::from_config(pg_config, tls, manager_config);
|
||||||
|
let built = deadpool_postgres::Pool::builder(manager)
|
||||||
|
.max_size(settings.max_connections as usize)
|
||||||
|
.wait_timeout(std::option::Option::Some(settings.wait_timeout))
|
||||||
|
.create_timeout(std::option::Option::Some(settings.create_timeout))
|
||||||
|
.recycle_timeout(std::option::Option::Some(settings.recycle_timeout))
|
||||||
|
.runtime(deadpool_postgres::Runtime::Tokio1)
|
||||||
|
.build();
|
||||||
|
return match built {
|
||||||
|
std::result::Result::Ok(pool) => std::result::Result::Ok(pool),
|
||||||
|
std::result::Result::Err(_) => std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_build")),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_verified_tls() -> std::result::Result<tokio_postgres_rustls::MakeRustlsConnect, crate::PostgresBackendError> {
|
||||||
|
let native = rustls_native_certs::load_native_certs();
|
||||||
|
let native_error_count = native.errors.len();
|
||||||
|
if native.certs.is_empty() {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, "no system TLS roots available for PostgreSQL verify_full");
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
|
||||||
|
}
|
||||||
|
let mut roots = rustls::RootCertStore::empty();
|
||||||
|
let (added, ignored) = roots.add_parsable_certificates(native.certs);
|
||||||
|
if added == 0 {
|
||||||
|
ksp_logging_lib::warn!(target: crate::TRACING_TARGET, native_error_count, ignored, "system TLS roots could not be admitted for PostgreSQL verify_full");
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "native_roots"));
|
||||||
|
}
|
||||||
|
if native_error_count > 0 || ignored > 0 {
|
||||||
|
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, added, ignored, native_error_count, "loaded PostgreSQL system TLS roots with partial diagnostics");
|
||||||
|
}
|
||||||
|
let provider = std::sync::Arc::new(rustls::crypto::aws_lc_rs::default_provider());
|
||||||
|
let builder_result = rustls::ClientConfig::builder_with_provider(provider).with_safe_default_protocol_versions();
|
||||||
|
let builder = match builder_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(_) => {
|
||||||
|
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::TlsFailed, "protocol_versions"));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let client_config = builder.with_root_certificates(roots).with_no_client_auth();
|
||||||
|
return std::result::Result::Ok(tokio_postgres_rustls::MakeRustlsConnect::new(client_config));
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
match timeout_type {
|
||||||
|
deadpool_postgres::TimeoutType::Wait => "pool_wait",
|
||||||
|
deadpool_postgres::TimeoutType::Create => "pool_create",
|
||||||
|
deadpool_postgres::TimeoutType::Recycle => "pool_recycle",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
deadpool_postgres::PoolError::Backend(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "physical_connect"),
|
||||||
|
deadpool_postgres::PoolError::Closed => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_closed"),
|
||||||
|
deadpool_postgres::PoolError::NoRuntimeSpecified => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConfigInvalid, "pool_runtime"),
|
||||||
|
deadpool_postgres::PoolError::PostCreateHook(_) => crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ConnectFailed, "pool_post_create"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "../unit_tests/runtime.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -1,36 +1,60 @@
|
|||||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
//! Dependency-boundary canaries for the PostgreSQL Store backend scaffold.
|
//! Dependency and ownership canaries for the physical PostgreSQL Store backend.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_backend_depends_on_store_api_without_reverse_facade_edge() {
|
fn pre_005_backend_owns_exact_physical_runtime_dependencies_without_reverse_facade_edge() {
|
||||||
let manifest = include_str!("../Cargo.toml");
|
let manifest = include_str!("../Cargo.toml");
|
||||||
assert!(manifest.contains("ksp-store-api = { path = \"../ksp-store-api\" }"));
|
for required in ["deadpool-postgres", "ksp-logging-lib", "ksp-store-api", "rustls", "rustls-native-certs", "tokio-postgres", "tokio-postgres-rustls"] {
|
||||||
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib"] {
|
assert!(manifest.contains(required), "missing PostgreSQL backend dependency: {required}");
|
||||||
|
}
|
||||||
|
for forbidden in ["ksp-store-lib", "ksp-config-lib", "ksp-materializer", "ksp-program", "ksp-onchain-transport-lib", "ksp-offchain-transport-lib", "sqlx"] {
|
||||||
assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
|
assert!(!manifest.contains(forbidden), "forbidden PostgreSQL backend dependency detected: {forbidden}");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_002_backend_does_not_advance_connection_pool_tls_or_migrations() {
|
fn pre_005_backend_keeps_environment_sql_migrations_and_physical_types_private() {
|
||||||
let manifest = include_str!("../Cargo.toml");
|
|
||||||
for forbidden in ["tokio", "tokio-postgres", "deadpool-postgres", "tokio-postgres-rustls", "rustls", "sha2"] {
|
|
||||||
assert!(!manifest.contains(forbidden), "premature PostgreSQL runtime dependency detected: {forbidden}");
|
|
||||||
}
|
|
||||||
let crate_root = include_str!("../src/lib.rs");
|
let crate_root = include_str!("../src/lib.rs");
|
||||||
assert!(crate_root.contains("mod constants;"));
|
assert!(crate_root.contains("mod error;"));
|
||||||
assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;"));
|
assert!(crate_root.contains("mod runtime;"));
|
||||||
assert!(crate_root.contains("const _: &str = TRACING_TARGET;"));
|
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
||||||
for forbidden in ["pub mod ", "pub struct", "pub enum", "pub trait", "tokio_postgres", "deadpool_postgres", "mod migration", "SELECT ", "CREATE TABLE"] {
|
for forbidden in [
|
||||||
assert!(!crate_root.contains(forbidden), "premature PostgreSQL backend surface detected: {forbidden}");
|
"pub mod ",
|
||||||
|
"ksp_store_lib",
|
||||||
|
"ksp_config_lib",
|
||||||
|
"tokio_postgres::Client",
|
||||||
|
"tokio_postgres::Row",
|
||||||
|
"tokio_postgres::Statement",
|
||||||
|
"deadpool_postgres::Pool;",
|
||||||
|
] {
|
||||||
|
assert!(!crate_root.contains(forbidden), "forbidden PostgreSQL crate-root surface detected: {forbidden}");
|
||||||
|
}
|
||||||
|
let runtime = include_str!("../src/runtime.rs");
|
||||||
|
for forbidden in [
|
||||||
|
"std::env",
|
||||||
|
"dotenv",
|
||||||
|
"KSP_",
|
||||||
|
"KSPB_",
|
||||||
|
"PGHOST",
|
||||||
|
"PGPORT",
|
||||||
|
"PGUSER",
|
||||||
|
"PGPASSWORD",
|
||||||
|
".pgpass",
|
||||||
|
"CREATE TABLE",
|
||||||
|
"INSERT INTO",
|
||||||
|
"UPDATE ",
|
||||||
|
"DELETE FROM",
|
||||||
|
"SELECT ",
|
||||||
|
"mod migration",
|
||||||
|
] {
|
||||||
|
assert!(!runtime.contains(forbidden), "forbidden PostgreSQL backend ownership/scope content detected: {forbidden}");
|
||||||
}
|
}
|
||||||
let constants = include_str!("../src/constants.rs");
|
|
||||||
assert!(constants.contains("pub(crate) const TRACING_TARGET: &str = \"ksp-store-postgres-lib\";"));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
44
crates/ksp-store-postgres-lib/tests/public_api.rs
Normal file
44
crates/ksp-store-postgres-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
#![warn(missing_docs)]
|
||||||
|
#![deny(unreachable_pub)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
//! Narrow physical bridge canaries consumed by `ksp-store-lib` without exposing driver or pool types.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_005_backend_bridge_is_constructible_without_io() {
|
||||||
|
let network = match ksp_store_api::RawNetworkId::new("devnet") {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("valid backend bridge network rejected: {error:?}"),
|
||||||
|
};
|
||||||
|
let settings = ksp_store_postgres_lib::PostgresBackendSettings::new(
|
||||||
|
network,
|
||||||
|
"postgresql://operator:secret@localhost/ksp",
|
||||||
|
8,
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
ksp_store_postgres_lib::PostgresBackendTlsMode::VerifyFull,
|
||||||
|
);
|
||||||
|
assert_eq!(settings.network().as_str(), "devnet");
|
||||||
|
assert_eq!(settings.tls_mode().code(), "verify_full");
|
||||||
|
let _open = ksp_store_postgres_lib::PostgresBackend::open;
|
||||||
|
let _close = ksp_store_postgres_lib::PostgresBackend::close;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pre_005_backend_error_projection_is_safe_and_static() {
|
||||||
|
let kinds = [
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ConfigInvalid,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ConnectFailed,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::PoolTimeout,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::ShutdownTimeout,
|
||||||
|
ksp_store_postgres_lib::PostgresBackendErrorKind::TlsFailed,
|
||||||
|
];
|
||||||
|
assert_eq!(kinds.len(), 5);
|
||||||
|
return;
|
||||||
|
}
|
||||||
99
crates/ksp-store-postgres-lib/unit_tests/runtime.rs
Normal file
99
crates/ksp-store-postgres-lib/unit_tests/runtime.rs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// file: crates/ksp-store-postgres-lib/unit_tests/runtime.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 backend test network rejected: {error:?}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settings(connection_uri: &str, tls_mode: crate::PostgresBackendTlsMode) -> crate::PostgresBackendSettings {
|
||||||
|
return crate::PostgresBackendSettings::new(
|
||||||
|
network(),
|
||||||
|
connection_uri,
|
||||||
|
8,
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
std::time::Duration::from_secs(5),
|
||||||
|
tls_mode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn physical_settings_debug_redacts_connection_uri() {
|
||||||
|
let secret = "postgresql://secret-user:secret-password@localhost/ksp";
|
||||||
|
let value = settings(secret, crate::PostgresBackendTlsMode::VerifyFull);
|
||||||
|
let rendered = format!("{value:?}");
|
||||||
|
assert!(!rendered.contains("secret-user"));
|
||||||
|
assert!(!rendered.contains("secret-password"));
|
||||||
|
assert!(rendered.contains("<redacted>"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_or_oversized_uri_is_rejected_without_retaining_input() {
|
||||||
|
let malformed = "not-a-postgresql-uri-secret-canary";
|
||||||
|
let malformed_result = super::normalized_config(&settings(malformed, crate::PostgresBackendTlsMode::Disabled));
|
||||||
|
let malformed_error = malformed_result.err();
|
||||||
|
assert_eq!(malformed_error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||||
|
assert!(!format!("{malformed_error:?}").contains("secret-canary"));
|
||||||
|
let oversized = "x".repeat(super::MAX_CONNECTION_URI_BYTES + 1);
|
||||||
|
let oversized_result = super::normalized_config(&settings(oversized.as_str(), crate::PostgresBackendTlsMode::Disabled));
|
||||||
|
assert_eq!(oversized_result.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_disabled_policy_overrides_uri_tls_and_connection_controls() {
|
||||||
|
let value = settings(
|
||||||
|
"postgresql://operator:secret@localhost/ksp?sslmode=require&application_name=hostile&connect_timeout=1&sslnegotiation=direct",
|
||||||
|
crate::PostgresBackendTlsMode::Disabled,
|
||||||
|
);
|
||||||
|
let config = match super::normalized_config(&value) {
|
||||||
|
std::result::Result::Ok(config) => config,
|
||||||
|
std::result::Result::Err(error) => panic!("valid disabled config rejected: {error:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Disable);
|
||||||
|
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
|
||||||
|
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
|
||||||
|
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(std::time::Duration::from_secs(10)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_verify_full_policy_forces_tls_and_rejects_hostaddr_only_identity() {
|
||||||
|
let value = settings(
|
||||||
|
"postgresql://operator:secret@localhost/ksp?sslmode=disable&application_name=hostile&connect_timeout=1",
|
||||||
|
crate::PostgresBackendTlsMode::VerifyFull,
|
||||||
|
);
|
||||||
|
let config = match super::normalized_config(&value) {
|
||||||
|
std::result::Result::Ok(config) => config,
|
||||||
|
std::result::Result::Err(error) => panic!("valid verify_full config rejected: {error:?}"),
|
||||||
|
};
|
||||||
|
assert_eq!(config.get_ssl_mode(), tokio_postgres::config::SslMode::Require);
|
||||||
|
assert_eq!(config.get_ssl_negotiation(), tokio_postgres::config::SslNegotiation::Postgres);
|
||||||
|
assert_eq!(config.get_application_name(), std::option::Option::Some(super::APPLICATION_NAME));
|
||||||
|
assert_eq!(config.get_connect_timeout(), std::option::Option::Some(std::time::Duration::from_secs(10)));
|
||||||
|
let hostaddr_only = settings("hostaddr=127.0.0.1 user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
|
||||||
|
let rejected = super::normalized_config(&hostaddr_only);
|
||||||
|
assert_eq!(rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let unix_socket = settings("host=/var/run/postgresql user=operator dbname=ksp", crate::PostgresBackendTlsMode::VerifyFull);
|
||||||
|
let unix_rejected = super::normalized_config(&unix_socket);
|
||||||
|
assert_eq!(unix_rejected.err().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn libpq_server_options_are_rejected_in_foundation_runtime() {
|
||||||
|
let value = settings("host=localhost user=operator dbname=ksp options='-c statement_timeout=0'", crate::PostgresBackendTlsMode::Disabled);
|
||||||
|
let rejected = super::normalized_config(&value);
|
||||||
|
let error = rejected.err();
|
||||||
|
assert_eq!(error.as_ref().map(|value| return value.kind()), std::option::Option::Some(crate::PostgresBackendErrorKind::ConfigInvalid));
|
||||||
|
assert_eq!(error.map(|value| return value.phase()), std::option::Option::Some("server_options"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
135
deltas/0.3.2/pre.005.md
Normal file
135
deltas/0.3.2/pre.005.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<!-- file: deltas/0.3.2/pre.005.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# Delta `0.3.2-pre.005` — PostgreSQL connection + pool + TLS
|
||||||
|
|
||||||
|
## 1. Base
|
||||||
|
|
||||||
|
Base exacte : `0.3.2-pre.004-fix.001`, incluant l'ajustement opérateur de `.env.example` version `13`.
|
||||||
|
|
||||||
|
Le gate opérateur du 29 août 2026 est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests Config, ownership, Store avec et sans feature PostgreSQL, compilation `--no-default-features` et Config Desk passent.
|
||||||
|
|
||||||
|
## 2. Objet
|
||||||
|
|
||||||
|
Matérialiser la première ouverture PostgreSQL physique sans ouvrir encore les migrations ou le SQL métier :
|
||||||
|
|
||||||
|
```text
|
||||||
|
tokio-postgres 0.7.18+
|
||||||
|
deadpool-postgres 0.14.x
|
||||||
|
tokio-postgres-rustls 0.14.x
|
||||||
|
rustls 0.23.x / AWS-LC
|
||||||
|
roots système
|
||||||
|
pool borné
|
||||||
|
Store::open physique
|
||||||
|
Store::close borné
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Frontière façade/backend
|
||||||
|
|
||||||
|
`ksp-store-lib` reste l'unique surface consumer et ne dépend directement d'aucun driver/pool/TLS. Il convertit ses settings vers un bridge `ksp-store-postgres-lib`, puis mappe les erreurs backend vers des `ErrorCode` Store stables.
|
||||||
|
|
||||||
|
`ksp-store-postgres-lib` possède seul :
|
||||||
|
|
||||||
|
```text
|
||||||
|
parse/normalisation tokio_postgres::Config
|
||||||
|
Deadpool Manager/Pool
|
||||||
|
connector Rustls
|
||||||
|
roots système
|
||||||
|
première acquisition physique
|
||||||
|
fermeture/drain physique
|
||||||
|
```
|
||||||
|
|
||||||
|
Les types physiques `Pool`, `Client`, `Row` et `Statement` ne sont jamais réexportés par la façade.
|
||||||
|
|
||||||
|
## 4. Normalisation de l'URI
|
||||||
|
|
||||||
|
Le backend consomme uniquement l'URI déjà résolue par Config. Après parsing, KSP impose :
|
||||||
|
|
||||||
|
```text
|
||||||
|
application_name = ksp-store
|
||||||
|
connect_timeout = settings typés
|
||||||
|
sslnegotiation = postgres
|
||||||
|
Disabled -> sslmode=disable
|
||||||
|
VerifyFull -> sslmode=require
|
||||||
|
```
|
||||||
|
|
||||||
|
`options=` est rejeté dans cette fondation. `VerifyFull` exige un host TCP afin de disposer d'une identité serveur à vérifier : `hostaddr` seul et les sockets Unix sont rejetés. Les erreurs de parsing ne conservent jamais le texte de l'URI.
|
||||||
|
|
||||||
|
## 5. Pool et lifecycle
|
||||||
|
|
||||||
|
Le pool applique les limites validées en `pre.003` : taille maximale et deadlines `wait/create/recycle`. Le manager utilise `RecyclingMethod::Verified`; la requête de vérification éventuelle reste interne à Deadpool et aucun SQL KSP n'est ajouté.
|
||||||
|
|
||||||
|
La création du pool est lazy, donc `Store::open` exécute explicitement une première `pool.get().await`. Le succès signifie ainsi qu'une connexion physique/auth/TLS a réellement été établie.
|
||||||
|
|
||||||
|
`Store::close(self)` appelle `Pool::close()` puis attend un drain `size == 0` sous `shutdown_timeout`. `Drop` ne fait qu'un close best-effort.
|
||||||
|
|
||||||
|
## 6. TLS
|
||||||
|
|
||||||
|
Modes exacts :
|
||||||
|
|
||||||
|
```text
|
||||||
|
Disabled
|
||||||
|
VerifyFull
|
||||||
|
```
|
||||||
|
|
||||||
|
`VerifyFull` charge les roots système dans `rustls::RootCertStore`, utilise explicitement le provider AWS-LC et conserve la vérification standard de certificat + identité serveur. Aucun mode `Prefer`, aucun verifier permissif et aucun fallback plaintext ne sont introduits.
|
||||||
|
|
||||||
|
## 7. Erreurs sûres
|
||||||
|
|
||||||
|
Le backend retourne uniquement une classification et une phase statique :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ConfigInvalid
|
||||||
|
ConnectFailed
|
||||||
|
PoolTimeout
|
||||||
|
ShutdownTimeout
|
||||||
|
TlsFailed
|
||||||
|
```
|
||||||
|
|
||||||
|
La façade mappe vers :
|
||||||
|
|
||||||
|
```text
|
||||||
|
store.postgres_config_invalid
|
||||||
|
store.postgres_connect_failed
|
||||||
|
store.postgres_pool_timeout
|
||||||
|
store.postgres_tls_failed
|
||||||
|
store.shutdown_timeout
|
||||||
|
```
|
||||||
|
|
||||||
|
Aucune erreur `tokio-postgres`, Deadpool, Rustls ou native-certs n'est attachée comme source publique.
|
||||||
|
|
||||||
|
## 8. Hors scope
|
||||||
|
|
||||||
|
```text
|
||||||
|
migrations/bootstrap SQL
|
||||||
|
ksp_store_schema_migrations
|
||||||
|
health snapshot public
|
||||||
|
RAW transaction/account persistence
|
||||||
|
routing multi-target/multi-réseau dans Store
|
||||||
|
live PostgreSQL integration test
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces éléments restent respectivement aux tranches `pre.006`, `pre.007`, `0.3.3/0.3.4` et `pre.008`.
|
||||||
|
|
||||||
|
## 9. Version
|
||||||
|
|
||||||
|
```text
|
||||||
|
workspace.package.version = 0.3.2-pre.5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. Gate opérateur requis
|
||||||
|
|
||||||
|
```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
|
||||||
|
cargo tree --duplicates
|
||||||
|
```
|
||||||
@@ -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: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Plan `0.3.2` — Store/PostgreSQL runtime foundation
|
# Plan `0.3.2` — Store/PostgreSQL runtime foundation
|
||||||
|
|
||||||
@@ -963,7 +963,22 @@ Document/schema/example/registry/adaptor, `.env.example`, provenance/sensitivity
|
|||||||
|
|
||||||
### `pre.005` — PostgreSQL connection + deadpool + Rustls
|
### `pre.005` — PostgreSQL connection + deadpool + Rustls
|
||||||
|
|
||||||
Implémenter parse/normalisation URI, pool borné, connect/open/close, VerifyFull/Disabled, timeouts et erreurs redacted. Aucun SQL métier.
|
Matérialiser le bridge physique backend sans modifier la façade publique backend-neutral :
|
||||||
|
|
||||||
|
```text
|
||||||
|
ksp-store-lib
|
||||||
|
-> PostgresBackendSettings privé à la composition
|
||||||
|
-> PostgresBackend::open/close
|
||||||
|
|
||||||
|
ksp-store-postgres-lib
|
||||||
|
-> tokio-postgres >= 0.7.18
|
||||||
|
-> deadpool-postgres ^0.14.2
|
||||||
|
-> tokio-postgres-rustls 0.14.x
|
||||||
|
-> rustls 0.23.x + AWS-LC
|
||||||
|
-> roots système via rustls-native-certs
|
||||||
|
```
|
||||||
|
|
||||||
|
Après parsing, la policy KSP réécrit `application_name`, `connect_timeout`, `sslnegotiation` et `sslmode`; les `options=` serveur sont rejetées. `Disabled` force le plaintext, `VerifyFull` force TLS + roots système + identité serveur et interdit un target `hostaddr` sans `host` ainsi que les sockets Unix, sur lesquels PostgreSQL ne négocie pas TLS. Le pool applique `max_size` et deadlines `wait/create/recycle`, utilise le recycling `Verified`, et `Store::open` ne réussit qu'après une première acquisition physique. Les erreurs externes ne sont jamais conservées comme source/texte public. `Store::close` ferme et draine sous deadline; `Drop` reste best-effort. Aucun SQL KSP, migration ou table métier n'est introduit.
|
||||||
|
|
||||||
### `pre.006` — Migration/bootstrap foundation
|
### `pre.006` — Migration/bootstrap foundation
|
||||||
|
|
||||||
@@ -1051,8 +1066,6 @@ Aucune question architecturale ne bloque `pre.002`.
|
|||||||
Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate :
|
Les détails suivants sont réservés à leur tranche sans rouvrir les décisions du gate :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
mapping exact deadpool timeouts en pre.005
|
|
||||||
construction exacte du rustls RootCertStore en pre.005
|
|
||||||
DDL précis de ksp_store_schema_migrations en pre.006
|
DDL précis de ksp_store_schema_migrations en pre.006
|
||||||
forme finale des snapshots health en pre.007
|
forme finale des snapshots health en pre.007
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
|
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
|
||||||
|
|
||||||
@@ -74,6 +74,8 @@ Le gate opérateur de `pre.002-fix.001`, fourni le 29 août 2026, est entièreme
|
|||||||
|
|
||||||
Le gate opérateur de `pre.003-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-lib` avec et sans feature par défaut, tests backend et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.004`.
|
Le gate opérateur de `pre.003-fix.001`, fourni le 29 août 2026, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, tests `ksp-store-lib` avec et sans feature par défaut, tests backend et compilation `--no-default-features` passent. Cette base est l'entrée effective de `pre.004`.
|
||||||
|
|
||||||
|
Le gate opérateur de `pre.004-fix.001`, après correction manuelle des commentaires `.env.example`, est entièrement vert : audits Rust/Markdown, workspace check/Clippy, 126 tests `ksp-config-lib`, ownership, `ksp-store-lib` avec et sans feature PostgreSQL, compilation `--no-default-features` et 63 tests Config Desk passent. Cette base est l'entrée effective de `pre.005`.
|
||||||
|
|
||||||
## 3. Frontières Cargo
|
## 3. Frontières Cargo
|
||||||
|
|
||||||
### V32-DEP-001 — Façade -> API
|
### V32-DEP-001 — Façade -> API
|
||||||
@@ -134,7 +136,9 @@ Store/backend -> Materializer
|
|||||||
consumer ordinaire -> ksp-store-postgres-lib
|
consumer ordinaire -> ksp-store-postgres-lib
|
||||||
```
|
```
|
||||||
|
|
||||||
Statut : `TODO pre.009`.
|
Matérialisé par `pre.005` : les types physiques nécessaires sont publics uniquement dans la crate backend pour la frontière inter-crates et ne sont jamais réexportés par `ksp-store-lib`; `Pool/Client/Row/Statement` restent absents de sa crate-root.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.005 / TODO pre.009`.
|
||||||
|
|
||||||
### V32-DEP-006 — `ksp-store-api` non régressé
|
### V32-DEP-006 — `ksp-store-api` non régressé
|
||||||
|
|
||||||
@@ -199,9 +203,9 @@ close borné
|
|||||||
Drop best-effort seulement
|
Drop best-effort seulement
|
||||||
```
|
```
|
||||||
|
|
||||||
`pre.003` fixe les signatures `Store::open(settings).await` et `Store::close(self).await` et garde `Store` opaque/non constructible par un consumer. Aucun succès d'ouverture n'est simulé avant la connexion réelle : le backend compilé s'arrête avec `store.backend_open_failed` et le contexte sûr `runtime_foundation_pending`.
|
`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 et le staging ; `TODO pre.007` pour le shutdown physique borné.
|
Statut : `PASS pre.003-fix.001 opérateur` pour les signatures / `TODO gate opérateur pre.005` pour l'ouverture et le shutdown physiques / `TODO pre.007` pour la composition health.
|
||||||
|
|
||||||
## 5. Config ownership
|
## 5. Config ownership
|
||||||
|
|
||||||
@@ -220,7 +224,7 @@ backend postgres explicite
|
|||||||
|
|
||||||
Matérialisé par `pre.004` puis corrigé par `pre.004-fix.001` avec `cfg.std.store` / `schema.std.store`, trois targets committed `devnet`/`mainnet`/`testnet`, `default_profile = devnet`, un `network` explicite par target et des URI PostgreSQL séparées.
|
Matérialisé par `pre.004` puis corrigé par `pre.004-fix.001` avec `cfg.std.store` / `schema.std.store`, trois targets committed `devnet`/`mainnet`/`testnet`, `default_profile = devnet`, un `network` explicite par target et des URI PostgreSQL séparées.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.004-fix.001`.
|
Statut : `PASS pre.004-fix.001 opérateur`.
|
||||||
|
|
||||||
### V32-CONFIG-002 — Secrets/provenance
|
### V32-CONFIG-002 — Secrets/provenance
|
||||||
|
|
||||||
@@ -235,7 +239,7 @@ dotenv inventory à jour
|
|||||||
|
|
||||||
Matérialisé par `pre.004-fix.001` : les trois URI réseau-spécifiques sont inventoriées, chaque fallback reste `Secret`, la safe projection est redacted et la provenance n'embarque aucune valeur.
|
Matérialisé par `pre.004-fix.001` : les trois URI réseau-spécifiques sont inventoriées, chaque fallback reste `Secret`, la safe projection est redacted et la provenance n'embarque aucune valeur.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.004-fix.001`.
|
Statut : `PASS pre.004-fix.001 opérateur`.
|
||||||
|
|
||||||
### V32-CONFIG-002B — Target/réseau sans multiplexage
|
### V32-CONFIG-002B — Target/réseau sans multiplexage
|
||||||
|
|
||||||
@@ -252,7 +256,7 @@ future RAW mismatch Store.network != entity/query.network rejeté avant I/O
|
|||||||
|
|
||||||
Matérialisé contractuellement par `pre.004-fix.001`; l'enforcement sur opérations RAW sera exercé dans `0.3.3`/`0.3.4`.
|
Matérialisé contractuellement par `pre.004-fix.001`; l'enforcement sur opérations RAW sera exercé dans `0.3.3`/`0.3.4`.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.004-fix.001 / TODO 0.3.3-0.3.4`.
|
Statut : `PASS pre.004-fix.001 opérateur` pour la sélection target/réseau / `TODO 0.3.3-0.3.4` pour le mismatch RAW.
|
||||||
|
|
||||||
### V32-CONFIG-003 — No-env Store/backend
|
### V32-CONFIG-003 — No-env Store/backend
|
||||||
|
|
||||||
@@ -269,7 +273,7 @@ PG*
|
|||||||
|
|
||||||
`pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; `pre.004-fix.001` conserve cette frontière tout en ajoutant le réseau au contrat `StoreSettings`. Store/backend restent sans dépendance Config et sans lecture KSP/KSPB.
|
`pre.004` renforce aussi le canari d'ownership avec les nouveaux filenames Store ; `pre.004-fix.001` conserve cette frontière tout en ajoutant le réseau au contrat `StoreSettings`. Store/backend restent sans dépendance Config et sans lecture KSP/KSPB.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.004-fix.001 / TODO pre.009`.
|
Statut : `PASS pre.004-fix.001 opérateur / TODO pre.009`.
|
||||||
|
|
||||||
### V32-CONFIG-004 — Adapter Config -> Store
|
### V32-CONFIG-004 — Adapter Config -> Store
|
||||||
|
|
||||||
@@ -277,7 +281,7 @@ Critère : `ksp-config-lib` seul transforme un profil/target résolu en `StoreSe
|
|||||||
|
|
||||||
Matérialisé par `pre.004` puis `pre.004-fix.001` : seul `ksp-config-lib` dépend de `ksp-store-lib` avec `default-features = false`; il mappe le `profile_id` sélectionné vers un target, construit son `RawNetworkId` et ses settings backend sans forcer la feature backend.
|
Matérialisé par `pre.004` puis `pre.004-fix.001` : seul `ksp-config-lib` dépend de `ksp-store-lib` avec `default-features = false`; il mappe le `profile_id` sélectionné vers un target, construit son `RawNetworkId` et ses settings backend sans forcer la feature backend.
|
||||||
|
|
||||||
Statut : `TODO gate opérateur pre.004-fix.001`.
|
Statut : `PASS pre.004-fix.001 opérateur`.
|
||||||
|
|
||||||
## 6. Pool et lifecycle PostgreSQL
|
## 6. Pool et lifecycle PostgreSQL
|
||||||
|
|
||||||
@@ -292,19 +296,25 @@ aucune taille zéro
|
|||||||
aucune valeur pathologique
|
aucune valeur pathologique
|
||||||
```
|
```
|
||||||
|
|
||||||
Statut : `TODO pre.005`.
|
Matérialisé par `pre.005` : `deadpool-postgres` possède un pool `max_size` explicite et applique les deadlines `wait/create/recycle`; `tokio-postgres::Config` reçoit aussi le `connect_timeout` typé après parsing de l'URI.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.005`.
|
||||||
|
|
||||||
### V32-POOL-002 — Connection task ownership
|
### V32-POOL-002 — Connection task ownership
|
||||||
|
|
||||||
Critère : chaque connection future tokio-postgres est pilotée par le manager retenu et son task handle reste possédé jusqu'au drop/close.
|
Critère : chaque connection future tokio-postgres est pilotée par le manager retenu et son task handle reste possédé jusqu'au drop/close.
|
||||||
|
|
||||||
Statut : `TODO pre.005/pre.009`.
|
`pre.005` délègue la création/recycle des connexions au `Manager` Deadpool retenu ; aucun `tokio::spawn` KSP n'est introduit dans Store. La preuve de non-régression détaillée reste au hardening.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.005 / TODO pre.009`.
|
||||||
|
|
||||||
### V32-POOL-003 — Open failure safe
|
### V32-POOL-003 — Open failure safe
|
||||||
|
|
||||||
Critère : DNS/connect/auth/server errors ne copient ni URI ni texte remote arbitraire dans Display/Debug public.
|
Critère : DNS/connect/auth/server errors ne copient ni URI ni texte remote arbitraire dans Display/Debug public.
|
||||||
|
|
||||||
Statut : `TODO pre.005`.
|
Matérialisé par `pre.005` : parsing/connexion/pool/TLS sont ramenés à une classification backend locale sans conserver le texte des erreurs externes ni l'URI. La façade mappe vers des codes `store.postgres_*` stables avec contexte statique sûr.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.005`.
|
||||||
|
|
||||||
### V32-POOL-004 — Close
|
### V32-POOL-004 — Close
|
||||||
|
|
||||||
@@ -317,7 +327,9 @@ shutdown respecte timeout
|
|||||||
aucune tâche volontairement laissée orpheline
|
aucune tâche volontairement laissée orpheline
|
||||||
```
|
```
|
||||||
|
|
||||||
Statut : `TODO 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 : `TODO gate opérateur pre.005 / TODO pre.007/pre.008`.
|
||||||
|
|
||||||
## 7. TLS
|
## 7. TLS
|
||||||
|
|
||||||
@@ -330,7 +342,9 @@ Disabled
|
|||||||
VerifyFull
|
VerifyFull
|
||||||
```
|
```
|
||||||
|
|
||||||
Statut : `TODO pre.005`.
|
Matérialisé par `pre.005` : la surface physique accepte exactement `Disabled` et `VerifyFull`; l'URI parsée est ensuite normalisée vers `SslMode::Disable` ou `SslMode::Require`, donc elle ne peut pas modifier la policy typée.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.005`.
|
||||||
|
|
||||||
### V32-TLS-002 — VerifyFull
|
### V32-TLS-002 — VerifyFull
|
||||||
|
|
||||||
@@ -344,7 +358,9 @@ nom serveur vérifié
|
|||||||
aucun fallback plaintext
|
aucun fallback plaintext
|
||||||
```
|
```
|
||||||
|
|
||||||
Statut : `TODO pre.005`.
|
Matérialisé par `pre.005` : `VerifyFull` construit un `rustls::RootCertStore` à partir des roots système, utilise explicitement le provider AWS-LC, requiert un host TCP pour l'identité serveur, rejette `hostaddr` seul et les sockets Unix, et ne permet aucun fallback plaintext. Les erreurs de roots/certificats sont réduites à des compteurs sûrs.
|
||||||
|
|
||||||
|
Statut : `TODO gate opérateur pre.005`.
|
||||||
|
|
||||||
### V32-TLS-003 — Pas de fichier TLS implicite
|
### V32-TLS-003 — Pas de fichier TLS implicite
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user