use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use liminal::durability::ClaimEvidence;
use liminal::conversation::ConversationSupervisor;
use liminal::durability::bridge::block_on;
use liminal::durability::{
DedupCache, DurabilityError, DurableStore, ProcessingReceipt, ScanCursor, ScanPage,
StoredEntry, open_ephemeral_rooted,
};
use super::{
ChannelCluster, DELIVERY_DEDUP_NAMESPACE, ProductionSubsystems, StoreHistory, SubsystemFactory,
THROWAWAY_SHARD_COUNT, ThrowawayStoreOpener, build_connection_services_via,
build_durable_store,
};
use crate::ServerError;
use crate::config::types::{LimitsConfig, ServerConfig, ServicesConfig};
use crate::test_fixture::fixture_copy;
type TestResult = Result<(), Box<dyn std::error::Error>>;
#[derive(Debug, Default)]
struct WorkCounters {
stores_opened: AtomicUsize,
writes: AtomicUsize,
flushes: AtomicUsize,
reads: AtomicUsize,
rows_read: AtomicUsize,
scans: AtomicUsize,
dedup_rows: AtomicUsize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct StartWork {
stores_opened: usize,
shards_created: usize,
writes: usize,
flushes: usize,
reads: usize,
rows_read: usize,
scans: usize,
}
impl StartWork {
const fn since(self, before: Self) -> Self {
Self {
stores_opened: self.stores_opened - before.stores_opened,
shards_created: self.shards_created - before.shards_created,
writes: self.writes - before.writes,
flushes: self.flushes - before.flushes,
reads: self.reads - before.reads,
rows_read: self.rows_read - before.rows_read,
scans: self.scans - before.scans,
}
}
fn assert_within(self, ceiling: Self, name: &str) {
let over: Vec<String> = [
("stores_opened", self.stores_opened, ceiling.stores_opened),
(
"shards_created",
self.shards_created,
ceiling.shards_created,
),
("writes", self.writes, ceiling.writes),
("flushes", self.flushes, ceiling.flushes),
("reads", self.reads, ceiling.reads),
("rows_read", self.rows_read, ceiling.rows_read),
("scans", self.scans, ceiling.scans),
]
.into_iter()
.filter(|(_, counted, allowed)| counted > allowed)
.map(|(field, counted, allowed)| format!("{field} {counted} > {allowed}"))
.collect();
assert!(
over.is_empty(),
"a start rose above {name}: {}; counted {self:?}",
over.join(", ")
);
}
}
const MISSING_PATH_START: StartWork = StartWork {
stores_opened: 0,
shards_created: 0,
writes: 0,
flushes: 0,
reads: 0,
rows_read: 0,
scans: 0,
};
const CREATED_PERSISTENT_START: StartWork = StartWork {
stores_opened: 1,
shards_created: 0,
writes: 0,
flushes: 0,
reads: 0,
rows_read: 0,
scans: 0,
};
const SETTLED_PERSISTENT_START: StartWork = StartWork {
stores_opened: 1,
shards_created: 0,
writes: 0,
flushes: 0,
reads: 46,
rows_read: 133,
scans: 0,
};
const SETTLED_EVIDENCED_CLAIMS: usize = 300;
#[derive(Debug)]
struct ScanCountingStore {
inner: Arc<dyn DurableStore>,
counters: Arc<WorkCounters>,
}
impl ScanCountingStore {
fn count_read(&self, stream_key: &str, rows: usize) {
self.counters.reads.fetch_add(1, Ordering::SeqCst);
self.counters.rows_read.fetch_add(rows, Ordering::SeqCst);
if stream_key.starts_with(&format!("{DELIVERY_DEDUP_NAMESPACE}:")) {
self.counters.dedup_rows.fetch_add(rows, Ordering::SeqCst);
}
}
fn count_write(&self) {
self.counters.writes.fetch_add(1, Ordering::SeqCst);
}
}
#[async_trait::async_trait]
impl DurableStore for ScanCountingStore {
async fn append(
&self,
stream_key: &str,
payload: Vec<u8>,
expected_seq: u64,
) -> Result<u64, DurabilityError> {
self.count_write();
self.inner.append(stream_key, payload, expected_seq).await
}
async fn append_all(
&self,
stream_key: &str,
payloads: Vec<Vec<u8>>,
expected_seq: u64,
) -> Result<u64, DurabilityError> {
self.count_write();
self.inner
.append_all(stream_key, payloads, expected_seq)
.await
}
async fn read_from(
&self,
stream_key: &str,
offset: u64,
limit: usize,
) -> Result<Vec<StoredEntry>, DurabilityError> {
let rows = self.inner.read_from(stream_key, offset, limit).await?;
self.count_read(stream_key, rows.len());
Ok(rows)
}
async fn read_at(
&self,
stream_key: &str,
sequence: u64,
) -> Result<Option<StoredEntry>, DurabilityError> {
let row = self.inner.read_at(stream_key, sequence).await?;
self.count_read(stream_key, usize::from(row.is_some()));
Ok(row)
}
async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError> {
self.count_write();
self.inner.cas(key, old_value, new_value).await
}
async fn read_stream_next_seq(&self, stream_key: &str) -> Result<Option<u64>, DurabilityError> {
self.inner.read_stream_next_seq(stream_key).await
}
async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
let value = self.inner.read_value(key).await?;
self.count_read(key, usize::from(value.is_some()));
Ok(value)
}
async fn scan_page(
&self,
prefix: &str,
cursor: Option<ScanCursor>,
limit: usize,
) -> Result<ScanPage, DurabilityError> {
self.counters.scans.fetch_add(1, Ordering::SeqCst);
let page = self.inner.scan_page(prefix, cursor, limit).await?;
self.count_read(prefix, page.entries.len());
Ok(page)
}
async fn flush(&self) -> Result<(), DurabilityError> {
self.counters.flushes.fetch_add(1, Ordering::SeqCst);
self.inner.flush().await
}
}
struct ScanCountingSubsystems {
ephemeral_root: tempfile::TempDir,
counters: Arc<WorkCounters>,
}
impl ScanCountingSubsystems {
fn new() -> Result<Self, std::io::Error> {
Ok(Self {
ephemeral_root: tempfile::tempdir()?,
counters: Arc::new(WorkCounters::default()),
})
}
fn scans(&self) -> usize {
self.counters.scans.load(Ordering::SeqCst)
}
fn dedup_rows(&self) -> usize {
self.counters.dedup_rows.load(Ordering::SeqCst)
}
fn work(&self, persistence_path: Option<&Path>) -> Result<StartWork, std::io::Error> {
let mut shards = shard_directories(self.ephemeral_root.path(), 2)?;
if let Some(path) = persistence_path {
shards += shard_directories(path, 2)?;
}
Ok(StartWork {
stores_opened: self.counters.stores_opened.load(Ordering::SeqCst),
shards_created: shards,
writes: self.counters.writes.load(Ordering::SeqCst),
flushes: self.counters.flushes.load(Ordering::SeqCst),
reads: self.counters.reads.load(Ordering::SeqCst),
rows_read: self.counters.rows_read.load(Ordering::SeqCst),
scans: self.scans(),
})
}
}
fn shard_directories(root: &Path, depth: usize) -> Result<usize, std::io::Error> {
if !root.is_dir() {
return Ok(0);
}
let mut found = 0;
for entry in std::fs::read_dir(root)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
if entry.file_name().to_string_lossy().starts_with("shard-") {
found += 1;
} else if depth > 1 {
found += shard_directories(&entry.path(), depth - 1)?;
}
}
Ok(found)
}
impl SubsystemFactory for ScanCountingSubsystems {
fn channel_cluster(
&self,
cluster_config: Option<&crate::config::types::ClusterConfig>,
) -> Result<ChannelCluster, ServerError> {
ProductionSubsystems.channel_cluster(cluster_config)
}
fn conversation_supervisor(&self) -> Result<Arc<ConversationSupervisor>, ServerError> {
ProductionSubsystems.conversation_supervisor()
}
fn durable_store(&self, persistence_path: &Path) -> Result<Arc<dyn DurableStore>, ServerError> {
let inner = build_durable_store(persistence_path)?;
self.counters.stores_opened.fetch_add(1, Ordering::SeqCst);
Ok(Arc::new(ScanCountingStore {
inner,
counters: Arc::clone(&self.counters),
}))
}
fn throwaway_store(&self) -> ThrowawayStoreOpener {
let root = self.ephemeral_root.path().to_path_buf();
let counters = Arc::clone(&self.counters);
Box::new(move || {
counters.stores_opened.fetch_add(1, Ordering::SeqCst);
open_ephemeral_rooted(&root, THROWAWAY_SHARD_COUNT)
})
}
}
fn config(persistence_path: Option<&Path>) -> Result<ServerConfig, Box<dyn std::error::Error>> {
Ok(ServerConfig {
listen_address: "127.0.0.1:0".parse()?,
health_listen_address: "127.0.0.1:0".parse()?,
drain_timeout_ms: 30_000,
channels: Vec::new(),
routing_rules: Vec::new(),
persistence_path: persistence_path.map(Path::to_path_buf),
cluster: None,
auth: None,
services: ServicesConfig::default(),
limits: LimitsConfig::default(),
participant: None,
websocket: None,
})
}
#[test]
fn a_missing_path_refuses_without_store_work() -> TestResult {
let subsystems = ScanCountingSubsystems::new()?;
let refused = build_connection_services_via(&config(None)?, &subsystems);
assert!(
matches!(refused, Err(ServerError::ConfigValidation { message }) if message.contains("no store is opened"))
);
subsystems
.work(None)?
.assert_within(MISSING_PATH_START, "MISSING_PATH_START");
Ok(())
}
#[test]
fn a_start_that_creates_its_persistent_store_stays_within_its_ceiling() -> TestResult {
let home = tempfile::tempdir()?;
let subsystems = ScanCountingSubsystems::new()?;
let services = build_connection_services_via(&config(Some(home.path()))?, &subsystems)?;
subsystems
.work(Some(home.path()))?
.assert_within(CREATED_PERSISTENT_START, "CREATED_PERSISTENT_START");
drop(services);
Ok(())
}
#[test]
fn a_restart_over_a_store_nothing_was_written_to_stays_within_its_ceiling() -> TestResult {
let home = tempfile::tempdir()?;
let subsystems = ScanCountingSubsystems::new()?;
drop(build_connection_services_via(
&config(Some(home.path()))?,
&subsystems,
)?);
let before = subsystems.work(Some(home.path()))?;
let services = build_connection_services_via(&config(Some(home.path()))?, &subsystems)?;
subsystems
.work(Some(home.path()))?
.since(before)
.assert_within(CREATED_PERSISTENT_START, "CREATED_PERSISTENT_START");
drop(services);
Ok(())
}
fn build_settled_evidenced_home(home: &Path) -> crate::test_fixture::FixtureResult<()> {
let subsystems = ScanCountingSubsystems::new()?;
drop(build_connection_services_via(
&config(Some(home))?,
&subsystems,
)?);
let store = subsystems.durable_store(home)?;
let dedup = StoreHistory::Created.delivery_dedup(Arc::clone(&store));
for index in 0..SETTLED_EVIDENCED_CLAIMS {
let key = format!("settled-{index}");
block_on(dedup.claim_with_evidence(&key, ClaimEvidence::new("orders", vec![0]), 1))??;
if index % 2 == 0 {
block_on(dedup.complete_receipt(&key, ProcessingReceipt::for_position(1)))??;
} else {
block_on(dedup.release_claim(&key))??;
}
}
Ok(())
}
#[test]
fn a_start_over_settled_claims_stays_within_its_ceiling() -> TestResult {
let home = fixture_copy("settled-evidenced-claims-300", build_settled_evidenced_home)?;
let subsystems = ScanCountingSubsystems::new()?;
let before = subsystems.work(Some(home.path()))?;
let services = build_connection_services_via(&config(Some(home.path()))?, &subsystems)?;
subsystems
.work(Some(home.path()))?
.since(before)
.assert_within(SETTLED_PERSISTENT_START, "SETTLED_PERSISTENT_START");
drop(services);
crate::test_fixture::pin_store_opens(
module_path!(),
"a_start_over_settled_claims_stays_within_its_ceiling",
)?;
Ok(())
}
#[test]
fn a_store_written_before_the_index_is_walked_once_at_start() -> TestResult {
let home = tempfile::tempdir()?;
let subsystems = ScanCountingSubsystems::new()?;
let store = subsystems.durable_store(home.path())?;
let legacy = DedupCache::new(Arc::clone(&store), DELIVERY_DEDUP_NAMESPACE);
block_on(legacy.claim_or_get("stranded", 1))??;
drop(legacy);
drop(store);
let scans = subsystems.scans();
let services = build_connection_services_via(&config(Some(home.path()))?, &subsystems)?;
assert!(
subsystems.scans() > scans,
"a store with claims and no index is walked to build it"
);
drop(services);
let scans = subsystems.scans();
let services = build_connection_services_via(&config(Some(home.path()))?, &subsystems)?;
assert_eq!(
subsystems.scans(),
scans,
"the walk is recorded and not made again"
);
drop(services);
Ok(())
}
const SETTLED_KEYS: usize = 600;
fn build_settled_keys_home(home: &Path) -> crate::test_fixture::FixtureResult<()> {
let subsystems = ScanCountingSubsystems::new()?;
drop(build_connection_services_via(
&config(Some(home))?,
&subsystems,
)?);
let store = subsystems.durable_store(home)?;
let dedup = StoreHistory::Created.delivery_dedup(Arc::clone(&store));
for index in 0..SETTLED_KEYS {
let key = format!("settled-{index}");
block_on(dedup.claim_or_get(&key, 1))??;
if index % 2 == 0 {
block_on(dedup.complete_receipt(&key, ProcessingReceipt::for_position(1)))??;
} else {
block_on(dedup.release_claim(&key))??;
}
}
Ok(())
}
#[test]
fn a_restart_reads_no_settled_claim_and_does_not_walk_the_namespace_again() -> TestResult {
let home = fixture_copy("settled-keys-600", build_settled_keys_home)?;
let subsystems = ScanCountingSubsystems::new()?;
let scans = subsystems.scans();
let rows = subsystems.dedup_rows();
let services = build_connection_services_via(&config(Some(home.path()))?, &subsystems)?;
assert_eq!(
subsystems.scans(),
scans,
"a start after the first reads the recorded in-flight claims, never the namespace"
);
assert_eq!(
subsystems.dedup_rows(),
rows,
"a start reads no dedup record of a key that is not in flight"
);
drop(services);
crate::test_fixture::pin_store_opens(
module_path!(),
"a_restart_reads_no_settled_claim_and_does_not_walk_the_namespace_again",
)?;
Ok(())
}