v0.3.2-pre.003
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
# file: Cargo.toml
|
||||
# version: 333
|
||||
# version: 334
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.2-pre.2.fix.1"
|
||||
version = "0.3.2-pre.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||
|
||||
13
crates/ksp-store-lib/src/error.rs
Normal file
13
crates/ksp-store-lib/src/error.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
// file: crates/ksp-store-lib/src/error.rs
|
||||
// version: 1
|
||||
|
||||
/// 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");
|
||||
/// Error code used when a known Store backend was selected but its Cargo feature is 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.
|
||||
pub const ERROR_CODE_BACKEND_OPEN_FAILED: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "backend_open_failed");
|
||||
/// 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");
|
||||
/// Error code used when a Store cannot complete its explicit shutdown inside the configured bound.
|
||||
pub const ERROR_CODE_SHUTDOWN_TIMEOUT: ksp_store_api::ErrorCode = ksp_store_api::ErrorCode::new("store", "shutdown_timeout");
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -7,19 +7,170 @@
|
||||
|
||||
//! Common backend-neutral Store runtime facade for KSP.
|
||||
//!
|
||||
//! `0.3.2-pre.002` establishes only the crate and Cargo feature graph. Runtime
|
||||
//! settings, backend selection, lifecycle, error mapping and API reexports are
|
||||
//! introduced by later prereleases of `0.3.2`.
|
||||
//! `0.3.2-pre.003` materializes Config-independent settings, stable backend
|
||||
//! identity, bounded validation and the opaque async Store lifecycle contract.
|
||||
//! Physical PostgreSQL connection, pool, TLS and migrations remain private
|
||||
//! future slices of this release.
|
||||
//!
|
||||
//! The default `postgres` feature compiles the official PostgreSQL backend as
|
||||
//! an optional implementation dependency. No backend implementation type is
|
||||
//! part of this crate's public surface.
|
||||
|
||||
mod constants;
|
||||
mod error;
|
||||
mod settings;
|
||||
mod store;
|
||||
|
||||
/// Crate-owned tracing target reserved for later Store runtime behavior.
|
||||
/// Error code reserved for operations attempted after a Store backend is closed.
|
||||
pub use self::error::ERROR_CODE_BACKEND_CLOSED;
|
||||
/// Error code used when a known Store backend is selected without its compiled feature.
|
||||
pub use self::error::ERROR_CODE_BACKEND_NOT_COMPILED;
|
||||
/// Error code used when a compiled Store backend cannot complete opening.
|
||||
pub use self::error::ERROR_CODE_BACKEND_OPEN_FAILED;
|
||||
/// Error code used when Store settings violate backend-neutral bounds or invariants.
|
||||
pub use self::error::ERROR_CODE_SETTINGS_INVALID;
|
||||
/// Error code used when explicit Store shutdown exceeds its configured deadline.
|
||||
pub use self::error::ERROR_CODE_SHUTDOWN_TIMEOUT;
|
||||
/// Bounded PostgreSQL bootstrap and migration settings owned by the Store facade.
|
||||
pub use self::settings::PostgresBootstrapSettings;
|
||||
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
|
||||
pub use self::settings::PostgresPoolSettings;
|
||||
/// PostgreSQL settings owned by the Store facade without exposing backend implementation types.
|
||||
pub use self::settings::PostgresStoreSettings;
|
||||
/// TLS policy accepted by the backend-neutral PostgreSQL settings surface.
|
||||
pub use self::settings::PostgresTlsMode;
|
||||
/// Backend identity understood independently from compiled Cargo features.
|
||||
pub use self::settings::StoreBackendKind;
|
||||
/// Backend-specific settings selected through the common Store facade.
|
||||
pub use self::settings::StoreBackendSettings;
|
||||
/// Complete backend-neutral settings consumed by the common Store runtime facade.
|
||||
pub use self::settings::StoreSettings;
|
||||
/// Opaque common Store runtime facade with consuming async shutdown.
|
||||
pub use self::store::Store;
|
||||
/// Error code used when a RAW write collides with divergent content for the same logical identity.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_CONFLICT;
|
||||
/// Error code used when a RAW Store model violates one of its backend-agnostic invariants.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_MODEL_INVALID;
|
||||
/// Error code used when a KSP-owned RAW persistence payload violates its format or admission contract.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_PAYLOAD_INVALID;
|
||||
/// Error code used when acquisition provenance is malformed, unsafe or internally inconsistent.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_PROVENANCE_INVALID;
|
||||
/// Error code used when one RAW query or cursor violates backend-agnostic query invariants.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_QUERY_INVALID;
|
||||
/// Error code used when a RAW retention transition violates the logical lifecycle contract.
|
||||
pub use ksp_store_api::ERROR_CODE_RAW_RETENTION_INVALID;
|
||||
/// Common KSP error type used by Store-facing contracts.
|
||||
pub use ksp_store_api::Error;
|
||||
/// Stable structured code identifying a KSP error category and condition.
|
||||
pub use ksp_store_api::ErrorCode;
|
||||
/// Structured contextual field attached to a KSP error.
|
||||
pub use ksp_store_api::ErrorContext;
|
||||
/// Maximum complete RAW account-data length admitted by the Store API.
|
||||
pub use ksp_store_api::MAX_RAW_ACCOUNT_DATA_BYTES;
|
||||
/// Maximum UTF-8 byte length accepted for one safe logical RAW/provenance code.
|
||||
pub use ksp_store_api::MAX_RAW_CODE_BYTES;
|
||||
/// Maximum opaque query cursor length admitted by the Store API.
|
||||
pub use ksp_store_api::MAX_RAW_PAGE_CURSOR_BYTES;
|
||||
/// Maximum KSP-owned canonical RAW payload admitted by the Store API.
|
||||
pub use ksp_store_api::MAX_RAW_PAYLOAD_BYTES;
|
||||
/// Maximum source-wire payload size recorded as acquisition metadata.
|
||||
pub use ksp_store_api::MAX_RAW_SOURCE_PAYLOAD_BYTES;
|
||||
/// Maximum supported Unix millisecond timestamp.
|
||||
pub use ksp_store_api::MAX_RAW_UNIX_MILLIS;
|
||||
/// Canonical Solana account address primitive shared by persistent models.
|
||||
pub use ksp_store_api::Pubkey;
|
||||
/// Persistable acquisition observation linked to one complete canonical RAW account state.
|
||||
pub use ksp_store_api::RawAccountObservation;
|
||||
/// Read capability for persisted RAW account-state observations.
|
||||
pub use ksp_store_api::RawAccountObservationRead;
|
||||
/// Write capability for additional observations of already persisted RAW account states.
|
||||
pub use ksp_store_api::RawAccountObservationWrite;
|
||||
/// Canonical complete N1 RAW account state independent from acquisition transport.
|
||||
pub use ksp_store_api::RawAccountState;
|
||||
/// Backend-independent list query for complete canonical RAW account states.
|
||||
pub use ksp_store_api::RawAccountStateQuery;
|
||||
/// Read capability for complete canonical RAW account states.
|
||||
pub use ksp_store_api::RawAccountStateRead;
|
||||
/// Durable backend-independent identity of one canonical RAW account state.
|
||||
pub use ksp_store_api::RawAccountStateReference;
|
||||
/// Write capability for complete canonical RAW account-state acquisitions.
|
||||
pub use ksp_store_api::RawAccountStateWrite;
|
||||
/// Origin category describing why one acquisition was performed.
|
||||
pub use ksp_store_api::RawAcquisitionOrigin;
|
||||
/// Safe source-independent acquisition provenance attached to one persisted observation.
|
||||
pub use ksp_store_api::RawAcquisitionProvenance;
|
||||
/// Combined outcome of one atomic canonical RAW entity plus observation acquisition.
|
||||
pub use ksp_store_api::RawAcquisitionWriteOutcome;
|
||||
/// Fixed-size digest identifying canonical or source bytes without retaining them.
|
||||
pub use ksp_store_api::RawContentHash;
|
||||
/// Outcome for one canonical RAW entity in an idempotent persistence operation.
|
||||
pub use ksp_store_api::RawEntityWriteOutcome;
|
||||
/// Bounded identifier of one KSP-owned source-independent RAW persistence format.
|
||||
pub use ksp_store_api::RawFormatId;
|
||||
/// Bounded logical network/cluster identifier used in backend-independent Store identities.
|
||||
pub use ksp_store_api::RawNetworkId;
|
||||
/// Stable deterministic idempotence key for one persisted acquisition observation.
|
||||
pub use ksp_store_api::RawObservationKey;
|
||||
/// Outcome for one deterministic acquisition observation write.
|
||||
pub use ksp_store_api::RawObservationWriteOutcome;
|
||||
/// One deterministic page of backend-independent Store results.
|
||||
pub use ksp_store_api::RawPage;
|
||||
/// Opaque backend-owned cursor returned by one deterministic Store query.
|
||||
pub use ksp_store_api::RawPageCursor;
|
||||
/// Caller-requested page size without an arbitrary KSP policy ceiling.
|
||||
pub use ksp_store_api::RawPageLimit;
|
||||
/// Opaque-cursor page request used by backend-independent list operations.
|
||||
pub use ksp_store_api::RawPageRequest;
|
||||
/// Bounded source-independent KSP RAW persistence payload.
|
||||
pub use ksp_store_api::RawPayload;
|
||||
/// Bounded logical code used by acquisition provenance fields.
|
||||
pub use ksp_store_api::RawProvenanceCode;
|
||||
/// Logical availability state of one canonical RAW payload.
|
||||
pub use ksp_store_api::RawRetentionState;
|
||||
/// Outcome of one atomic RAW retention transition.
|
||||
pub use ksp_store_api::RawRetentionWriteOutcome;
|
||||
/// Optional inclusive Solana slot bounds for one Store query.
|
||||
pub use ksp_store_api::RawSlotRange;
|
||||
/// Deterministic traversal direction for Store list queries.
|
||||
pub use ksp_store_api::RawSortDirection;
|
||||
/// Bounded UTC timestamp represented as whole milliseconds since Unix epoch.
|
||||
pub use ksp_store_api::RawTimestamp;
|
||||
/// Canonical source-independent N1 RAW transaction persisted by Store backends.
|
||||
pub use ksp_store_api::RawTransaction;
|
||||
/// Explicit write mode for canonical RAW transaction acquisitions.
|
||||
pub use ksp_store_api::RawTransactionAcquisitionMode;
|
||||
/// Persistable acquisition observation linked to one canonical RAW transaction.
|
||||
pub use ksp_store_api::RawTransactionObservation;
|
||||
/// Read capability for persisted RAW transaction observations.
|
||||
pub use ksp_store_api::RawTransactionObservationRead;
|
||||
/// Write capability for additional observations of already persisted RAW transactions.
|
||||
pub use ksp_store_api::RawTransactionObservationWrite;
|
||||
/// Backend-independent list query for canonical RAW transactions.
|
||||
pub use ksp_store_api::RawTransactionQuery;
|
||||
/// Read capability for canonical RAW transactions.
|
||||
pub use ksp_store_api::RawTransactionRead;
|
||||
/// Durable backend-independent identity of one canonical RAW transaction.
|
||||
pub use ksp_store_api::RawTransactionReference;
|
||||
/// Read capability for canonical RAW transaction retention metadata.
|
||||
pub use ksp_store_api::RawTransactionRetentionRead;
|
||||
/// Requested compare-and-transition operation for one RAW transaction retention state.
|
||||
pub use ksp_store_api::RawTransactionRetentionTransition;
|
||||
/// Write capability for policy-authorized RAW transaction retention transitions.
|
||||
pub use ksp_store_api::RawTransactionRetentionWrite;
|
||||
/// Canonical 64-byte Solana transaction signature used by Store identities.
|
||||
pub use ksp_store_api::RawTransactionSignature;
|
||||
/// Minimal durable identity retained after a canonical RAW transaction payload is purged.
|
||||
pub use ksp_store_api::RawTransactionTombstone;
|
||||
/// Write capability for canonical RAW transaction acquisitions.
|
||||
pub use ksp_store_api::RawTransactionWrite;
|
||||
/// Common KSP result alias using [`Error`].
|
||||
pub use ksp_store_api::Result;
|
||||
/// Boxed async operation returned by object-safe Store capability contracts.
|
||||
pub use ksp_store_api::StoreApiFuture;
|
||||
|
||||
/// Crate-owned tracing target reserved for Store runtime behavior.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
|
||||
// Keep the mandatory crate-owned tracing target part of the compiled scaffold
|
||||
// without inventing runtime logging before the first behavioral tranche.
|
||||
const _: &str = TRACING_TARGET;
|
||||
// without inventing runtime logging before the first behavioral log site.
|
||||
const _: &str = crate::TRACING_TARGET;
|
||||
|
||||
381
crates/ksp-store-lib/src/settings.rs
Normal file
381
crates/ksp-store-lib/src/settings.rs
Normal file
@@ -0,0 +1,381 @@
|
||||
// file: crates/ksp-store-lib/src/settings.rs
|
||||
// version: 1
|
||||
|
||||
const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
|
||||
const DEFAULT_MAX_CONNECTIONS: u32 = 8;
|
||||
const DEFAULT_MIGRATION_LOCK_TIMEOUT_MS: u64 = 10_000;
|
||||
const DEFAULT_MIGRATION_TIMEOUT_MS: u64 = 30_000;
|
||||
const DEFAULT_POOL_CREATE_TIMEOUT_MS: u64 = 10_000;
|
||||
const DEFAULT_POOL_RECYCLE_TIMEOUT_MS: u64 = 5_000;
|
||||
const DEFAULT_POOL_WAIT_TIMEOUT_MS: u64 = 5_000;
|
||||
const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 5_000;
|
||||
const MAX_CONNECTIONS: u32 = 64;
|
||||
const MAX_CONNECT_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_MIGRATION_LOCK_TIMEOUT_MS: u64 = 120_000;
|
||||
const MAX_MIGRATION_TIMEOUT_MS: u64 = 300_000;
|
||||
const MAX_POOL_CREATE_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_POOL_RECYCLE_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_POOL_WAIT_TIMEOUT_MS: u64 = 60_000;
|
||||
const MAX_SHUTDOWN_TIMEOUT_MS: u64 = 30_000;
|
||||
const MIN_CONNECTIONS: u32 = 1;
|
||||
const MIN_CONNECT_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_MIGRATION_LOCK_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_MIGRATION_TIMEOUT_MS: u64 = 1_000;
|
||||
const MIN_POOL_CREATE_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_POOL_RECYCLE_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_POOL_WAIT_TIMEOUT_MS: u64 = 100;
|
||||
const MIN_SHUTDOWN_TIMEOUT_MS: u64 = 100;
|
||||
|
||||
/// Backend identity understood by the common Store runtime independently from compiled Cargo features.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum StoreBackendKind {
|
||||
/// Official PostgreSQL Store backend.
|
||||
Postgres,
|
||||
}
|
||||
|
||||
impl StoreBackendKind {
|
||||
/// Returns the stable safe backend code used in diagnostics and configuration mapping.
|
||||
#[must_use]
|
||||
pub const fn code(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::Postgres => "postgres",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS policy accepted by the backend-neutral PostgreSQL settings surface.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum PostgresTlsMode {
|
||||
/// Connect without TLS.
|
||||
Disabled,
|
||||
/// Require TLS and verify both the certificate chain and requested server identity.
|
||||
VerifyFull,
|
||||
}
|
||||
|
||||
/// Bounded PostgreSQL connection-pool settings owned by the Store facade.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresPoolSettings {
|
||||
connect_timeout: std::time::Duration,
|
||||
create_timeout: std::time::Duration,
|
||||
max_connections: u32,
|
||||
recycle_timeout: std::time::Duration,
|
||||
wait_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl PostgresPoolSettings {
|
||||
/// Creates explicit PostgreSQL pool bounds without performing any I/O.
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
max_connections: u32,
|
||||
connect_timeout: std::time::Duration,
|
||||
wait_timeout: std::time::Duration,
|
||||
create_timeout: std::time::Duration,
|
||||
recycle_timeout: std::time::Duration,
|
||||
) -> Self {
|
||||
return Self { connect_timeout, create_timeout, max_connections, recycle_timeout, wait_timeout };
|
||||
}
|
||||
|
||||
/// Returns the timeout for establishing one physical PostgreSQL connection.
|
||||
#[must_use]
|
||||
pub const fn connect_timeout(&self) -> std::time::Duration {
|
||||
return self.connect_timeout;
|
||||
}
|
||||
|
||||
/// Returns the timeout for creating one pooled PostgreSQL object.
|
||||
#[must_use]
|
||||
pub const fn create_timeout(&self) -> std::time::Duration {
|
||||
return self.create_timeout;
|
||||
}
|
||||
|
||||
/// Returns the maximum number of physical PostgreSQL connections owned by the pool.
|
||||
#[must_use]
|
||||
pub const fn max_connections(&self) -> u32 {
|
||||
return self.max_connections;
|
||||
}
|
||||
|
||||
/// Returns the timeout for recycling one pooled PostgreSQL object.
|
||||
#[must_use]
|
||||
pub const fn recycle_timeout(&self) -> std::time::Duration {
|
||||
return self.recycle_timeout;
|
||||
}
|
||||
|
||||
/// Returns the maximum time one acquisition can wait for pool capacity.
|
||||
#[must_use]
|
||||
pub const fn wait_timeout(&self) -> std::time::Duration {
|
||||
return self.wait_timeout;
|
||||
}
|
||||
|
||||
/// Validates all pool bounds without opening a connection.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
if self.max_connections < MIN_CONNECTIONS || self.max_connections > MAX_CONNECTIONS {
|
||||
return std::result::Result::Err(
|
||||
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL pool connection bound is invalid")
|
||||
.with_context("field", "postgres.pool.max_connections")
|
||||
.with_context("minimum", MIN_CONNECTIONS.to_string())
|
||||
.with_context("maximum", MAX_CONNECTIONS.to_string()),
|
||||
);
|
||||
}
|
||||
let connect_validation = validate_duration("postgres.pool.connect_timeout", self.connect_timeout, MIN_CONNECT_TIMEOUT_MS, MAX_CONNECT_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = connect_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let wait_validation = validate_duration("postgres.pool.wait_timeout", self.wait_timeout, MIN_POOL_WAIT_TIMEOUT_MS, MAX_POOL_WAIT_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = wait_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let create_validation = validate_duration("postgres.pool.create_timeout", self.create_timeout, MIN_POOL_CREATE_TIMEOUT_MS, MAX_POOL_CREATE_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = create_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let recycle_validation =
|
||||
validate_duration("postgres.pool.recycle_timeout", self.recycle_timeout, MIN_POOL_RECYCLE_TIMEOUT_MS, MAX_POOL_RECYCLE_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = recycle_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for PostgresPoolSettings {
|
||||
fn default() -> Self {
|
||||
return Self::new(
|
||||
DEFAULT_MAX_CONNECTIONS,
|
||||
std::time::Duration::from_millis(DEFAULT_CONNECT_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_POOL_WAIT_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_POOL_CREATE_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_POOL_RECYCLE_TIMEOUT_MS),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded PostgreSQL bootstrap settings owned by the Store facade.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct PostgresBootstrapSettings {
|
||||
auto_migrate: bool,
|
||||
migration_lock_timeout: std::time::Duration,
|
||||
migration_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl PostgresBootstrapSettings {
|
||||
/// Creates explicit bootstrap behavior and migration deadlines.
|
||||
#[must_use]
|
||||
pub const fn new(auto_migrate: bool, migration_timeout: std::time::Duration, migration_lock_timeout: std::time::Duration) -> Self {
|
||||
return Self { auto_migrate, migration_lock_timeout, migration_timeout };
|
||||
}
|
||||
|
||||
/// Returns whether pending KSP-owned migrations may be applied during Store opening.
|
||||
#[must_use]
|
||||
pub const fn auto_migrate(&self) -> bool {
|
||||
return self.auto_migrate;
|
||||
}
|
||||
|
||||
/// Returns the bounded wait allowed for the private PostgreSQL migration lock.
|
||||
#[must_use]
|
||||
pub const fn migration_lock_timeout(&self) -> std::time::Duration {
|
||||
return self.migration_lock_timeout;
|
||||
}
|
||||
|
||||
/// Returns the bounded duration allowed for one migration/bootstrap run.
|
||||
#[must_use]
|
||||
pub const fn migration_timeout(&self) -> std::time::Duration {
|
||||
return self.migration_timeout;
|
||||
}
|
||||
|
||||
/// Validates bootstrap and migration deadlines without contacting PostgreSQL.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
let migration_validation =
|
||||
validate_duration("postgres.bootstrap.migration_timeout", self.migration_timeout, MIN_MIGRATION_TIMEOUT_MS, MAX_MIGRATION_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = migration_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let lock_validation = validate_duration(
|
||||
"postgres.bootstrap.migration_lock_timeout",
|
||||
self.migration_lock_timeout,
|
||||
MIN_MIGRATION_LOCK_TIMEOUT_MS,
|
||||
MAX_MIGRATION_LOCK_TIMEOUT_MS,
|
||||
);
|
||||
if let std::result::Result::Err(error) = lock_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for PostgresBootstrapSettings {
|
||||
fn default() -> Self {
|
||||
return Self::new(
|
||||
true,
|
||||
std::time::Duration::from_millis(DEFAULT_MIGRATION_TIMEOUT_MS),
|
||||
std::time::Duration::from_millis(DEFAULT_MIGRATION_LOCK_TIMEOUT_MS),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// PostgreSQL settings owned by the Store facade and independent from Config or backend implementation types.
|
||||
pub struct PostgresStoreSettings {
|
||||
bootstrap: PostgresBootstrapSettings,
|
||||
connection_uri: std::string::String,
|
||||
pool: PostgresPoolSettings,
|
||||
tls_mode: PostgresTlsMode,
|
||||
}
|
||||
|
||||
impl PostgresStoreSettings {
|
||||
/// Creates PostgreSQL Store settings from an explicitly supplied sensitive connection URI and typed runtime bounds.
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
connection_uri: impl std::convert::Into<std::string::String>,
|
||||
pool: PostgresPoolSettings,
|
||||
tls_mode: PostgresTlsMode,
|
||||
bootstrap: PostgresBootstrapSettings,
|
||||
) -> Self {
|
||||
return Self { bootstrap, connection_uri: connection_uri.into(), pool, tls_mode };
|
||||
}
|
||||
|
||||
/// Returns the PostgreSQL bootstrap settings without exposing the sensitive connection URI.
|
||||
#[must_use]
|
||||
pub const fn bootstrap(&self) -> PostgresBootstrapSettings {
|
||||
return self.bootstrap;
|
||||
}
|
||||
|
||||
/// Returns the PostgreSQL pool settings without exposing the sensitive connection URI.
|
||||
#[must_use]
|
||||
pub const fn pool(&self) -> PostgresPoolSettings {
|
||||
return self.pool;
|
||||
}
|
||||
|
||||
/// Returns the selected PostgreSQL TLS policy without exposing the sensitive connection URI.
|
||||
#[must_use]
|
||||
pub const fn tls_mode(&self) -> PostgresTlsMode {
|
||||
return self.tls_mode;
|
||||
}
|
||||
|
||||
/// Validates backend-neutral PostgreSQL settings without parsing the URI or performing I/O.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
if self.connection_uri.is_empty() {
|
||||
return std::result::Result::Err(
|
||||
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "PostgreSQL connection URI is required")
|
||||
.with_context("field", "postgres.connection_uri"),
|
||||
);
|
||||
}
|
||||
let pool_validation = self.pool.validate();
|
||||
if let std::result::Result::Err(error) = pool_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let bootstrap_validation = self.bootstrap.validate();
|
||||
if let std::result::Result::Err(error) = bootstrap_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PostgresStoreSettings {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
return formatter
|
||||
.debug_struct("PostgresStoreSettings")
|
||||
.field("connection_uri", &"<redacted>")
|
||||
.field("pool", &self.pool)
|
||||
.field("tls_mode", &self.tls_mode)
|
||||
.field("bootstrap", &self.bootstrap)
|
||||
.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend-specific settings selected through the common Store facade.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum StoreBackendSettings {
|
||||
/// Settings for the known PostgreSQL backend, whether or not its Cargo feature is compiled.
|
||||
Postgres(PostgresStoreSettings),
|
||||
}
|
||||
|
||||
impl StoreBackendSettings {
|
||||
/// Returns the stable backend identity represented by these settings.
|
||||
#[must_use]
|
||||
pub const fn kind(&self) -> StoreBackendKind {
|
||||
return match self {
|
||||
Self::Postgres(_) => StoreBackendKind::Postgres,
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates backend-specific settings without performing I/O.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
return match self {
|
||||
Self::Postgres(settings) => settings.validate(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete backend-neutral settings consumed by the common Store runtime facade.
|
||||
#[derive(Debug)]
|
||||
pub struct StoreSettings {
|
||||
backend: StoreBackendSettings,
|
||||
shutdown_timeout: std::time::Duration,
|
||||
}
|
||||
|
||||
impl StoreSettings {
|
||||
/// Creates complete Store runtime settings from one explicit backend and shutdown bound.
|
||||
#[must_use]
|
||||
pub const fn new(backend: StoreBackendSettings, shutdown_timeout: std::time::Duration) -> Self {
|
||||
return Self { backend, shutdown_timeout };
|
||||
}
|
||||
|
||||
/// Returns the selected backend settings.
|
||||
#[must_use]
|
||||
pub const fn backend(&self) -> &StoreBackendSettings {
|
||||
return &self.backend;
|
||||
}
|
||||
|
||||
/// Returns the selected stable backend identity.
|
||||
#[must_use]
|
||||
pub const fn backend_kind(&self) -> StoreBackendKind {
|
||||
return self.backend.kind();
|
||||
}
|
||||
|
||||
/// Returns the maximum duration allowed for explicit Store shutdown.
|
||||
#[must_use]
|
||||
pub const fn shutdown_timeout(&self) -> std::time::Duration {
|
||||
return self.shutdown_timeout;
|
||||
}
|
||||
|
||||
/// Validates all backend-neutral Store settings before any backend I/O can start.
|
||||
pub fn validate(&self) -> ksp_store_api::Result<()> {
|
||||
let shutdown_validation = validate_duration("shutdown_timeout", self.shutdown_timeout, MIN_SHUTDOWN_TIMEOUT_MS, MAX_SHUTDOWN_TIMEOUT_MS);
|
||||
if let std::result::Result::Err(error) = shutdown_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let backend_validation = self.backend.validate();
|
||||
if let std::result::Result::Err(error) = backend_validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreSettings {
|
||||
/// Creates settings using the common default shutdown bound while keeping backend construction explicit.
|
||||
#[must_use]
|
||||
pub fn with_default_shutdown(backend: StoreBackendSettings) -> Self {
|
||||
return Self::new(backend, std::time::Duration::from_millis(DEFAULT_SHUTDOWN_TIMEOUT_MS));
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_duration(field: &'static str, value: std::time::Duration, minimum_ms: u64, maximum_ms: u64) -> ksp_store_api::Result<()> {
|
||||
let minimum = std::time::Duration::from_millis(minimum_ms);
|
||||
let maximum = std::time::Duration::from_millis(maximum_ms);
|
||||
if value < minimum || value > maximum {
|
||||
return std::result::Result::Err(
|
||||
ksp_store_api::Error::new(crate::ERROR_CODE_SETTINGS_INVALID, "Store runtime duration is outside the supported resource bound")
|
||||
.with_context("field", field)
|
||||
.with_context("minimum_ms", minimum_ms.to_string())
|
||||
.with_context("maximum_ms", maximum_ms.to_string()),
|
||||
);
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/settings.rs"]
|
||||
mod tests;
|
||||
56
crates/ksp-store-lib/src/store.rs
Normal file
56
crates/ksp-store-lib/src/store.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
// file: crates/ksp-store-lib/src/store.rs
|
||||
// version: 1
|
||||
|
||||
/// 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
|
||||
/// contract but intentionally has no successful opening path until the PostgreSQL runtime foundation is materialized by later prereleases.
|
||||
#[derive(Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Store;
|
||||
|
||||
impl Store {
|
||||
/// Validates settings, selects the requested backend and opens a ready Store runtime.
|
||||
///
|
||||
/// 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
|
||||
/// with [`crate::ERROR_CODE_BACKEND_OPEN_FAILED`] because physical connection ownership is introduced in `pre.005`.
|
||||
pub async fn open(settings: crate::StoreSettings) -> ksp_store_api::Result<Self> {
|
||||
let validation = settings.validate();
|
||||
if let std::result::Result::Err(error) = validation {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let backend_kind = settings.backend_kind();
|
||||
return match backend_kind {
|
||||
crate::StoreBackendKind::Postgres => open_postgres_contract(backend_kind).await,
|
||||
};
|
||||
}
|
||||
|
||||
/// Explicitly closes the Store runtime and consumes its facade handle.
|
||||
///
|
||||
/// `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<()> {
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> {
|
||||
return std::result::Result::Err(
|
||||
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(not(feature = "postgres"))]
|
||||
async fn open_postgres_contract(backend_kind: crate::StoreBackendKind) -> ksp_store_api::Result<Store> {
|
||||
return std::result::Result::Err(
|
||||
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)]
|
||||
#[path = "../unit_tests/store.rs"]
|
||||
mod tests;
|
||||
@@ -1,11 +1,11 @@
|
||||
// file: crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Cargo-feature and dependency-boundary canaries for the common Store runtime scaffold.
|
||||
//! Cargo-feature and dependency-boundary canaries for the common Store runtime facade.
|
||||
|
||||
#[test]
|
||||
fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() {
|
||||
@@ -24,30 +24,27 @@ fn pre_002_manifest_owns_default_postgres_feature_and_optional_backend_edge() {
|
||||
"deadpool-postgres",
|
||||
"rustls",
|
||||
] {
|
||||
assert!(!manifest.contains(forbidden), "forbidden pre.002 Store facade dependency detected: {forbidden}");
|
||||
assert!(!manifest.contains(forbidden), "forbidden Store facade dependency detected: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_002_scaffold_keeps_backend_types_and_future_runtime_surface_private() {
|
||||
fn pre_003_facade_adds_only_backend_neutral_settings_lifecycle_and_api_reexports() {
|
||||
let crate_root = include_str!("../src/lib.rs");
|
||||
assert!(crate_root.contains("mod constants;"));
|
||||
assert!(crate_root.contains("pub(crate) use self::constants::TRACING_TARGET;"));
|
||||
assert!(crate_root.contains("const _: &str = TRACING_TARGET;"));
|
||||
for forbidden in [
|
||||
"pub mod ",
|
||||
"pub use ksp_store_postgres_lib",
|
||||
"StoreSettings",
|
||||
"StoreBackendSettings",
|
||||
"PostgresStoreSettings",
|
||||
"pub struct Store",
|
||||
"tokio_postgres",
|
||||
"deadpool_postgres",
|
||||
] {
|
||||
assert!(!crate_root.contains(forbidden), "forbidden pre.002 Store facade surface detected: {forbidden}");
|
||||
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::store::Store;"));
|
||||
assert!(crate_root.contains("pub use ksp_store_api::RawTransaction;"));
|
||||
assert!(crate_root.contains("const _: &str = crate::TRACING_TARGET;"));
|
||||
for forbidden in ["pub mod ", "pub use ksp_store_postgres_lib", "tokio_postgres", "deadpool_postgres", "rustls::", "Pool", "Client", "Row", "Statement"] {
|
||||
assert!(!crate_root.contains(forbidden), "forbidden physical backend facade surface detected: {forbidden}");
|
||||
}
|
||||
let production = format!("{}\n{}", include_str!("../src/settings.rs"), include_str!("../src/store.rs"));
|
||||
for forbidden in ["ksp_config_lib", "std::env", "dotenv", "PGHOST", "PGPORT", "PGUSER", "PGPASSWORD", ".pgpass", "tokio_postgres", "deadpool_postgres"] {
|
||||
assert!(!production.contains(forbidden), "forbidden Store facade ownership bypass detected: {forbidden}");
|
||||
}
|
||||
let constants = include_str!("../src/constants.rs");
|
||||
assert!(constants.contains("pub(crate) const TRACING_TARGET: &str = \"ksp-store-lib\";"));
|
||||
return;
|
||||
}
|
||||
|
||||
46
crates/ksp-store-lib/tests/feature_mismatch.rs
Normal file
46
crates/ksp-store-lib/tests/feature_mismatch.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
// file: crates/ksp-store-lib/tests/feature_mismatch.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Feature-selection canaries for known Store backends.
|
||||
|
||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||
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"),
|
||||
};
|
||||
}
|
||||
|
||||
fn settings() -> ksp_store_lib::StoreSettings {
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
||||
"postgresql://operator-supplied-sensitive-value",
|
||||
ksp_store_lib::PostgresPoolSettings::default(),
|
||||
ksp_store_lib::PostgresTlsMode::Disabled,
|
||||
ksp_store_lib::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
return ksp_store_lib::StoreSettings::with_default_shutdown(ksp_store_lib::StoreBackendSettings::Postgres(postgres));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
#[test]
|
||||
fn pre_003_known_postgres_without_feature_returns_stable_error_before_io() {
|
||||
let result = poll_ready(ksp_store_lib::Store::open(settings()));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_NOT_COMPILED));
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
#[test]
|
||||
fn pre_003_compiled_postgres_path_refuses_to_fake_readiness_before_pre_005() {
|
||||
let result = poll_ready(ksp_store_lib::Store::open(settings()));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(ksp_store_lib::ERROR_CODE_BACKEND_OPEN_FAILED));
|
||||
return;
|
||||
}
|
||||
45
crates/ksp-store-lib/tests/public_api.rs
Normal file
45
crates/ksp-store-lib/tests/public_api.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
// file: crates/ksp-store-lib/tests/public_api.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Public API canaries for Store settings, lifecycle and Store API reexports.
|
||||
|
||||
#[test]
|
||||
fn pre_003_settings_and_lifecycle_contract_are_available_from_crate_root() {
|
||||
let postgres = ksp_store_lib::PostgresStoreSettings::new(
|
||||
"postgresql://operator-supplied-sensitive-value",
|
||||
ksp_store_lib::PostgresPoolSettings::default(),
|
||||
ksp_store_lib::PostgresTlsMode::VerifyFull,
|
||||
ksp_store_lib::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
let settings = ksp_store_lib::StoreSettings::with_default_shutdown(ksp_store_lib::StoreBackendSettings::Postgres(postgres));
|
||||
assert_eq!(settings.backend_kind(), ksp_store_lib::StoreBackendKind::Postgres);
|
||||
assert!(settings.validate().is_ok());
|
||||
let _open = ksp_store_lib::Store::open;
|
||||
let _close = ksp_store_lib::Store::close;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_common_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.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_OPEN_FAILED.code(), "backend_open_failed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_BACKEND_CLOSED.code(), "backend_closed");
|
||||
assert_eq!(ksp_store_lib::ERROR_CODE_SHUTDOWN_TIMEOUT.code(), "shutdown_timeout");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_003_facade_reexports_backend_agnostic_store_api_types() {
|
||||
let _raw_transaction = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransaction>>();
|
||||
let _raw_account_state = std::mem::size_of::<std::option::Option<ksp_store_lib::RawAccountState>>();
|
||||
let _query = std::mem::size_of::<std::option::Option<ksp_store_lib::RawTransactionQuery>>();
|
||||
let _capability = std::mem::size_of::<std::option::Option<&dyn ksp_store_lib::RawTransactionRead>>();
|
||||
let _result: ksp_store_lib::Result<()> = std::result::Result::Ok(());
|
||||
return;
|
||||
}
|
||||
139
crates/ksp-store-lib/unit_tests/settings.rs
Normal file
139
crates/ksp-store-lib/unit_tests/settings.rs
Normal file
@@ -0,0 +1,139 @@
|
||||
// file: crates/ksp-store-lib/unit_tests/settings.rs
|
||||
// version: 1
|
||||
|
||||
fn valid_postgres_settings() -> crate::PostgresStoreSettings {
|
||||
return crate::PostgresStoreSettings::new(
|
||||
"postgresql://secret-user:secret-password@db.internal/ksp",
|
||||
crate::PostgresPoolSettings::default(),
|
||||
crate::PostgresTlsMode::VerifyFull,
|
||||
crate::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_match_the_pre_001_runtime_bounds() {
|
||||
let pool = crate::PostgresPoolSettings::default();
|
||||
assert_eq!(pool.max_connections(), 8);
|
||||
assert_eq!(pool.connect_timeout(), std::time::Duration::from_millis(10_000));
|
||||
assert_eq!(pool.wait_timeout(), std::time::Duration::from_millis(5_000));
|
||||
assert_eq!(pool.create_timeout(), std::time::Duration::from_millis(10_000));
|
||||
assert_eq!(pool.recycle_timeout(), std::time::Duration::from_millis(5_000));
|
||||
let bootstrap = crate::PostgresBootstrapSettings::default();
|
||||
assert!(bootstrap.auto_migrate());
|
||||
assert_eq!(bootstrap.migration_timeout(), std::time::Duration::from_millis(30_000));
|
||||
assert_eq!(bootstrap.migration_lock_timeout(), std::time::Duration::from_millis(10_000));
|
||||
let store = crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(valid_postgres_settings()));
|
||||
assert_eq!(store.shutdown_timeout(), std::time::Duration::from_millis(5_000));
|
||||
assert_eq!(store.backend_kind(), crate::StoreBackendKind::Postgres);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_runtime_boundaries_validate_and_adjacent_values_are_rejected() {
|
||||
let minimum_pool = crate::PostgresPoolSettings::new(
|
||||
1,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
);
|
||||
assert!(minimum_pool.validate().is_ok());
|
||||
let maximum_pool = crate::PostgresPoolSettings::new(
|
||||
64,
|
||||
std::time::Duration::from_millis(60_000),
|
||||
std::time::Duration::from_millis(60_000),
|
||||
std::time::Duration::from_millis(60_000),
|
||||
std::time::Duration::from_millis(60_000),
|
||||
);
|
||||
assert!(maximum_pool.validate().is_ok());
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
0,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
65,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
1,
|
||||
std::time::Duration::from_millis(99),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresPoolSettings::new(
|
||||
1,
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(60_001),
|
||||
std::time::Duration::from_millis(100),
|
||||
std::time::Duration::from_millis(100),
|
||||
)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
let minimum_bootstrap = crate::PostgresBootstrapSettings::new(false, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(100));
|
||||
assert!(minimum_bootstrap.validate().is_ok());
|
||||
let maximum_bootstrap = crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(300_000), std::time::Duration::from_millis(120_000));
|
||||
assert!(maximum_bootstrap.validate().is_ok());
|
||||
assert!(
|
||||
crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(999), std::time::Duration::from_millis(100),)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::PostgresBootstrapSettings::new(true, std::time::Duration::from_millis(1_000), std::time::Duration::from_millis(120_001),)
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_shutdown_bound_is_independent_from_backend_and_rejects_outside_values() {
|
||||
let valid = crate::StoreSettings::new(crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(100));
|
||||
assert!(valid.validate().is_ok());
|
||||
let invalid = crate::StoreSettings::new(crate::StoreBackendSettings::Postgres(valid_postgres_settings()), std::time::Duration::from_millis(30_001));
|
||||
let error = invalid.validate().err();
|
||||
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_uri_is_required_but_never_rendered_by_debug_or_validation_error() {
|
||||
let secret = "postgresql://secret-user:secret-password@db.internal/ksp";
|
||||
let settings = valid_postgres_settings();
|
||||
let debug = format!("{settings:?}");
|
||||
assert!(!debug.contains(secret));
|
||||
assert!(!debug.contains("secret-user"));
|
||||
assert!(!debug.contains("secret-password"));
|
||||
assert!(debug.contains("<redacted>"));
|
||||
let empty = crate::PostgresStoreSettings::new(
|
||||
std::string::String::new(),
|
||||
crate::PostgresPoolSettings::default(),
|
||||
crate::PostgresTlsMode::Disabled,
|
||||
crate::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
let error = empty.validate().err();
|
||||
assert_eq!(error.as_ref().map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
||||
assert!(!format!("{:?}", error).contains(secret));
|
||||
return;
|
||||
}
|
||||
55
crates/ksp-store-lib/unit_tests/store.rs
Normal file
55
crates/ksp-store-lib/unit_tests/store.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
// file: crates/ksp-store-lib/unit_tests/store.rs
|
||||
// version: 1
|
||||
|
||||
fn poll_ready<T>(future: impl std::future::Future<Output = T>) -> T {
|
||||
let mut future = std::boxed::Box::pin(future);
|
||||
let waker = std::task::Waker::noop();
|
||||
let mut context = std::task::Context::from_waker(waker);
|
||||
return match std::future::Future::poll(future.as_mut(), &mut context) {
|
||||
std::task::Poll::Ready(value) => value,
|
||||
std::task::Poll::Pending => panic!("pre.003 Store contract future unexpectedly became pending before any I/O exists"),
|
||||
};
|
||||
}
|
||||
|
||||
fn valid_store_settings() -> crate::StoreSettings {
|
||||
let postgres = crate::PostgresStoreSettings::new(
|
||||
"postgresql://secret-user:secret-password@db.internal/ksp",
|
||||
crate::PostgresPoolSettings::default(),
|
||||
crate::PostgresTlsMode::VerifyFull,
|
||||
crate::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
return crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_settings_are_rejected_before_backend_dispatch() {
|
||||
let postgres = crate::PostgresStoreSettings::new(
|
||||
std::string::String::new(),
|
||||
crate::PostgresPoolSettings::default(),
|
||||
crate::PostgresTlsMode::Disabled,
|
||||
crate::PostgresBootstrapSettings::default(),
|
||||
);
|
||||
let settings = crate::StoreSettings::with_default_shutdown(crate::StoreBackendSettings::Postgres(postgres));
|
||||
let result = poll_ready(crate::Store::open(settings));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_SETTINGS_INVALID));
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
#[test]
|
||||
fn compiled_postgres_dispatch_does_not_fake_readiness_before_connection_materialization() {
|
||||
let result = poll_ready(crate::Store::open(valid_store_settings()));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_OPEN_FAILED));
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "postgres"))]
|
||||
#[test]
|
||||
fn known_postgres_without_feature_is_rejected_before_io() {
|
||||
let result = poll_ready(crate::Store::open(valid_store_settings()));
|
||||
let error = result.err();
|
||||
assert_eq!(error.map(|value| value.code()), std::option::Option::Some(crate::ERROR_CODE_BACKEND_NOT_COMPILED));
|
||||
return;
|
||||
}
|
||||
214
deltas/0.3.2/pre.003.md
Normal file
214
deltas/0.3.2/pre.003.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Delta `0.3.2-pre.003` — Store settings, backend selection et lifecycle contract
|
||||
|
||||
## 1. Base requise
|
||||
|
||||
Base exacte :
|
||||
|
||||
```text
|
||||
0.3.2-pre.002-fix.001
|
||||
workspace.package.version = 0.3.2-pre.2.fix.1
|
||||
```
|
||||
|
||||
Le gate opérateur fourni le 29 août 2026 est entièrement vert et sans warning :
|
||||
|
||||
```text
|
||||
cargo fmt --all PASS
|
||||
audit Rust général / exports / workspace PASS
|
||||
audit Markdown PASS — 186 tables / 126 files
|
||||
cargo check --workspace PASS
|
||||
cargo clippy --workspace --all-targets PASS
|
||||
cargo test -p ksp-store-lib PASS
|
||||
cargo test -p ksp-store-postgres-lib PASS
|
||||
cargo check -p ksp-store-lib --no-default-features PASS
|
||||
```
|
||||
|
||||
Le correctif `pre.002-fix.001` est déjà commité. `pre.003` ne réécrit pas ce delta. Le crate-root `ksp-store-lib` étant réellement modifié dans cette tranche, son ancrage `TRACING_TARGET` adopte désormais la forme canonique `const _: &str = crate::TRACING_TARGET;`. `crates/ksp-store-lib/src/constants.rs` n'est pas modifié et conserve donc son header/version existant.
|
||||
|
||||
## 2. Objectif
|
||||
|
||||
Matérialiser uniquement le contrat runtime backend-neutral prévu par le plan 023 :
|
||||
|
||||
```text
|
||||
StoreBackendKind
|
||||
StoreBackendSettings
|
||||
PostgresStoreSettings
|
||||
PostgresPoolSettings
|
||||
PostgresBootstrapSettings
|
||||
PostgresTlsMode
|
||||
StoreSettings
|
||||
Store::open(settings).await
|
||||
Store::close(self).await
|
||||
codes d'erreur Store stables
|
||||
réexports ksp-store-api depuis la façade
|
||||
```
|
||||
|
||||
Aucune connexion, pool physique, TLS connector, migration, Config `std.store` ou SQL n'est introduit.
|
||||
|
||||
## 3. Settings et bornes
|
||||
|
||||
`PostgresStoreSettings` possède une URI explicitement fournie par le caller. Il n'existe aucun `Default` qui invente une URI ou un backend.
|
||||
|
||||
La chaîne URI :
|
||||
|
||||
```text
|
||||
est possédée par le settings
|
||||
est intégralement redacted dans Debug
|
||||
n'a aucun getter public
|
||||
n'entre dans aucun ErrorContext
|
||||
n'est pas parsée avant pre.005
|
||||
```
|
||||
|
||||
Les settings typés matérialisent les bornes décidées en `pre.001` :
|
||||
|
||||
```text
|
||||
max_connections défaut 8 plage 1..64
|
||||
connect_timeout_ms défaut 10_000 plage 100..60_000
|
||||
pool_wait_timeout_ms défaut 5_000 plage 100..60_000
|
||||
pool_create_timeout_ms défaut 10_000 plage 100..60_000
|
||||
pool_recycle_timeout_ms défaut 5_000 plage 100..60_000
|
||||
shutdown_timeout_ms défaut 5_000 plage 100..30_000
|
||||
auto_migrate défaut true
|
||||
migration_timeout_ms défaut 30_000 plage 1_000..300_000
|
||||
migration_lock_ms défaut 10_000 plage 100..120_000
|
||||
```
|
||||
|
||||
Ces bornes restent des garde-fous de ressources/lifecycle et non une policy worker/job.
|
||||
|
||||
## 4. Backend selection et lifecycle
|
||||
|
||||
Backend connu :
|
||||
|
||||
```text
|
||||
StoreBackendKind::Postgres
|
||||
```
|
||||
|
||||
Le dispatch `Store::open` valide d'abord tous les settings puis :
|
||||
|
||||
```text
|
||||
feature postgres absente -> store.backend_not_compiled avant I/O
|
||||
feature postgres présente -> store.backend_open_failed / runtime_foundation_pending avant I/O
|
||||
```
|
||||
|
||||
Le second résultat est volontairement transitoire : `pre.003` ne fabrique jamais un faux `Store` prêt avant la connexion réelle de `pre.005`. La signature finale `Store::close(self).await` est fixée maintenant, mais aucune ressource physique n'existe encore à fermer.
|
||||
|
||||
## 5. Erreurs Store communes
|
||||
|
||||
Codes crate-root :
|
||||
|
||||
```text
|
||||
ERROR_CODE_BACKEND_CLOSED -> store.backend_closed
|
||||
ERROR_CODE_BACKEND_NOT_COMPILED -> store.backend_not_compiled
|
||||
ERROR_CODE_BACKEND_OPEN_FAILED -> store.backend_open_failed
|
||||
ERROR_CODE_SETTINGS_INVALID -> store.settings_invalid
|
||||
ERROR_CODE_SHUTDOWN_TIMEOUT -> store.shutdown_timeout
|
||||
```
|
||||
|
||||
Aucune erreur externe ou chaîne distante n'est attachée à cette tranche.
|
||||
|
||||
## 6. Réexports API
|
||||
|
||||
`ksp-store-lib` réexporte explicitement les 60 éléments crate-root de `ksp-store-api` nécessaires à la consommation de la façade.
|
||||
|
||||
La façade compte donc à cette tranche :
|
||||
|
||||
```text
|
||||
60 reexports ksp-store-api
|
||||
13 éléments runtime Store propres
|
||||
73 exports crate-root au total
|
||||
```
|
||||
|
||||
Aucun type, handle ou symbole de `ksp-store-postgres-lib` n'est réexporté.
|
||||
|
||||
## 7. Fichiers
|
||||
|
||||
Modifiés :
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
crates/ksp-store-lib/src/lib.rs
|
||||
crates/ksp-store-lib/tests/dependency_boundary.rs
|
||||
docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md
|
||||
docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md
|
||||
```
|
||||
|
||||
Ajoutés :
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/src/error.rs
|
||||
crates/ksp-store-lib/src/settings.rs
|
||||
crates/ksp-store-lib/src/store.rs
|
||||
crates/ksp-store-lib/unit_tests/settings.rs
|
||||
crates/ksp-store-lib/unit_tests/store.rs
|
||||
crates/ksp-store-lib/tests/feature_mismatch.rs
|
||||
crates/ksp-store-lib/tests/public_api.rs
|
||||
deltas/0.3.2/pre.003.md
|
||||
```
|
||||
|
||||
Non modifiés intentionnellement :
|
||||
|
||||
```text
|
||||
crates/ksp-store-lib/src/constants.rs
|
||||
crates/ksp-store-postgres-lib/**
|
||||
ksp-store-api/**
|
||||
config/**
|
||||
CHANGELOG.md
|
||||
ROADMAP.md
|
||||
```
|
||||
|
||||
## 8. Version
|
||||
|
||||
La tranche touche le runtime/API Rust ; la version workspace devient :
|
||||
|
||||
```text
|
||||
0.3.2-pre.3
|
||||
```
|
||||
|
||||
Identifiant de livraison :
|
||||
|
||||
```text
|
||||
0.3.2-pre.003
|
||||
```
|
||||
|
||||
## 9. Validations exécutées pendant la préparation
|
||||
|
||||
```text
|
||||
python3 scripts/audit_rust_workspace_rules.py PASS
|
||||
```
|
||||
|
||||
Les validations Cargo ne sont pas disponibles dans l'environnement de génération et ne sont jamais déclarées PASS.
|
||||
|
||||
## 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-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
cargo tree -p ksp-store-lib --edges normal
|
||||
cargo tree -p ksp-store-lib -e features
|
||||
cargo tree -p ksp-store-postgres-lib --edges normal
|
||||
```
|
||||
|
||||
## 11. Hors scope et suite
|
||||
|
||||
Toujours absents :
|
||||
|
||||
```text
|
||||
Config std.store
|
||||
tokio-postgres
|
||||
Deadpool
|
||||
Rustls
|
||||
connexion PostgreSQL
|
||||
migrations/bootstrap physique
|
||||
health/readiness physique
|
||||
RawTransaction PostgreSQL
|
||||
RawAccountState PostgreSQL
|
||||
```
|
||||
|
||||
Si le gate est vert, `0.3.2-pre.004` ouvre exclusivement Config `std.store`, son schema/example/registry/adaptor et les impacts de packaging strictement requis.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/023-V0_3_2_STORE_POSTGRES_FOUNDATION_PLAN.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Plan `0.3.2` — Store/PostgreSQL runtime foundation
|
||||
|
||||
@@ -366,7 +366,26 @@ migration_lock_ms défaut 10_000 plage 100..120_000
|
||||
|
||||
Il n'existe pas de `Default` qui invente une URI de production. Config et les callers programmatiques doivent construire explicitement le backend settings.
|
||||
|
||||
### 8.3 URI et secrets
|
||||
### 8.3 Surface exacte matérialisée en `pre.003`
|
||||
|
||||
Les noms publics sont figés comme suit :
|
||||
|
||||
```text
|
||||
StoreBackendKind::Postgres
|
||||
StoreBackendSettings::Postgres(PostgresStoreSettings)
|
||||
PostgresTlsMode::{Disabled, VerifyFull}
|
||||
PostgresPoolSettings
|
||||
PostgresBootstrapSettings
|
||||
PostgresStoreSettings
|
||||
StoreSettings
|
||||
Store
|
||||
```
|
||||
|
||||
`PostgresPoolSettings::default()` et `PostgresBootstrapSettings::default()` portent exactement les bornes décidées par le gate `pre.001`. `StoreSettings::with_default_shutdown(...)` fournit uniquement la borne commune de shutdown à 5 s. Il n'existe toujours aucun `Default` pour `PostgresStoreSettings` ou `StoreSettings`, afin de ne jamais inventer une URI ou un backend de production.
|
||||
|
||||
Les validations `pre.003` restent backend-neutral et sans parsing PostgreSQL : URI non vide, pool 1..64, timeouts pool/connect 100..60 000 ms, migration 1 000..300 000 ms, lock 100..120 000 ms et shutdown 100..30 000 ms. Le parsing/normalisation de l'URI et les options réellement honorées restent propriétaires de `pre.005`.
|
||||
|
||||
### 8.4 URI et secrets
|
||||
|
||||
La connexion URI est considérée sensible intégralement, même lorsqu'elle ne contient pas de mot de passe visible.
|
||||
|
||||
@@ -421,16 +440,16 @@ La fermeture consomme le Store, ferme le pool, interdit de nouvelles acquisition
|
||||
|
||||
Cette forme limite structurellement le risque de close concurrent avec une opération qui emprunte encore le Store.
|
||||
|
||||
### 9.3 Erreurs stables candidates
|
||||
### 9.3 Erreurs stables matérialisées
|
||||
|
||||
Domaine Store commun :
|
||||
Domaine Store commun, figé en `pre.003` :
|
||||
|
||||
```text
|
||||
STORE_SETTINGS_INVALID
|
||||
STORE_BACKEND_NOT_COMPILED
|
||||
STORE_BACKEND_OPEN_FAILED
|
||||
STORE_BACKEND_CLOSED
|
||||
STORE_SHUTDOWN_TIMEOUT
|
||||
ERROR_CODE_BACKEND_CLOSED -> store.backend_closed
|
||||
ERROR_CODE_BACKEND_NOT_COMPILED -> store.backend_not_compiled
|
||||
ERROR_CODE_BACKEND_OPEN_FAILED -> store.backend_open_failed
|
||||
ERROR_CODE_SETTINGS_INVALID -> store.settings_invalid
|
||||
ERROR_CODE_SHUTDOWN_TIMEOUT -> store.shutdown_timeout
|
||||
```
|
||||
|
||||
Backend PostgreSQL :
|
||||
@@ -899,6 +918,23 @@ Les deux crates possèdent déjà leur `src/constants.rs` et leur `TRACING_TARGE
|
||||
|
||||
Matérialiser `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`, erreurs stable, façade `Store` sans connexion lourde et réexports API. Tester la feature mismatch.
|
||||
|
||||
**Statut : matérialisé par `0.3.2-pre.003`, gate opérateur requis.**
|
||||
|
||||
La tranche fixe :
|
||||
|
||||
```text
|
||||
73 exports crate-root de ksp-store-lib : 60 reexports ksp-store-api + 13 éléments runtime Store
|
||||
settings Config-independent sans serde/env
|
||||
URI PostgreSQL intégralement redacted et sans getter public
|
||||
pool/bootstrap/shutdown bornés selon pre.001
|
||||
Store::open(settings).await
|
||||
Store::close(self).await
|
||||
postgres absent de la build -> store.backend_not_compiled avant I/O
|
||||
postgres compilé avant pre.005 -> store.backend_open_failed/runtime_foundation_pending avant I/O
|
||||
```
|
||||
|
||||
Le dernier comportement est volontairement transitoire : `pre.003` ne retourne jamais un faux `Store` prêt. `pre.005` remplacera uniquement ce stop de staging par la construction physique, puis `pre.007` fermera le shutdown/health end-to-end. Aucun type PostgreSQL physique n'est public.
|
||||
|
||||
### `pre.004` — Config `std.store`
|
||||
|
||||
Document/schema/example/registry/adaptor, `.env.example`, provenance/sensitivity/redaction, packaging resources strictement nécessaires. Aucun env dans Store/backend.
|
||||
@@ -993,8 +1029,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 :
|
||||
|
||||
```text
|
||||
nom exact des structs/methods settings en pre.003
|
||||
codes numériques/strings finaux des ErrorCode en pre.003
|
||||
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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/validation/019-V0_3_2_STORE_POSTGRES_FOUNDATION.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Validation `0.3.2` — Store/PostgreSQL runtime foundation
|
||||
|
||||
@@ -70,6 +70,8 @@ Le gate opérateur après application de `pre.001`, fourni le 29 août 2026, est
|
||||
|
||||
Le gate opérateur de `pre.002`, fourni le 29 août 2026, confirme également les tests ciblés, `--no-default-features` et les `cargo tree`. `cargo check`/Clippy terminent avec succès mais signalent deux warnings par nouvelle crate (`TRACING_TARGET` importé mais inutilisé et constant `dead_code`) ; `pre.002-fix.001` traite cet écart par un canari compile-time privé, tout en conservant les invariants KSP-TRACE-102/103 et sans ajouter de logging runtime.
|
||||
|
||||
Le gate opérateur de `pre.002-fix.001`, fourni le 29 août 2026, est entièrement vert et sans warning : audits Rust/Markdown, workspace check/Clippy, tests des deux crates et compilation `ksp-store-lib --no-default-features` passent. Cette base est l'entrée effective de `pre.003`.
|
||||
|
||||
## 3. Frontières Cargo
|
||||
|
||||
### V32-DEP-001 — Façade -> API
|
||||
@@ -150,13 +152,17 @@ Statut : `TODO gate final`, baseline `v0.3.1` déjà verte.
|
||||
|
||||
Critère : `StoreSettings` est constructible sans `ksp-config-lib`, sans env et sans serde requis par la façade.
|
||||
|
||||
Statut : `TODO pre.003`.
|
||||
Matérialisé par `pre.003` : `StoreSettings`, `StoreBackendSettings`, `PostgresStoreSettings`, pool/bootstrap/TLS typés, sans dépendance Config/serde/env.
|
||||
|
||||
Statut : `TODO gate opérateur pre.003`.
|
||||
|
||||
### V32-API-002 — Backend connu non compilé
|
||||
|
||||
Critère : `Postgres` reste un backend connu sans feature et `Store::open` échoue avant I/O avec un code stable.
|
||||
|
||||
Statut : `TODO pre.003`.
|
||||
Matérialisé par `pre.003` : le test `feature_mismatch` appelle réellement `Store::open` sous `--no-default-features` et exige `store.backend_not_compiled`.
|
||||
|
||||
Statut : `TODO gate opérateur pre.003 / TODO pre.009 final`.
|
||||
|
||||
### V32-API-003 — Aucun type backend physique public
|
||||
|
||||
@@ -175,7 +181,9 @@ Statut : `TODO pre.009`.
|
||||
|
||||
Critère : un consumer de `ksp-store-lib` accède aux contrats Store API utiles sans dépendre directement de la crate backend.
|
||||
|
||||
Statut : `TODO pre.003`.
|
||||
`pre.003` réexporte explicitement les 60 symboles crate-root acquis de `ksp-store-api` depuis `ksp-store-lib`, sans glob et sans réexport backend.
|
||||
|
||||
Statut : `TODO gate opérateur pre.003 / TODO pre.009 exact exports`.
|
||||
|
||||
### V32-API-005 — Lifecycle
|
||||
|
||||
@@ -189,7 +197,9 @@ close borné
|
||||
Drop best-effort seulement
|
||||
```
|
||||
|
||||
Statut : `TODO pre.003/pre.007`.
|
||||
`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`.
|
||||
|
||||
Statut : `TODO gate opérateur pre.003` pour les signatures et le staging ; `TODO pre.007` pour le shutdown physique borné.
|
||||
|
||||
## 5. Config ownership
|
||||
|
||||
@@ -485,11 +495,15 @@ Statut : `TODO pre.009`.
|
||||
|
||||
Cas : zéro, inversion, dépassement bornes pour pool/connect/migration/close.
|
||||
|
||||
Statut : `TODO pre.003/pre.005/pre.009`.
|
||||
Les bornes backend-neutral décidées en `pre.001` sont matérialisées et couvertes par tests unitaires en `pre.003`. Le parsing PostgreSQL et les timeouts physiques restent à `pre.005`.
|
||||
|
||||
Statut : `TODO gate opérateur pre.003 / TODO pre.005 / TODO pre.009`.
|
||||
|
||||
### V32-SEC-004 — Feature mismatch avant I/O
|
||||
|
||||
Statut : `TODO pre.003/pre.009`.
|
||||
`pre.003` matérialise un canari d'intégration compilé avec et sans `postgres`. Sans feature, `Store::open` retourne le code stable `store.backend_not_compiled` avant tout chemin backend physique.
|
||||
|
||||
Statut : `TODO gate opérateur pre.003 / TODO pre.009`.
|
||||
|
||||
### V32-SEC-005 — Server error sanitization
|
||||
|
||||
@@ -514,6 +528,7 @@ Après création des crates :
|
||||
```bash
|
||||
cargo test -p ksp-store-api
|
||||
cargo test -p ksp-store-lib
|
||||
cargo test -p ksp-store-lib --no-default-features
|
||||
cargo test -p ksp-store-postgres-lib
|
||||
cargo test -p ksp-config-lib
|
||||
cargo check -p ksp-store-lib --no-default-features
|
||||
|
||||
Reference in New Issue
Block a user