use self::StoreMode::{DualTier, KvOnly, MemOnly};
use super::engines::{KvEngine, LargeEngine};
#[cfg(feature = "postgres")]
use super::fleet::mint_owner_id;
use super::fleet::FleetState;
use super::Store;
#[cfg(all(test, feature = "postgres", feature = "pg-lo"))]
use crate::backend::Backend;
use crate::dispatch::{route, Tier};
#[cfg(all(
test,
any(feature = "postgres", all(feature = "pg-lo", feature = "mem"))
))]
use crate::facade::LivenessRegistration;
#[cfg(all(
test,
any(feature = "postgres", all(feature = "pg-lo", feature = "mem"))
))]
use crate::facade::LivenessSource;
#[cfg(all(feature = "ops", feature = "postgres"))]
use crate::gc::EngineFleetState;
use crate::gc::GcHome;
#[cfg(all(
test,
any(
feature = "sqlite",
feature = "postgres",
all(feature = "pg-lo", feature = "mem")
)
))]
use crate::keys::Key;
#[cfg(any(feature = "mem", feature = "sqlite", feature = "postgres"))]
use crate::local::LocalBackend;
#[cfg(feature = "mem")]
use crate::mem::MemBackend;
#[cfg(any(feature = "postgres", all(feature = "pg-lo", feature = "mem")))]
use crate::pg_common::PgOpts;
#[cfg(all(feature = "pg-lo", any(feature = "mem", feature = "postgres")))]
use crate::pg_lo::PgLoBackend;
#[cfg(feature = "postgres")]
use crate::postgres::PostgresBackend;
#[cfg(feature = "sqlite")]
use crate::sqlite::SqliteBackend;
use crate::Error;
#[cfg(all(
test,
any(feature = "postgres", all(feature = "pg-lo", feature = "mem"))
))]
use std::collections::BTreeSet;
#[cfg(any(feature = "mem", feature = "postgres", feature = "sqlite"))]
use std::path::PathBuf;
#[cfg(any(
feature = "postgres",
all(
test,
any(feature = "postgres", all(feature = "pg-lo", feature = "mem"))
)
))]
use std::sync::Arc;
use std::time::Duration;
pub const DEFAULT_DELETE_WINDOW_DELAY: Duration = Duration::from_secs(5 * 60);
#[cfg(feature = "postgres")]
pub const DEFAULT_PIN_TTL: Duration = Duration::from_secs(crate::postgres::DEFAULT_PIN_TTL_SECS);
#[cfg(feature = "postgres")]
pub struct FleetSharedMedia {
_seal: (),
}
#[cfg(feature = "postgres")]
impl FleetSharedMedia {
pub fn asserted() -> Self {
Self { _seal: () }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StoreMode {
DualTier,
KvOnly,
MemOnly,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FleetKvShape {
Postgres,
Sqlite,
Mem,
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
enum FleetLargeShape {
None,
LocalSharedMedia,
LocalUndeclared,
PgLo,
}
#[cfg(feature = "postgres")]
fn fleet_pool_size_guard(opts: &PgOpts) -> Result<(), Error> {
if opts.max_size < 2 {
return Err(Error::Io(
"fleet: the kv pool must size at least 2 connections — the sweep lease holds \
its checkout for the sweep's duration (ADR-013 §2) and the sweep body's other \
clauses ride further checkouts under it; a single-connection pool would \
deadlock the fleet sweep"
.to_owned(),
));
}
Ok(())
}
#[allow(dead_code)]
fn fleet_validity_error(kv: FleetKvShape, large: FleetLargeShape) -> Option<Error> {
match (kv, large) {
(FleetKvShape::Postgres, FleetLargeShape::None) => None,
(FleetKvShape::Postgres, FleetLargeShape::LocalSharedMedia) => None,
(FleetKvShape::Postgres, FleetLargeShape::PgLo) => None,
(FleetKvShape::Postgres, FleetLargeShape::LocalUndeclared) => Some(Error::Io(
"fleet: the local large tier rejects without the shared-media declaration \
(ADR-008 \u{a7}3): a fleet-mode constructor requires the explicit \
FleetSharedMedia assertion"
.to_owned(),
)),
(kv, _) => Some(Error::Io(format!(
"fleet: the kv engine {kv:?} rejects the fleet declaration (ADR-012 \u{a7}3): \
kv = postgres is the composite fleet-validity rule's kv clause — \
{kv:?} is never fleet-valid"
))),
}
}
impl FleetKvShape {
#[cfg(any(feature = "mem", feature = "postgres", feature = "sqlite"))]
#[allow(dead_code)]
fn of(engine: &KvEngine) -> FleetKvShape {
match engine {
#[cfg(feature = "mem")]
KvEngine::Mem(_) => FleetKvShape::Mem,
#[cfg(feature = "postgres")]
KvEngine::Postgres(_) => FleetKvShape::Postgres,
#[cfg(feature = "sqlite")]
KvEngine::Sqlite(_) => FleetKvShape::Sqlite,
}
}
}
#[allow(dead_code)]
async fn ensure_gc_schema_at_construction(
kv: &KvEngine,
large: Option<&LargeEngine>,
) -> Result<(), Error> {
#[cfg(not(feature = "mem"))]
let _ = large;
match kv {
#[cfg(feature = "mem")]
KvEngine::Mem(_) => match large {
Some(engine) => engine.ensure_gc_schema().await,
None => Ok(()),
},
#[cfg(feature = "postgres")]
KvEngine::Postgres(_) => kv.ensure_gc_schema().await,
#[cfg(feature = "sqlite")]
KvEngine::Sqlite(_) => kv.ensure_gc_schema().await,
#[cfg(not(any(feature = "mem", feature = "sqlite", feature = "postgres")))]
_ => Err(super::engines::no_kv_engine()),
}
}
impl Store {
#[cfg(feature = "mem")]
pub fn mem_only() -> Store {
Store {
kv: KvEngine::Mem(MemBackend::default()),
large: None,
threshold: crate::dispatch::DEFAULT_DISPATCH_THRESHOLD,
window_delay: DEFAULT_DELETE_WINDOW_DELAY,
mode: MemOnly,
gc: GcHome::default(),
fleet: None,
}
}
#[cfg(feature = "mem")]
pub fn kv_only_mem(threshold: u64) -> Store {
Store {
kv: KvEngine::Mem(MemBackend::default()),
large: None,
threshold,
window_delay: DEFAULT_DELETE_WINDOW_DELAY,
mode: KvOnly,
gc: GcHome::default(),
fleet: None,
}
}
#[cfg(feature = "mem")]
pub async fn dual_tier_mem_local(
root: impl Into<PathBuf>,
threshold: u64,
) -> Result<Store, Error> {
Ok(Store {
kv: KvEngine::Mem(MemBackend::default()),
large: Some(LargeEngine::Local(LocalBackend::new(root).await?)),
threshold,
window_delay: DEFAULT_DELETE_WINDOW_DELAY,
mode: DualTier,
gc: GcHome::default(),
fleet: None,
})
}
#[cfg(feature = "sqlite")]
pub async fn kv_only_sqlite(
database: impl Into<PathBuf>,
threshold: u64,
window_delay: Duration,
) -> Result<Store, Error> {
let kv = KvEngine::Sqlite(SqliteBackend::new(database).await?);
ensure_gc_schema_at_construction(&kv, None).await?;
Ok(Store {
kv,
large: None,
threshold,
window_delay,
mode: KvOnly,
gc: GcHome::default(),
fleet: None,
})
}
#[cfg(feature = "sqlite")]
pub async fn dual_tier_sqlite_local(
database: impl Into<PathBuf>,
root: impl Into<PathBuf>,
threshold: u64,
window_delay: Duration,
) -> Result<Store, Error> {
let kv = KvEngine::Sqlite(SqliteBackend::new(database).await?);
let large = LargeEngine::Local(LocalBackend::new(root).await?);
ensure_gc_schema_at_construction(&kv, Some(&large)).await?;
Ok(Store {
kv,
large: Some(large),
threshold,
window_delay,
mode: DualTier,
gc: GcHome::default(),
fleet: None,
})
}
#[cfg(feature = "postgres")]
pub async fn kv_only_postgres(
config: &str,
opts: PgOpts,
threshold: u64,
window_delay: Duration,
) -> Result<Store, Error> {
let kv = KvEngine::Postgres(Arc::new(PostgresBackend::new(config, opts).await?));
ensure_gc_schema_at_construction(&kv, None).await?;
Ok(Store {
kv,
large: None,
threshold,
window_delay,
mode: KvOnly,
gc: GcHome::default(),
fleet: None,
})
}
#[cfg(feature = "postgres")]
pub async fn dual_tier_postgres_local(
config: &str,
opts: PgOpts,
root: impl Into<PathBuf>,
threshold: u64,
window_delay: Duration,
) -> Result<Store, Error> {
let kv = KvEngine::Postgres(Arc::new(PostgresBackend::new(config, opts).await?));
let large = LargeEngine::Local(LocalBackend::new(root).await?);
ensure_gc_schema_at_construction(&kv, Some(&large)).await?;
Ok(Store {
kv,
large: Some(large),
threshold,
window_delay,
mode: DualTier,
gc: GcHome::default(),
fleet: None,
})
}
#[cfg(all(feature = "postgres", feature = "pg-lo"))]
pub async fn dual_tier_postgres_pglo(
config: &str,
opts: PgOpts,
large_config: &str,
large_opts: PgOpts,
threshold: u64,
window_delay: Duration,
) -> Result<Store, Error> {
let kv = KvEngine::Postgres(Arc::new(PostgresBackend::new(config, opts).await?));
let large = LargeEngine::PgLo(PgLoBackend::new(large_config, large_opts).await?);
ensure_gc_schema_at_construction(&kv, Some(&large)).await?;
Ok(Store {
kv,
large: Some(large),
threshold,
window_delay,
mode: DualTier,
gc: GcHome::default(),
fleet: None,
})
}
#[cfg(feature = "postgres")]
pub async fn kv_only_postgres_fleet(
config: &str,
opts: PgOpts,
threshold: u64,
window_delay: Duration,
pin_ttl: Duration,
) -> Result<Store, Error> {
if pin_ttl.is_zero() {
return Err(Error::Io(
"fleet: pin_ttl must be positive — the TTL/3 renewal cadence degenerates \
at zero, and a zero-TTL pin expires at once"
.to_owned(),
));
}
fleet_pool_size_guard(&opts)?;
let kv = KvEngine::Postgres(Arc::new(
PostgresBackend::new_with_pin_ttl(config, opts, pin_ttl).await?,
));
if let Some(err) = fleet_validity_error(FleetKvShape::of(&kv), FleetLargeShape::None) {
return Err(err);
}
ensure_gc_schema_at_construction(&kv, None).await?;
Ok(Store {
kv,
large: None,
threshold,
window_delay,
mode: KvOnly,
gc: GcHome::default(),
fleet: Some(FleetState {
pin_ttl,
owner: mint_owner_id(),
}),
})
}
#[cfg(feature = "postgres")]
pub async fn dual_tier_postgres_local_fleet(
config: &str,
opts: PgOpts,
root: impl Into<PathBuf>,
threshold: u64,
window_delay: Duration,
pin_ttl: Duration,
shared_media: FleetSharedMedia,
) -> Result<Store, Error> {
if pin_ttl.is_zero() {
return Err(Error::Io(
"fleet: pin_ttl must be positive — the TTL/3 renewal cadence degenerates \
at zero, and a zero-TTL pin expires at once"
.to_owned(),
));
}
fleet_pool_size_guard(&opts)?;
let kv = KvEngine::Postgres(Arc::new(
PostgresBackend::new_with_pin_ttl(config, opts, pin_ttl).await?,
));
let large = LargeEngine::Local(LocalBackend::new(root).await?);
let _ = shared_media;
if let Some(err) =
fleet_validity_error(FleetKvShape::of(&kv), FleetLargeShape::LocalSharedMedia)
{
return Err(err);
}
ensure_gc_schema_at_construction(&kv, Some(&large)).await?;
Ok(Store {
kv,
large: Some(large),
threshold,
window_delay,
mode: DualTier,
gc: GcHome::default(),
fleet: Some(FleetState {
pin_ttl,
owner: mint_owner_id(),
}),
})
}
#[cfg(all(feature = "postgres", feature = "pg-lo"))]
pub async fn dual_tier_postgres_pglo_fleet(
config: &str,
opts: PgOpts,
large_config: &str,
large_opts: PgOpts,
threshold: u64,
window_delay: Duration,
pin_ttl: Duration,
) -> Result<Store, Error> {
if pin_ttl.is_zero() {
return Err(Error::Io(
"fleet: pin_ttl must be positive — the TTL/3 renewal cadence degenerates \
at zero, and a zero-TTL pin expires at once"
.to_owned(),
));
}
fleet_pool_size_guard(&opts)?;
let kv_backend = Arc::new(PostgresBackend::new_with_pin_ttl(config, opts, pin_ttl).await?);
let large_backend = PgLoBackend::new(large_config, large_opts).await?;
let kv_id = kv_backend.system_identifier().await?;
let large_id = large_backend.system_identifier().await?;
let kv = KvEngine::Postgres(kv_backend);
let large = LargeEngine::PgLo(large_backend);
if let Some(err) = fleet_validity_error(FleetKvShape::of(&kv), FleetLargeShape::PgLo) {
return Err(err);
}
if kv_id != large_id {
return Err(Error::Io(format!(
"fleet: the kv engine and the pg-lo large engine resolve to different pg \
instances (system_identifier {kv_id} vs {large_id}) — ADR-012 \u{a7}4's \
same-instance clause; cross-instance pg-lo is valid only as fleet: false"
)));
}
ensure_gc_schema_at_construction(&kv, Some(&large)).await?;
Ok(Store {
kv,
large: Some(large),
threshold,
window_delay,
mode: DualTier,
gc: GcHome::default(),
fleet: Some(FleetState {
pin_ttl,
owner: mint_owner_id(),
}),
})
}
#[cfg(all(feature = "mem", feature = "pg-lo"))]
pub async fn dual_tier_mem_pglo(
config: &str,
opts: PgOpts,
threshold: u64,
window_delay: Duration,
) -> Result<Store, Error> {
let kv = KvEngine::Mem(MemBackend::default());
let large = LargeEngine::PgLo(PgLoBackend::new(config, opts).await?);
ensure_gc_schema_at_construction(&kv, Some(&large)).await?;
Ok(Store {
kv,
large: Some(large),
threshold,
window_delay,
mode: DualTier,
gc: GcHome::default(),
fleet: None,
})
}
pub fn mode(&self) -> StoreMode {
self.mode
}
pub(crate) fn threshold(&self) -> Option<u64> {
match self.mode {
DualTier | KvOnly => Some(self.threshold),
MemOnly => None,
}
}
#[allow(dead_code)]
pub(crate) fn window_delay(&self) -> Duration {
self.window_delay
}
pub(crate) fn fleet(&self) -> Option<&FleetState> {
self.fleet.as_ref()
}
pub(crate) fn tier_for(&self, len: u64) -> Tier {
match self.mode {
DualTier => route(len, self.threshold),
KvOnly | MemOnly => Tier::Kv,
}
}
pub(crate) fn kv(&self) -> &KvEngine {
&self.kv
}
pub(crate) fn large(&self) -> Option<&LargeEngine> {
self.large.as_ref()
}
pub(crate) fn gc(&self) -> &GcHome {
&self.gc
}
#[cfg(all(feature = "ops", feature = "postgres"))]
pub(crate) async fn ops_pin_renew(&self, key: &[u8], owner: &[u8]) -> Result<bool, Error> {
match self.fleet_sql_host() {
Some(host) => EngineFleetState::pin_renew(&**host, key, owner).await,
None => Err(Error::Io(
"pin renewal outside a declared fleet store: under fleet: false the token \
is the in-process guard's custody (nothing expires) — the renewal probe \
answers from the ops layer's custody, never through this verb"
.to_owned(),
)),
}
}
#[cfg(all(feature = "ops", feature = "postgres"))]
pub(crate) async fn ops_pin_check(&self, key: &[u8]) -> Result<crate::facade::PinState, Error> {
match self.fleet_sql_host() {
Some(host) => EngineFleetState::pin_check(&**host, key).await,
None => Err(Error::Io(
"pin check outside a declared fleet store: the pin-row classification \
is the fleet form's act (ADR-010 §2) — under fleet: false the in-process \
guard's custody is the classification, and the ops layer holds it"
.to_owned(),
)),
}
}
}
#[cfg(all(test, feature = "sqlite"))]
mod sqlite_construction_tests {
use super::*;
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
struct TempSite {
database: PathBuf,
root: PathBuf,
}
impl TempSite {
fn path(&self) -> &Path {
&self.root
}
fn drop_sidecar(&self, suffix: &str) {
let base = self.database.to_string_lossy().into_owned();
let _ = fs::remove_file(format!("{base}{suffix}"));
}
}
impl Drop for TempSite {
fn drop(&mut self) {
self.drop_sidecar("-wal");
self.drop_sidecar("-shm");
let _ = fs::remove_file(&self.database);
let _ = fs::remove_dir_all(&self.root);
}
}
fn temp_site(tag: &str) -> TempSite {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let base = std::env::temp_dir().join(format!(
"alkblobs-store-sqlite-{tag}-{}-{nanos:x}-{seq}",
std::process::id()
));
fs::create_dir_all(&base).expect("temp site created");
TempSite {
database: base.join("pool.db"),
root: base.join("local-root"),
}
}
fn key_for(content: &[u8]) -> Key {
Key::derive(content, content.len() as u64)
}
fn sqlite_engine(store: &Store) -> &SqliteBackend {
match store.kv() {
#[cfg(feature = "sqlite")]
KvEngine::Sqlite(engine) => engine,
#[cfg(feature = "mem")]
KvEngine::Mem(_) => panic!("these tests construct sqlite stores"),
#[cfg(feature = "postgres")]
KvEngine::Postgres(_) => panic!("these tests construct sqlite stores"),
}
}
async fn assert_gc_schema_ran(store: &Store) {
let engine = sqlite_engine(store);
assert!(
engine.table_exists("blobs").await.unwrap(),
"the pool table exists at construction"
);
assert!(
engine.table_exists("gc_window").await.unwrap(),
"ensure_gc_schema ran at construction: the window table exists in the fresh database"
);
assert!(
!engine.table_exists("gc_pins").await.unwrap(),
"gc_pins is never created on sqlite (ADR-012 §5)"
);
}
#[tokio::test]
async fn dual_tier_sqlite_local_constructs_and_routes() {
let site = temp_site("dual-tier");
let store = Store::dual_tier_sqlite_local(
&site.database,
site.path(),
1024,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("dual-tier sqlite+local constructs");
assert_eq!(store.mode(), DualTier);
assert_eq!(store.threshold(), Some(1024));
assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY);
assert!(store.large().is_some(), "both tiers present");
for (len, tier) in [
(0u64, Tier::Kv),
(1023, Tier::Kv),
(1024, Tier::Kv),
(1025, Tier::Large),
(u64::MAX, Tier::Large),
] {
assert_eq!(
store.tier_for(len),
tier,
"length {len} routes deterministically"
);
}
assert_gc_schema_ran(&store).await;
}
#[tokio::test]
async fn kv_only_sqlite_constructs_and_rejects_over_threshold_puts() {
let site = temp_site("kv-only");
let store = Store::kv_only_sqlite(&site.database, 64, DEFAULT_DELETE_WINDOW_DELAY)
.await
.expect("kv-only sqlite constructs");
assert_eq!(store.mode(), KvOnly);
assert_eq!(store.threshold(), Some(64));
assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY);
assert!(store.large().is_none(), "kv-only has no large tier");
assert_gc_schema_ran(&store).await;
for len in [0u64, 63, 64, 65, u64::MAX] {
assert_eq!(
store.tier_for(len),
Tier::Kv,
"kv-only routes every length kv"
);
}
let err = store
.put(Some(65), Cursor::new(&b"never read"[..]))
.await
.unwrap_err();
assert!(
err.to_string().contains("kv-only mode rejects"),
"the immediate rejection is the mode's: {err}"
);
assert_eq!(
store.list().await.unwrap(),
Vec::<Key>::new(),
"zero residue"
);
let streamed = [3u8; 100];
let err = store
.put(None, Cursor::new(&streamed[..]))
.await
.unwrap_err();
assert!(
err.to_string()
.contains("kv-only mode rejects an over-threshold put"),
"the overflow rejection is the mode's, not the stream's: {err}"
);
assert_eq!(
store.list().await.unwrap(),
Vec::<Key>::new(),
"zero residue"
);
let at_threshold = vec![9u8; 64];
let (key, pin) = store
.put(Some(64), Cursor::new(&at_threshold[..]))
.await
.expect("exactly-at-threshold known put lands");
assert_eq!(key, key_for(&at_threshold));
assert!(store.has(&key.as_bytes()).await.unwrap());
drop(pin);
}
#[tokio::test]
async fn window_delay_default_is_pinned_and_constructor_tunable() {
assert_eq!(
DEFAULT_DELETE_WINDOW_DELAY,
Duration::from_secs(5 * 60),
"the shipped default is 5 minutes (ADR-008 §2)"
);
let site = temp_site("delay-default");
let store = Store::dual_tier_sqlite_local(
&site.database,
site.path(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("constructs at the shipped default");
assert_eq!(store.window_delay(), Duration::from_secs(300));
let site = temp_site("delay-tuned");
let store = Store::kv_only_sqlite(&site.database, 64, Duration::from_secs(90))
.await
.expect("constructs at a custom tuning");
assert_eq!(store.window_delay(), Duration::from_secs(90));
let site = temp_site("delay-zero");
let store = Store::dual_tier_sqlite_local(&site.database, site.path(), 64, Duration::ZERO)
.await
.expect("the degenerate zero tuning is legal");
assert_eq!(store.window_delay(), Duration::ZERO);
let site = temp_site("delay-long");
Store::kv_only_sqlite(&site.database, 64, Duration::from_secs(6000))
.await
.expect("a long delay is a legal tuning too");
}
#[tokio::test]
async fn dual_tier_sqlite_shape_round_trips_end_to_end_through_both_tiers() {
let site = temp_site("round-trip");
let store = Store::dual_tier_sqlite_local(&site.database, site.path(), 64, Duration::ZERO)
.await
.expect("dual-tier constructs");
let small = b"a sub-threshold blob";
let (key, pin) = store
.put(Some(small.len() as u64), Cursor::new(&small[..]))
.await
.expect("kv put publishes");
assert_eq!(key, key_for(small));
assert!(
store.kv().has(&key.as_bytes()).await.unwrap(),
"landed on the kv tier (sqlite)"
);
assert!(
!store.large().unwrap().has(&key.as_bytes()).await.unwrap(),
"no large-tier copy on the kv route"
);
let (len, blob) = store
.get(&key.as_bytes())
.await
.unwrap()
.expect("entry present");
assert_eq!(len, small.len() as u64);
assert_eq!(blob.read_at(0, len).await.unwrap(), &small[..]);
drop(pin);
let big = vec![42u8; 200];
let (key, pin) = store
.put(Some(200), Cursor::new(&big[..]))
.await
.expect("large staged put publishes");
assert_eq!(key, key_for(&big));
assert!(
store.large().unwrap().has(&key.as_bytes()).await.unwrap(),
"landed on the large tier (commit-rename)"
);
assert!(
!store.kv().has(&key.as_bytes()).await.unwrap(),
"no kv copy on the large route"
);
let (len, blob) = store
.get(&key.as_bytes())
.await
.unwrap()
.expect("entry present");
assert_eq!(len, 200);
assert_eq!(blob.read_at(0, len).await.unwrap(), big);
drop(pin);
let listed = store.list().await.expect("list succeeds");
assert_eq!(listed.len(), 2, "one entry per tier, both enumerated");
for window in listed.windows(2) {
assert!(window[0] < window[1], "list is sorted");
}
let (_, pin) = store
.put(Some(small.len() as u64), Cursor::new(&small[..]))
.await
.expect("dedup re-put");
assert_eq!(store.list().await.unwrap().len(), 2, "still two entries");
drop(pin);
}
}
#[cfg(all(test, feature = "postgres"))]
mod postgres_construction_tests {
use super::*;
use crate::postgres::test_support::SchemaJanitor;
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
struct TempRoot(PathBuf);
fn temp_root(tag: &'static str) -> TempRoot {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let dir = std::env::temp_dir().join(format!(
"alkblobs-store-pg-{tag}-{}-{:x}-{seq}",
std::process::id(),
nanos
));
fs::create_dir_all(&dir).expect("temp root created");
TempRoot(dir)
}
impl TempRoot {
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempRoot {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
static SCHEMA_SEQ: AtomicU64 = AtomicU64::new(0);
fn fresh_schema(tag: &str) -> String {
format!(
"abt_sctor_{}_{}_{}",
tag,
std::process::id(),
SCHEMA_SEQ.fetch_add(1, Ordering::Relaxed)
)
}
fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
fn harness_dsn() -> Option<String> {
let host = env_nonempty("ALKBLOBS_PG_HOST")?;
Some(format!(
"host={host} port={} user={} password={} dbname={}",
env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()),
env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()),
env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()),
env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()),
))
}
fn skip_note() {
eprintln!(
"skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB"
);
}
fn fresh_opts(tag: &str) -> Option<(PgOpts, SchemaJanitor)> {
let dsn = harness_dsn()?;
let schema = fresh_schema(tag);
Some((
PgOpts {
schema: schema.clone(),
..Default::default()
},
SchemaJanitor::new(&dsn, &schema),
))
}
fn key_for(content: &[u8]) -> Key {
Key::derive(content, content.len() as u64)
}
fn postgres_engine(store: &Store) -> &PostgresBackend {
match store.kv() {
#[cfg(feature = "postgres")]
KvEngine::Postgres(engine) => engine,
#[cfg(feature = "mem")]
KvEngine::Mem(_) => panic!("these tests construct postgres stores"),
#[cfg(feature = "sqlite")]
KvEngine::Sqlite(_) => panic!("these tests construct postgres stores"),
}
}
async fn assert_gc_schema_ran(store: &Store) {
let engine = postgres_engine(store);
assert!(
engine.table_exists("blobs").await.unwrap(),
"the pool table exists at construction"
);
assert!(
engine.table_exists("gc_window").await.unwrap(),
"ensure_gc_schema ran at construction: the window table exists in the fresh schema"
);
assert!(
engine.table_exists("gc_pins").await.unwrap(),
"ensure_gc_schema ran at construction: the pins table exists (the fleet host's both-tables row)"
);
}
#[tokio::test]
async fn dual_tier_postgres_local_constructs_and_routes() {
let Some((opts, _janitor)) = fresh_opts("dual-tier") else {
skip_note();
return;
};
let root = temp_root("dual-tier");
let dsn = harness_dsn().expect("harness dsn re-read");
let store = Store::dual_tier_postgres_local(
&dsn,
opts,
root.path(),
1024,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("dual-tier postgres+local constructs");
assert_eq!(store.mode(), DualTier);
assert_eq!(store.threshold(), Some(1024));
assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY);
assert!(store.large().is_some(), "both tiers present");
for (len, tier) in [
(0u64, Tier::Kv),
(1023, Tier::Kv),
(1024, Tier::Kv),
(1025, Tier::Large),
(u64::MAX, Tier::Large),
] {
assert_eq!(
store.tier_for(len),
tier,
"length {len} routes deterministically"
);
}
assert_gc_schema_ran(&store).await;
}
#[tokio::test]
async fn kv_only_postgres_constructs_and_rejects_over_threshold_puts() {
let Some((opts, _janitor)) = fresh_opts("kv-only") else {
skip_note();
return;
};
let dsn = harness_dsn().expect("harness dsn re-read");
let store = Store::kv_only_postgres(&dsn, opts, 64, DEFAULT_DELETE_WINDOW_DELAY)
.await
.expect("kv-only postgres constructs");
assert_eq!(store.mode(), KvOnly);
assert_eq!(store.threshold(), Some(64));
assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY);
assert!(store.large().is_none(), "kv-only has no large tier");
assert_gc_schema_ran(&store).await;
for len in [0u64, 63, 64, 65, u64::MAX] {
assert_eq!(
store.tier_for(len),
Tier::Kv,
"kv-only routes every length kv"
);
}
let err = store
.put(Some(65), Cursor::new(&b"never read"[..]))
.await
.unwrap_err();
assert!(
err.to_string().contains("kv-only mode rejects"),
"the immediate rejection is the mode's: {err}"
);
assert_eq!(
store.list().await.unwrap(),
Vec::<Key>::new(),
"zero residue"
);
let streamed = [3u8; 100];
let err = store
.put(None, Cursor::new(&streamed[..]))
.await
.unwrap_err();
assert!(
err.to_string()
.contains("kv-only mode rejects an over-threshold put"),
"the overflow rejection is the mode's, not the stream's: {err}"
);
assert_eq!(
store.list().await.unwrap(),
Vec::<Key>::new(),
"zero residue"
);
let at_threshold = vec![9u8; 64];
let (key, pin) = store
.put(Some(64), Cursor::new(&at_threshold[..]))
.await
.expect("exactly-at-threshold known put lands");
assert_eq!(key, key_for(&at_threshold));
assert!(store.has(&key.as_bytes()).await.unwrap());
drop(pin);
}
#[tokio::test]
async fn window_delay_default_is_pinned_and_constructor_tunable() {
assert_eq!(
DEFAULT_DELETE_WINDOW_DELAY,
Duration::from_secs(5 * 60),
"the shipped default is 5 minutes (ADR-008 §2)"
);
let Some((opts, _janitor)) = fresh_opts("delay-default") else {
skip_note();
return;
};
let dsn = harness_dsn().expect("harness dsn re-read");
let root = temp_root("delay-default");
let store = Store::dual_tier_postgres_local(
&dsn,
opts,
root.path(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("constructs at the shipped default");
assert_eq!(store.window_delay(), Duration::from_secs(300));
let Some((opts, _janitor)) = fresh_opts("delay-tuned") else {
return;
};
let store = Store::kv_only_postgres(&dsn, opts, 64, Duration::from_secs(90))
.await
.expect("constructs at a custom tuning");
assert_eq!(store.window_delay(), Duration::from_secs(90));
let Some((opts, _janitor)) = fresh_opts("delay-zero") else {
return;
};
let root = temp_root("delay-zero");
let store = Store::dual_tier_postgres_local(&dsn, opts, root.path(), 64, Duration::ZERO)
.await
.expect("the degenerate zero tuning is legal");
assert_eq!(store.window_delay(), Duration::ZERO);
let Some((opts, _janitor)) = fresh_opts("delay-long") else {
return;
};
Store::kv_only_postgres(&dsn, opts, 64, Duration::from_secs(6000))
.await
.expect("a long delay is a legal tuning too");
}
#[tokio::test]
async fn dual_tier_postgres_shape_round_trips_end_to_end_through_both_tiers() {
let Some((opts, _janitor)) = fresh_opts("round-trip") else {
skip_note();
return;
};
let dsn = harness_dsn().expect("harness dsn re-read");
let root = temp_root("round-trip");
let store = Store::dual_tier_postgres_local(&dsn, opts, root.path(), 64, Duration::ZERO)
.await
.expect("dual-tier constructs");
let small = b"a sub-threshold blob";
let (key, pin) = store
.put(Some(small.len() as u64), Cursor::new(&small[..]))
.await
.expect("kv put publishes");
assert_eq!(key, key_for(small));
assert!(
store.kv().has(&key.as_bytes()).await.unwrap(),
"landed on the kv tier (postgres)"
);
assert!(
!store.large().unwrap().has(&key.as_bytes()).await.unwrap(),
"no large-tier copy on the kv route"
);
let (len, blob) = store
.get(&key.as_bytes())
.await
.expect("get succeeds")
.expect("kv entry present");
assert_eq!(len, small.len() as u64, "whole-blob length exact");
assert_eq!(
blob.read_at(0, len).await.expect("full window"),
small,
"kv-tier round-trip byte-identical"
);
drop(pin);
let large = vec![7u8; 128];
let (key, pin) = store
.put(Some(large.len() as u64), Cursor::new(&large[..]))
.await
.expect("staged put publishes");
assert_eq!(key, key_for(&large));
assert!(
store.large().unwrap().has(&key.as_bytes()).await.unwrap(),
"landed on the large tier (local)"
);
assert!(
!store.kv().has(&key.as_bytes()).await.unwrap(),
"no kv-tier copy on the large route"
);
let (len, blob) = store
.get(&key.as_bytes())
.await
.expect("get succeeds")
.expect("large entry present");
assert_eq!(len, large.len() as u64);
assert_eq!(
blob.read_at(0, len).await.expect("full window"),
large,
"large-tier round-trip byte-identical"
);
drop(pin);
let mut expected: Vec<Key> = [key_for(small), key_for(&large)].to_vec();
expected.sort();
assert_eq!(
store.list().await.expect("list succeeds"),
expected,
"the pool list is the sorted union of both tiers"
);
}
#[derive(Default)]
struct SetSource {
live: parking_lot::Mutex<BTreeSet<Key>>,
}
impl LivenessSource for SetSource {
fn live_keys(&self) -> Vec<Key> {
self.live.lock().iter().cloned().collect()
}
fn contains(&self, key: &Key) -> bool {
self.live.lock().contains(key)
}
}
#[tokio::test]
async fn sweep_runs_the_durable_form_on_the_postgres_host() {
let source = Arc::new(SetSource::default());
let Some((opts, _janitor)) = fresh_opts("durable-sweep") else {
drop(source);
skip_note();
return;
};
let dsn = harness_dsn().expect("harness dsn re-read");
let root = temp_root("durable-sweep");
let store = Store::dual_tier_postgres_local(&dsn, opts, root.path(), 64, Duration::ZERO)
.await
.expect("dual-tier constructs");
let _ = store.register_liveness_source(LivenessRegistration::InProcess(source.clone()));
let dead = key_for(b"a dead candidate under the durable form");
store
.put(
Some(dead_content().len() as u64),
Cursor::new(&dead_content()[..]),
)
.await
.expect("the candidate's put publishes");
assert_eq!(store.list().await.unwrap(), vec![dead.clone()]);
let first = store.sweep().await.expect("sweeps are never errors");
assert_eq!(first.aborted, None);
assert_eq!(
first.candidates_staged, 1,
"the dead candidate staged into the durable window"
);
assert_eq!(
first.deleted, 0,
"the durable form executes nothing in its own staging sweep"
);
assert!(
store.has(&dead.as_bytes()).await.unwrap(),
"the staged candidate still waits out its window"
);
let second = store.sweep().await.expect("sweeps are never errors");
assert_eq!(second.aborted, None);
assert_eq!(
second.deleted, 1,
"the next sweep executed the overdue candidate"
);
assert_eq!(
second.candidates_staged, 0,
"the pool holds nothing dead after the execution"
);
assert!(
!store.has(&dead.as_bytes()).await.unwrap(),
"the expired candidate is gone"
);
}
fn dead_content() -> Vec<u8> {
b"a dead candidate under the durable form".to_vec()
}
}
#[cfg(all(test, feature = "pg-lo", feature = "mem"))]
mod mem_pglo_construction_tests {
use super::*;
use crate::backend::Backend;
use crate::pg_common::test_support::DatabaseJanitor;
use crate::pg_lo::PgLoBackend;
use std::io::Cursor;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio_postgres::NoTls;
static DB_SEQ: AtomicU64 = AtomicU64::new(0);
fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
fn harness_dsn() -> Option<String> {
let host = env_nonempty("ALKBLOBS_PG_HOST")?;
Some(format!(
"host={host} port={} user={} password={} dbname={}",
env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()),
env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()),
env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()),
env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()),
))
}
fn skip_note() {
eprintln!(
"skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB"
);
}
struct TestDatabase {
dsn: String,
_janitor: DatabaseJanitor,
}
fn admin_dsn(dsn: &str) -> String {
let base = dsn.rsplit_once("dbname=").expect("dbname").0;
format!("{base} dbname=postgres")
}
fn db_dsn(dsn: &str, db: &str) -> String {
let base = dsn.rsplit_once("dbname=").expect("dbname").0;
format!("{base} dbname={db}")
}
async fn fresh_db(tag: &str) -> Option<TestDatabase> {
let dsn = harness_dsn()?;
let db = format!(
"abtmplo_{}_{}_{:x}",
tag,
std::process::id(),
DB_SEQ.fetch_add(1, Ordering::Relaxed)
);
let admin = admin_dsn(&dsn);
{
let (client, conn) = tokio_postgres::connect(&admin, NoTls)
.await
.expect("admin connect");
tokio::spawn(async move {
let _ = conn.await;
});
client
.execute(
&format!(
"CREATE DATABASE {}",
crate::pg_common::quote_identifier(&db)
),
&[],
)
.await
.expect("create test database");
}
Some(TestDatabase {
dsn: db_dsn(&dsn, &db),
_janitor: DatabaseJanitor::new(&admin, &db),
})
}
fn pglo_engine(store: &Store) -> &PgLoBackend {
match store.large() {
Some(LargeEngine::PgLo(engine)) => engine,
Some(LargeEngine::Local(_)) | None => {
panic!("these tests construct pg-lo-tier stores")
}
}
}
fn key_for(content: &[u8]) -> Key {
Key::derive(content, content.len() as u64)
}
#[tokio::test]
async fn dual_tier_mem_pglo_constructs_and_validates() {
let Some(site) = fresh_db("mem-pglo").await else {
skip_note();
return;
};
let store = Store::dual_tier_mem_pglo(
&site.dsn,
PgOpts::default(),
1024,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("mem+pglo constructs");
assert_eq!(store.mode(), DualTier);
assert_eq!(store.threshold(), Some(1024));
assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY);
assert!(store.large().is_some(), "both tiers present");
for (len, tier) in [
(0u64, Tier::Kv),
(1023, Tier::Kv),
(1024, Tier::Kv),
(1025, Tier::Large),
(u64::MAX, Tier::Large),
] {
assert_eq!(
store.tier_for(len),
tier,
"length {len} routes deterministically"
);
}
let lo = pglo_engine(&store);
assert!(
lo.table_exists("lo_entries").await.unwrap(),
"the pg-lo engine's companion table bootstrapped at construction"
);
assert!(
lo.table_exists("gc_window").await.unwrap(),
"ensure_gc_schema ran at construction on the pg-lo host: \
gc_window in the pg-lo schema (the one-window-host rule's \
large-host arm)"
);
assert!(
!lo.table_exists("gc_pins").await.unwrap(),
"gc_pins is never created by pg-lo (fleet pins live on the kv host)"
);
}
#[tokio::test]
async fn dual_tier_mem_pglo_shape_round_trips_with_lo_placement() {
let Some(site) = fresh_db("mem-pglo-rt").await else {
skip_note();
return;
};
let store = Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::ZERO)
.await
.expect("mem+pglo constructs");
let small = b"a sub-threshold blob";
let (key, pin) = store
.put(Some(small.len() as u64), Cursor::new(&small[..]))
.await
.expect("kv put publishes");
assert_eq!(key, key_for(small));
assert!(
store.kv().has(&key.as_bytes()).await.unwrap(),
"landed on the kv tier (mem)"
);
let lo = pglo_engine(&store);
assert!(
!lo.has(&key.as_bytes()).await.unwrap(),
"kv entries never land in the pg database"
);
drop(pin);
let big = vec![7u8; 200];
let (key, pin) = store
.put(Some(200), Cursor::new(&big[..]))
.await
.expect("staged put publishes");
assert_eq!(key, key_for(&big));
assert!(
lo.has(&key.as_bytes()).await.unwrap(),
"landed on the pg-lo tier: the companion row is present"
);
assert_eq!(
lo.size(&key.as_bytes()).await.unwrap(),
Some(200),
"the companion row's size is exact"
);
assert!(
!store.kv().has(&key.as_bytes()).await.unwrap(),
"no kv copy on the large route"
);
let (len, blob) = store
.get(&key.as_bytes())
.await
.unwrap()
.expect("entry present");
assert_eq!(len, 200);
assert_eq!(
blob.read_at(0, len).await.unwrap(),
big,
"the LO's content round-trips byte-identical (large placement)"
);
drop(pin);
let mut expected: Vec<Key> = [key_for(small), key_for(&big)].to_vec();
expected.sort();
assert_eq!(
store.list().await.expect("list succeeds"),
expected,
"the pool list is the sorted union of both tiers"
);
}
#[tokio::test]
async fn window_delay_default_is_pinned_and_constructor_tunable() {
assert_eq!(
DEFAULT_DELETE_WINDOW_DELAY,
Duration::from_secs(5 * 60),
"the shipped default is 5 minutes (ADR-008 §2)"
);
let Some(site) = fresh_db("mem-pglo-delay").await else {
skip_note();
return;
};
let store = Store::dual_tier_mem_pglo(
&site.dsn,
PgOpts::default(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("constructs at the shipped default");
assert_eq!(store.window_delay(), Duration::from_secs(300));
let store =
Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::from_secs(90))
.await
.expect("constructs at a custom tuning");
assert_eq!(store.window_delay(), Duration::from_secs(90));
let store = Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::ZERO)
.await
.expect("the degenerate zero tuning is legal");
assert_eq!(store.window_delay(), Duration::ZERO);
Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::from_secs(6000))
.await
.expect("a long delay is a legal tuning too");
}
#[derive(Default)]
struct SetSource {
live: parking_lot::Mutex<BTreeSet<Key>>,
}
impl LivenessSource for SetSource {
fn live_keys(&self) -> Vec<Key> {
self.live.lock().iter().cloned().collect()
}
fn contains(&self, key: &Key) -> bool {
self.live.lock().contains(key)
}
}
#[tokio::test]
async fn sweep_runs_the_durable_form_on_the_pg_lo_host() {
let Some(site) = fresh_db("mem-pglo-sweep").await else {
skip_note();
return;
};
let store = Store::dual_tier_mem_pglo(&site.dsn, PgOpts::default(), 64, Duration::ZERO)
.await
.expect("mem+pglo constructs");
let _ = store.register_liveness_source(LivenessRegistration::InProcess(Arc::new(
SetSource::default(),
)));
let content = b"a dead candidate under the pg-lo host's window";
let (dead, _) = store
.put(Some(content.len() as u64), Cursor::new(&content[..]))
.await
.expect("the candidate's put publishes");
assert_eq!(store.list().await.unwrap(), vec![dead.clone()]);
let first = store.sweep().await.expect("sweeps are never errors");
assert_eq!(first.aborted, None);
assert_eq!(
first.candidates_staged, 1,
"the dead candidate staged into the pg-lo schema's gc_window"
);
assert_eq!(
first.deleted, 0,
"the durable form executes nothing in its own staging sweep \
(the form-branch-is-large-host proof)"
);
assert!(
store.has(&dead.as_bytes()).await.unwrap(),
"the staged candidate still waits out its window"
);
let second = store.sweep().await.expect("sweeps are never errors");
assert_eq!(second.aborted, None);
assert_eq!(
second.candidates_staged, 0,
"nothing newly dead after the execution"
);
assert_eq!(
second.deleted, 1,
"the next sweep executed the overdue set, re-arbitrated at \
execution time"
);
assert!(!store.has(&dead.as_bytes()).await.unwrap());
}
}
#[cfg(all(test, feature = "pg-lo", feature = "postgres"))]
mod postgres_pglo_construction_tests {
use super::*;
use crate::pg_common::test_support::DatabaseJanitor;
use std::io::Cursor;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio_postgres::NoTls;
static DB_SEQ: AtomicU64 = AtomicU64::new(0);
fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
fn harness_dsn() -> Option<String> {
let host = env_nonempty("ALKBLOBS_PG_HOST")?;
Some(format!(
"host={host} port={} user={} password={} dbname={}",
env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()),
env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()),
env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()),
env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()),
))
}
fn skip_note() {
eprintln!(
"skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB"
);
}
struct TestDatabase {
dsn: String,
_janitor: DatabaseJanitor,
}
fn admin_dsn(dsn: &str) -> String {
let base = dsn.rsplit_once("dbname=").expect("dbname").0;
format!("{base} dbname=postgres")
}
fn db_dsn(dsn: &str, db: &str) -> String {
let base = dsn.rsplit_once("dbname=").expect("dbname").0;
format!("{base} dbname={db}")
}
async fn fresh_db(tag: &str) -> Option<TestDatabase> {
let dsn = harness_dsn()?;
let db = format!(
"abtmpglo_{}_{}_{:x}",
tag,
std::process::id(),
DB_SEQ.fetch_add(1, Ordering::Relaxed)
);
let admin = admin_dsn(&dsn);
{
let (client, conn) = tokio_postgres::connect(&admin, NoTls)
.await
.expect("admin connect");
tokio::spawn(async move {
let _ = conn.await;
});
client
.execute(
&format!(
"CREATE DATABASE {}",
crate::pg_common::quote_identifier(&db)
),
&[],
)
.await
.expect("create test database");
}
Some(TestDatabase {
dsn: db_dsn(&dsn, &db),
_janitor: DatabaseJanitor::new(&admin, &db),
})
}
fn fresh_schema(tag: &str, seq: u64) -> String {
format!("abtmpglo_{tag}_{}_{seq:x}", std::process::id())
}
fn pglo_engine(store: &Store) -> &crate::pg_lo::PgLoBackend {
match store.large() {
Some(LargeEngine::PgLo(engine)) => engine,
Some(LargeEngine::Local(_)) | None => {
panic!("these tests construct pg-lo-tier stores")
}
}
}
fn postgres_engine(store: &Store) -> &PostgresBackend {
match store.kv() {
#[cfg(feature = "postgres")]
KvEngine::Postgres(engine) => engine,
#[cfg(feature = "mem")]
KvEngine::Mem(_) => panic!("these tests construct postgres stores"),
#[cfg(feature = "sqlite")]
KvEngine::Sqlite(_) => panic!("these tests construct postgres stores"),
}
}
fn key_for(content: &[u8]) -> Key {
Key::derive(content, content.len() as u64)
}
#[tokio::test]
async fn dual_tier_postgres_pglo_constructs_with_the_kv_host_winning_the_window() {
let Some(site) = fresh_db("consolidation").await else {
skip_note();
return;
};
let seq = DB_SEQ.fetch_add(1, Ordering::Relaxed);
let kv_schema = fresh_schema("kv", seq);
let lo_schema = fresh_schema("lo", seq);
let store = Store::dual_tier_postgres_pglo(
&site.dsn,
PgOpts {
schema: kv_schema,
..Default::default()
},
&site.dsn,
PgOpts {
schema: lo_schema,
..Default::default()
},
1024,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("consolidation constructs");
assert_eq!(store.mode(), DualTier);
assert_eq!(store.threshold(), Some(1024));
assert_eq!(store.window_delay(), DEFAULT_DELETE_WINDOW_DELAY);
assert!(store.large().is_some(), "both tiers present");
for (len, tier) in [
(0u64, Tier::Kv),
(1023, Tier::Kv),
(1024, Tier::Kv),
(1025, Tier::Large),
(u64::MAX, Tier::Large),
] {
assert_eq!(
store.tier_for(len),
tier,
"length {len} routes deterministically"
);
}
let kv = postgres_engine(&store);
assert!(
kv.table_exists("blobs").await.unwrap(),
"the pool table exists at construction"
);
assert!(
kv.table_exists("gc_window").await.unwrap(),
"the kv host's call ran at construction: gc_window in the kv schema"
);
assert!(
kv.table_exists("gc_pins").await.unwrap(),
"the fleet host's both-tables row: gc_pins in the kv schema"
);
let lo = pglo_engine(&store);
assert!(
lo.table_exists("lo_entries").await.unwrap(),
"the pg-lo engine's companion table bootstrapped at construction"
);
assert!(
!lo.table_exists("gc_window").await.unwrap(),
"the pg-lo window clauses are dormant on the dual-SQL instance \
(ADR-012 §5): the pg-lo schema carries no gc_window"
);
assert!(
!lo.table_exists("gc_pins").await.unwrap(),
"gc_pins is never created by pg-lo (fleet pins live on the kv host)"
);
}
#[tokio::test]
async fn consolidation_shape_round_trips_end_to_end_through_both_tiers() {
let Some(site) = fresh_db("consolidation-rt").await else {
skip_note();
return;
};
let opts = PgOpts::default();
let store = Store::dual_tier_postgres_pglo(
&site.dsn,
opts.clone(),
&site.dsn,
opts,
64,
Duration::ZERO,
)
.await
.expect("consolidation constructs");
let small = b"a sub-threshold blob";
let (key, pin) = store
.put(Some(small.len() as u64), Cursor::new(&small[..]))
.await
.expect("kv put publishes");
assert_eq!(key, key_for(small));
assert!(
store.kv().has(&key.as_bytes()).await.unwrap(),
"landed on the kv tier (postgres)"
);
let lo = pglo_engine(&store);
assert!(
!lo.has(&key.as_bytes()).await.unwrap(),
"the sub-threshold key never entered the pg-lo companion table"
);
drop(pin);
let big = vec![42u8; 200];
let (key, pin) = store
.put(Some(200), Cursor::new(&big[..]))
.await
.expect("staged put publishes");
assert_eq!(key, key_for(&big));
assert!(
lo.has(&key.as_bytes()).await.unwrap(),
"landed on the pg-lo tier: the companion row is present"
);
assert_eq!(
lo.size(&key.as_bytes()).await.unwrap(),
Some(200),
"the companion row's size is exact"
);
assert!(
!store.kv().has(&key.as_bytes()).await.unwrap(),
"no kv copy on the large route"
);
let (len, blob) = store
.get(&key.as_bytes())
.await
.unwrap()
.expect("entry present");
assert_eq!(len, 200);
assert_eq!(
blob.read_at(0, len).await.unwrap(),
big,
"the LO's content round-trips byte-identical (LO placement)"
);
drop(pin);
let mut expected: Vec<Key> = [key_for(small), key_for(&big)].to_vec();
expected.sort();
assert_eq!(
store.list().await.expect("list succeeds"),
expected,
"the pool list is the sorted union of both tiers"
);
}
#[tokio::test]
async fn window_delay_default_is_pinned_and_constructor_tunable() {
assert_eq!(
DEFAULT_DELETE_WINDOW_DELAY,
Duration::from_secs(5 * 60),
"the shipped default is 5 minutes (ADR-008 §2)"
);
let Some(site) = fresh_db("consolidation-delay").await else {
skip_note();
return;
};
let store = Store::dual_tier_postgres_pglo(
&site.dsn,
PgOpts::default(),
&site.dsn,
PgOpts::default(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
)
.await
.expect("constructs at the shipped default");
assert_eq!(store.window_delay(), Duration::from_secs(300));
let store = Store::dual_tier_postgres_pglo(
&site.dsn,
PgOpts::default(),
&site.dsn,
PgOpts::default(),
64,
Duration::from_secs(90),
)
.await
.expect("constructs at a custom tuning");
assert_eq!(store.window_delay(), Duration::from_secs(90));
let store = Store::dual_tier_postgres_pglo(
&site.dsn,
PgOpts::default(),
&site.dsn,
PgOpts::default(),
64,
Duration::ZERO,
)
.await
.expect("the degenerate zero tuning is legal");
assert_eq!(store.window_delay(), Duration::ZERO);
Store::dual_tier_postgres_pglo(
&site.dsn,
PgOpts::default(),
&site.dsn,
PgOpts::default(),
64,
Duration::from_secs(6000),
)
.await
.expect("a long delay is a legal tuning too");
}
}
#[cfg(test)]
mod fleet_validity_predicate_tests {
use super::*;
#[test]
fn every_fleet_valid_shape_admits() {
assert_eq!(
fleet_validity_error(FleetKvShape::Postgres, FleetLargeShape::None),
None,
"the re-routing posture (large = none) is fleet-valid with kv = postgres"
);
assert_eq!(
fleet_validity_error(FleetKvShape::Postgres, FleetLargeShape::LocalSharedMedia),
None,
"the declared shared-media arm is fleet-valid"
);
assert_eq!(
fleet_validity_error(FleetKvShape::Postgres, FleetLargeShape::PgLo),
None,
"the pg-lo arm is fleet-valid (its same-instance clause probes at the constructor)"
);
}
#[test]
fn mem_anywhere_rejects() {
for large in [
FleetLargeShape::None,
FleetLargeShape::LocalSharedMedia,
FleetLargeShape::LocalUndeclared,
FleetLargeShape::PgLo,
] {
let err = fleet_validity_error(FleetKvShape::Mem, large)
.expect("mem is never fleet-valid, any large shape");
assert!(
err.to_string().contains("Mem is never fleet-valid"),
"mem is the kv engine, large = {large:?}: {err}"
);
}
}
#[test]
fn sqlite_rejects() {
for large in [
FleetLargeShape::None,
FleetLargeShape::LocalSharedMedia,
FleetLargeShape::LocalUndeclared,
FleetLargeShape::PgLo,
] {
assert!(
fleet_validity_error(FleetKvShape::Sqlite, large).is_some(),
"the sqlite kv engine, large = {large:?}, rejects"
);
}
}
#[test]
fn local_without_the_shared_media_declaration_rejects() {
let err = fleet_validity_error(FleetKvShape::Postgres, FleetLargeShape::LocalUndeclared)
.expect("the undeclared arm rejects");
assert!(
err.to_string().contains("shared-media declaration"),
"the error names the missing declaration: {err}"
);
}
}
#[cfg(all(test, feature = "postgres"))]
mod postgres_fleet_construction_tests {
use super::*;
use crate::postgres::test_support::SchemaJanitor;
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
struct TempRoot(PathBuf);
fn temp_root(tag: &'static str) -> TempRoot {
static SEQ: AtomicU64 = AtomicU64::new(0);
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"alkblobs-store-fleet-{tag}-{}-{:x}-{seq}",
std::process::id(),
seq,
));
fs::create_dir_all(&dir).expect("temp root created");
TempRoot(dir)
}
impl TempRoot {
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for TempRoot {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.0);
}
}
static SCHEMA_SEQ: AtomicU64 = AtomicU64::new(0);
fn fresh_schema(tag: &str) -> String {
format!(
"abtfleet_{}_{}_{:x}",
tag,
std::process::id(),
SCHEMA_SEQ.fetch_add(1, Ordering::Relaxed)
)
}
fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
fn harness_dsn() -> Option<String> {
let host = env_nonempty("ALKBLOBS_PG_HOST")?;
Some(format!(
"host={host} port={} user={} password={} dbname={}",
env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()),
env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()),
env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()),
env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()),
))
}
fn skip_note() {
eprintln!(
"skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB"
);
}
fn fresh_opts(tag: &str) -> Option<(PgOpts, SchemaJanitor)> {
let dsn = harness_dsn()?;
let schema = fresh_schema(tag);
Some((
PgOpts {
schema: schema.clone(),
..Default::default()
},
SchemaJanitor::new(&dsn, &schema),
))
}
fn key_for(content: &[u8]) -> Key {
Key::derive(content, content.len() as u64)
}
fn postgres_engine(store: &Store) -> &PostgresBackend {
match store.kv() {
KvEngine::Postgres(engine) => engine,
#[cfg(feature = "mem")]
KvEngine::Mem(_) => panic!("these tests construct postgres stores"),
#[cfg(feature = "sqlite")]
KvEngine::Sqlite(_) => panic!("these tests construct postgres stores"),
}
}
async fn assert_gc_schema_ran(store: &Store) {
let engine = postgres_engine(store);
assert!(
engine.table_exists("blobs").await.unwrap(),
"the pool table exists at construction"
);
assert!(
engine.table_exists("gc_window").await.unwrap(),
"ensure_gc_schema ran at construction"
);
assert!(
engine.table_exists("gc_pins").await.unwrap(),
"the fleet host's both-tables row: gc_pins exists"
);
}
#[test]
fn default_pin_ttl_is_the_pinned_sixty_seconds() {
assert_eq!(
DEFAULT_PIN_TTL,
Duration::from_secs(crate::postgres::DEFAULT_PIN_TTL_SECS),
"the store's shipped default is the engine constant's seconds"
);
assert_eq!(
DEFAULT_PIN_TTL,
Duration::from_secs(60),
"the shipped default is 60 s (ADR-008 §2)"
);
}
#[test]
fn owner_id_mints_unique_and_fixed_width() {
let first = mint_owner_id();
let second = mint_owner_id();
assert_eq!(first.len(), 32, "the SHA-256 fold's width");
assert_eq!(second.len(), 32);
assert_ne!(
first, second,
"two instances in one process mint distinct ids"
);
}
#[tokio::test]
async fn zero_pin_ttl_is_a_constructor_error_at_entry() {
let err = Store::kv_only_postgres_fleet(
"host=no-such-host.invalid port=1 user=x password=x dbname=x",
PgOpts::default(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
Duration::ZERO,
)
.await
.err()
.expect("the entry validation rejects before any connection attempt");
assert!(
err.to_string().contains("pin_ttl must be positive"),
"the zero TTL rejects before any connection attempt: {err}"
);
let err = Store::dual_tier_postgres_local_fleet(
"host=no-such-host.invalid port=1 user=x password=x dbname=x",
PgOpts::default(),
std::env::temp_dir().join("alkblobs-fleet-zero-ttl"),
64,
DEFAULT_DELETE_WINDOW_DELAY,
Duration::ZERO,
FleetSharedMedia::asserted(),
)
.await
.err()
.expect("the entry validation rejects on the shared-media shape too");
assert!(
err.to_string().contains("pin_ttl must be positive"),
"the zero TTL rejects at entry on the shared-media shape too: {err}"
);
}
#[tokio::test]
async fn kv_only_postgres_fleet_constructs_and_introspects_fleet_state() {
let Some((opts, _janitor)) = fresh_opts("fleet-kv-only") else {
skip_note();
return;
};
let dsn = harness_dsn().expect("harness dsn re-read");
let store = Store::kv_only_postgres_fleet(
&dsn,
opts,
64,
DEFAULT_DELETE_WINDOW_DELAY,
DEFAULT_PIN_TTL,
)
.await
.expect("the fleet kv-only posture constructs");
assert_eq!(store.mode(), KvOnly);
assert!(store.large().is_none(), "no large tier (large = none)");
let fleet = store
.fleet()
.expect("fleet: true declares: the state carries");
assert_eq!(fleet.pin_ttl(), DEFAULT_PIN_TTL);
assert_eq!(fleet.owner().len(), 32);
assert_gc_schema_ran(&store).await;
for (len, tier) in [
(0u64, Tier::Kv),
(63, Tier::Kv),
(64, Tier::Kv),
(65, Tier::Kv),
(u64::MAX, Tier::Kv),
] {
assert_eq!(
store.tier_for(len),
tier,
"kv-only routes every length kv, fleet or not"
);
}
let err = store
.put(Some(65), Cursor::new(&b"never read"[..]))
.await
.unwrap_err();
assert!(
err.to_string().contains("kv-only mode rejects"),
"the put-time rejection is unchanged under the declaration: {err}"
);
assert_eq!(
store.list().await.unwrap(),
Vec::<Key>::new(),
"zero residue"
);
drop(store);
let Some((opts, _janitor)) = fresh_opts("fleet-kv-ttl") else {
skip_note();
return;
};
let store = Store::kv_only_postgres_fleet(
&dsn,
opts,
64,
DEFAULT_DELETE_WINDOW_DELAY,
Duration::from_secs(90),
)
.await
.expect("constructs at a custom pin TTL");
let fleet = store.fleet().expect("fleet state carries");
assert_eq!(
fleet.pin_ttl(),
Duration::from_secs(90),
"the TTL is a fleet constructor parameter"
);
let same_instance_owner = fleet.owner().to_vec();
drop(store);
let Some((opts, _janitor)) = fresh_opts("fleet-kv-owner2") else {
skip_note();
return;
};
let other = Store::kv_only_postgres_fleet(
&dsn,
opts,
64,
DEFAULT_DELETE_WINDOW_DELAY,
DEFAULT_PIN_TTL,
)
.await
.expect("a second instance constructs");
assert_ne!(
other.fleet().expect("fleet state carries").owner(),
same_instance_owner,
"two instances never share the store-instance id"
);
}
#[tokio::test]
async fn fleet_declaration_runs_the_sweep_machinery_on_the_kv_host() {
struct SetSource;
impl LivenessSource for SetSource {
fn live_keys(&self) -> Vec<Key> {
Vec::new()
}
fn contains(&self, _key: &Key) -> bool {
false
}
}
let Some((opts, _janitor)) = fresh_opts("fleet-inert") else {
skip_note();
return;
};
let dsn = harness_dsn().expect("harness dsn re-read");
let store =
Store::kv_only_postgres_fleet(&dsn, opts, 64, Duration::ZERO, Duration::from_secs(2))
.await
.expect("constructs");
let _ =
store.register_liveness_source(LivenessRegistration::InProcess(Arc::new(SetSource)));
let content = b"a dead candidate on a declared fleet store";
let (key, pin) = store
.put(Some(content.len() as u64), Cursor::new(&content[..]))
.await
.expect("the fleet put publishes through the joint tx");
assert_eq!(key, key_for(content));
assert!(store.has(&key.as_bytes()).await.unwrap());
drop(pin);
let staged = store.sweep().await.expect("sweeps are never errors");
assert_eq!(staged.aborted, None);
assert_eq!(
staged.candidates_staged, 1,
"the fleet body staged on the kv host (the one-window-host rule's fleet reading)"
);
assert_eq!(staged.deleted, 0);
let executed = store.sweep().await.expect("sweeps are never errors");
assert_eq!(
(executed.deleted, executed.cancelled_by_arbitration),
(0, 1),
"the instance's own un-expired pin row protects at the execution-time SQL \
re-arbitration (guard dropped, the row lives its TTL)"
);
assert!(store.has(&key.as_bytes()).await.unwrap());
tokio::time::sleep(Duration::from_secs(3)).await;
let reaped = store.sweep().await.expect("sweeps are never errors");
assert_eq!(
reaped.deleted, 1,
"the expired row reaped by the maintenance step, the re-arbitration passes"
);
assert!(!store.has(&key.as_bytes()).await.unwrap());
}
#[tokio::test]
async fn dual_tier_postgres_local_fleet_constructs_and_introspects() {
let Some((opts, _janitor)) = fresh_opts("fleet-shared-media") else {
skip_note();
return;
};
let dsn = harness_dsn().expect("harness dsn re-read");
let root = temp_root("fleet-shared-media");
let store = Store::dual_tier_postgres_local_fleet(
&dsn,
opts,
root.path(),
1024,
DEFAULT_DELETE_WINDOW_DELAY,
DEFAULT_PIN_TTL,
FleetSharedMedia::asserted(),
)
.await
.expect("the fleet shared-media posture constructs");
assert_eq!(store.mode(), DualTier);
assert!(store.large().is_some(), "both tiers present");
let fleet = store
.fleet()
.expect("fleet: true declares: the state carries");
assert_eq!(fleet.pin_ttl(), DEFAULT_PIN_TTL);
assert_eq!(fleet.owner().len(), 32);
assert_gc_schema_ran(&store).await;
for (len, tier) in [
(0u64, Tier::Kv),
(1023, Tier::Kv),
(1024, Tier::Kv),
(1025, Tier::Large),
(u64::MAX, Tier::Large),
] {
assert_eq!(
store.tier_for(len),
tier,
"the declaration does not disturb the dispatch"
);
}
}
}
#[cfg(all(test, feature = "pg-lo", feature = "postgres"))]
mod postgres_pglo_fleet_construction_tests {
use super::*;
use crate::pg_common::test_support::DatabaseJanitor;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio_postgres::NoTls;
static DB_SEQ: AtomicU64 = AtomicU64::new(0);
fn env_nonempty(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
fn harness_dsn() -> Option<String> {
let host = env_nonempty("ALKBLOBS_PG_HOST")?;
Some(format!(
"host={host} port={} user={} password={} dbname={}",
env_nonempty("ALKBLOBS_PG_PORT").unwrap_or_else(|| "15432".to_owned()),
env_nonempty("ALKBLOBS_PG_USER").unwrap_or_else(|| "postgres".to_owned()),
env_nonempty("ALKBLOBS_PG_PASSWORD").unwrap_or_else(|| "poc".to_owned()),
env_nonempty("ALKBLOBS_PG_DB").unwrap_or_else(|| "blobs".to_owned()),
))
}
fn skip_note() {
eprintln!(
"skipping (no postgres harness server): set ALKBLOBS_PG_HOST/PORT/USER/PASSWORD/DB"
);
}
struct TestDatabase {
dsn: String,
_janitor: DatabaseJanitor,
}
fn admin_dsn(dsn: &str) -> String {
let base = dsn.rsplit_once("dbname=").expect("dbname").0;
format!("{base} dbname=postgres")
}
fn db_dsn(dsn: &str, db: &str) -> String {
let base = dsn.rsplit_once("dbname=").expect("dbname").0;
format!("{base} dbname={db}")
}
async fn fresh_db(tag: &str) -> Option<TestDatabase> {
let dsn = harness_dsn()?;
let db = format!(
"abtfleetlo_{}_{}_{:x}",
tag,
std::process::id(),
DB_SEQ.fetch_add(1, Ordering::Relaxed)
);
let admin = admin_dsn(&dsn);
{
let (client, conn) = tokio_postgres::connect(&admin, NoTls)
.await
.expect("admin connect");
tokio::spawn(async move {
let _ = conn.await;
});
client
.execute(
&format!(
"CREATE DATABASE {}",
crate::pg_common::quote_identifier(&db)
),
&[],
)
.await
.expect("create test database");
}
Some(TestDatabase {
dsn: db_dsn(&dsn, &db),
_janitor: DatabaseJanitor::new(&admin, &db),
})
}
fn fresh_schema(tag: &str) -> String {
format!(
"abtfleetlo_{}_{}_{:x}",
tag,
std::process::id(),
DB_SEQ.fetch_add(1, Ordering::Relaxed)
)
}
fn pglo_engine(store: &Store) -> &crate::pg_lo::PgLoBackend {
match store.large() {
Some(LargeEngine::PgLo(engine)) => engine,
Some(LargeEngine::Local(_)) | None => panic!("these tests construct pg-lo-tier stores"),
}
}
fn postgres_engine(store: &Store) -> &PostgresBackend {
match store.kv() {
KvEngine::Postgres(engine) => engine,
#[cfg(feature = "mem")]
KvEngine::Mem(_) => panic!("these tests construct postgres stores"),
#[cfg(feature = "sqlite")]
KvEngine::Sqlite(_) => panic!("these tests construct postgres stores"),
}
}
#[tokio::test]
async fn dual_tier_postgres_pglo_fleet_constructs_same_instance() {
let Some(site) = fresh_db("fleet-consolidation").await else {
skip_note();
return;
};
let kv_schema = fresh_schema("kv");
let lo_schema = fresh_schema("lo");
let store = Store::dual_tier_postgres_pglo_fleet(
&site.dsn,
PgOpts {
schema: kv_schema.clone(),
..Default::default()
},
&site.dsn,
PgOpts {
schema: lo_schema.clone(),
..Default::default()
},
1024,
DEFAULT_DELETE_WINDOW_DELAY,
DEFAULT_PIN_TTL,
)
.await
.expect("the same-instance consolidation posture constructs");
assert_eq!(store.mode(), DualTier);
assert!(store.large().is_some());
let fleet = store
.fleet()
.expect("fleet: true declares: the state carries");
assert_eq!(fleet.pin_ttl(), DEFAULT_PIN_TTL);
assert_eq!(fleet.owner().len(), 32);
for (len, tier) in [
(0u64, Tier::Kv),
(1023, Tier::Kv),
(1024, Tier::Kv),
(1025, Tier::Large),
(u64::MAX, Tier::Large),
] {
assert_eq!(
store.tier_for(len),
tier,
"the declaration does not disturb the dispatch"
);
}
let kv = postgres_engine(&store);
assert!(kv.table_exists("blobs").await.unwrap());
assert!(
kv.table_exists("gc_window").await.unwrap(),
"the window hosts on the kv engine (the one-window-host rule under fleet)"
);
assert!(
kv.table_exists("gc_pins").await.unwrap(),
"the fleet host's both-tables row: gc_pins in the kv schema"
);
let lo = pglo_engine(&store);
assert!(lo.table_exists("lo_entries").await.unwrap());
assert!(
!lo.table_exists("gc_window").await.unwrap(),
"the pg-lo window clauses stay dormant on the dual-SQL instance"
);
assert!(
!lo.table_exists("gc_pins").await.unwrap(),
"gc_pins is never created by pg-lo (fleet pins live on the kv host)"
);
drop(store);
let _ = (kv_schema, lo_schema);
}
#[tokio::test]
async fn dual_tier_postgres_pglo_fleet_two_databases_one_instance() {
let Some(kv_site) = fresh_db("fleet-two-db-kv").await else {
skip_note();
return;
};
let Some(lo_site) = fresh_db("fleet-two-db-lo").await else {
skip_note();
return;
};
let store = Store::dual_tier_postgres_pglo_fleet(
&kv_site.dsn,
PgOpts::default(),
&lo_site.dsn,
PgOpts::default(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
DEFAULT_PIN_TTL,
)
.await
.expect("two databases on one instance are one fleet-valid instance");
let fleet = store
.fleet()
.expect("fleet: true declares: the state carries");
assert_eq!(fleet.pin_ttl(), DEFAULT_PIN_TTL);
}
#[tokio::test]
async fn dual_tier_postgres_pglo_fleet_unresolvable_instance_errors_closed() {
let Some(kv_site) = fresh_db("fleet-closed-kv").await else {
skip_note();
return;
};
let nonexistent = db_dsn(&kv_site.dsn, "abtnosuchdb_fleet_negative");
let err = Store::dual_tier_postgres_pglo_fleet(
&kv_site.dsn,
PgOpts::default(),
&nonexistent,
PgOpts::default(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
DEFAULT_PIN_TTL,
)
.await
.err()
.expect("the cross-instance (unresolvable) negative errors closed");
assert!(
err.to_string().contains("postgres"),
"the rejection is a pg-side failure surfaced as the constructor error: {err}"
);
assert!(
!matches!(
err,
Error::Missing | Error::Verification | Error::KeyInvalid | Error::GcRefuse
),
"the failure never masquerades as a non-Io member"
);
}
#[tokio::test]
async fn zero_pin_ttl_is_a_constructor_error_at_entry() {
let err = Store::dual_tier_postgres_pglo_fleet(
"host=no-such-host.invalid port=1 user=x password=x dbname=x",
PgOpts::default(),
"host=no-such-host.invalid port=1 user=x password=x dbname=x",
PgOpts::default(),
64,
DEFAULT_DELETE_WINDOW_DELAY,
Duration::ZERO,
)
.await
.err()
.expect("the entry validation rejects before any connection attempt");
assert!(
err.to_string().contains("pin_ttl must be positive"),
"the zero TTL rejects at entry on the consolidation shape too: {err}"
);
}
}