use haematite::{ApiError, Database, DatabaseConfig, Event, EventStore};
use std::path::Path;
use std::sync::Arc;
use super::DurabilityError;
use tempfile::TempDir;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StoredEntry {
pub payload: Vec<u8>,
pub sequence: u64,
pub timestamp: u64,
}
#[async_trait::async_trait]
pub trait DurableStore: std::fmt::Debug + Send + Sync {
async fn append(
&self,
stream_key: &str,
payload: Vec<u8>,
expected_seq: u64,
) -> Result<u64, DurabilityError>;
async fn read_from(
&self,
stream_key: &str,
offset: u64,
limit: usize,
) -> Result<Vec<StoredEntry>, DurabilityError>;
async fn read_at(
&self,
stream_key: &str,
sequence: u64,
) -> Result<Option<StoredEntry>, DurabilityError> {
Ok(self
.read_from(stream_key, sequence, 1)
.await?
.into_iter()
.next())
}
async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError>;
async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError>;
async fn scan(&self, prefix: &str) -> Result<Vec<StoredEntry>, DurabilityError>;
async fn flush(&self) -> Result<(), DurabilityError>;
}
#[derive(Clone, Debug)]
pub struct HaematiteStore {
event_store: Arc<EventStore>,
}
impl HaematiteStore {
#[must_use]
pub const fn new(event_store: Arc<EventStore>) -> Self {
Self { event_store }
}
fn bounded_page(
&self,
stream_key: &str,
offset: u64,
limit: usize,
) -> Result<Option<Vec<StoredEntry>>, DurabilityError> {
const TIMESTAMP_WIDTH: usize = std::mem::size_of::<u64>();
let Some(engine_from) = offset.checked_add(1) else {
return Ok(None);
};
let Some(engine_end) = u64::try_from(limit)
.ok()
.and_then(|limit| engine_from.checked_add(limit))
else {
return Ok(None);
};
let key = stream_key.as_bytes();
let from = haematite::encode_stream_key(key, engine_from);
let to = haematite::encode_stream_key(key, engine_end);
let entries = self
.event_store
.database()
.range_routed(key, &from, &to)
.map_err(ApiError::from)
.map_err(DurabilityError::from)?;
if entries.len() != limit {
return Ok(None);
}
let mut page = Vec::with_capacity(entries.len());
for (encoded_key, value) in entries {
let Some((decoded_key, engine_sequence)) = haematite::decode_stream_key(&encoded_key)
else {
return Err(DurabilityError::StoreError(ApiError::CorruptEvent(
format!("paged read key does not encode an event for stream {stream_key}"),
)));
};
if decoded_key != key {
return Err(DurabilityError::StoreError(ApiError::CorruptEvent(
format!("paged read key does not encode stream {stream_key}"),
)));
}
let sequence = engine_sequence.checked_sub(1).ok_or_else(|| {
DurabilityError::StoreError(ApiError::CorruptEvent(format!(
"paged read event key has zero seq for stream {stream_key}"
)))
})?;
let Some(timestamp_bytes) = value.get(..TIMESTAMP_WIDTH) else {
return Err(DurabilityError::StoreError(ApiError::CorruptEvent(
format!(
"paged read event value is shorter than its timestamp for stream {stream_key}"
),
)));
};
let timestamp = u64::from_be_bytes(timestamp_bytes.try_into().map_err(|_| {
DurabilityError::StoreError(ApiError::CorruptEvent(format!(
"paged read event timestamp has the wrong width for stream {stream_key}"
)))
})?);
let Some(payload) = value.get(TIMESTAMP_WIDTH..) else {
return Err(DurabilityError::StoreError(ApiError::CorruptEvent(
format!("paged read event has no payload boundary for stream {stream_key}"),
)));
};
page.push(StoredEntry {
payload: payload.to_vec(),
sequence,
timestamp,
});
}
Ok(Some(page))
}
}
#[async_trait::async_trait]
impl DurableStore for HaematiteStore {
async fn append(
&self,
stream_key: &str,
payload: Vec<u8>,
expected_seq: u64,
) -> Result<u64, DurabilityError> {
let next_seq = self
.event_store
.append(stream_key.as_bytes(), &payload, expected_seq)
.map_err(DurabilityError::from)?;
next_seq.checked_sub(1).ok_or_else(|| {
DurabilityError::StoreError(ApiError::CorruptEvent(format!(
"append returned next-seq 0 for stream {stream_key}"
)))
})
}
async fn read_from(
&self,
stream_key: &str,
offset: u64,
limit: usize,
) -> Result<Vec<StoredEntry>, DurabilityError> {
if limit > 0 {
if let Some(page) = self.bounded_page(stream_key, offset, limit)? {
account_engine_read(page.len(), false);
return Ok(page);
}
}
let mut events = self
.event_store
.read_from(stream_key.as_bytes(), offset)
.map_err(DurabilityError::from)?;
account_engine_read(events.len(), true);
events.truncate(limit);
Ok(events.into_iter().map(StoredEntry::from).collect())
}
async fn read_at(
&self,
stream_key: &str,
sequence: u64,
) -> Result<Option<StoredEntry>, DurabilityError> {
const TIMESTAMP_WIDTH: usize = std::mem::size_of::<u64>();
let engine_sequence = sequence.checked_add(1).ok_or_else(|| {
DurabilityError::StoreError(ApiError::CorruptEvent(format!(
"point read sequence overflow for stream {stream_key}"
)))
})?;
let event_key = haematite::encode_stream_key(stream_key.as_bytes(), engine_sequence);
let Some(value) = self
.event_store
.database()
.get_routed(stream_key.as_bytes(), &event_key)
.map_err(ApiError::from)
.map_err(DurabilityError::from)?
else {
return Ok(None);
};
let Some(timestamp_bytes) = value.get(..TIMESTAMP_WIDTH) else {
return Err(DurabilityError::StoreError(ApiError::CorruptEvent(
format!(
"point-read event value is shorter than its timestamp for stream {stream_key}"
),
)));
};
let timestamp = u64::from_be_bytes(timestamp_bytes.try_into().map_err(|_| {
DurabilityError::StoreError(ApiError::CorruptEvent(format!(
"point-read event timestamp has the wrong width for stream {stream_key}"
)))
})?);
let Some(payload) = value.get(TIMESTAMP_WIDTH..) else {
return Err(DurabilityError::StoreError(ApiError::CorruptEvent(
format!("point-read event has no payload boundary for stream {stream_key}"),
)));
};
Ok(Some(StoredEntry {
payload: payload.to_vec(),
sequence,
timestamp,
}))
}
async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError> {
if new_value == 0 {
return self
.event_store
.read_value(key.as_bytes())
.map_err(DurabilityError::from)?
.map_or(Ok(()), |stored| {
Err(DurabilityError::CursorRegression {
stored,
attempted: old_value,
})
});
}
let expected = if old_value == 0 {
None
} else {
Some(old_value)
};
self.event_store
.cas(key.as_bytes(), expected, new_value)
.map_err(DurabilityError::from)
}
async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
self.event_store
.read_value(key.as_bytes())
.map_err(DurabilityError::from)
}
async fn scan(&self, prefix: &str) -> Result<Vec<StoredEntry>, DurabilityError> {
let prefix_bytes = prefix.as_bytes().to_vec();
let matches = self
.event_store
.scan(|meta| meta.stream_key.starts_with(&prefix_bytes))
.map_err(DurabilityError::from)?;
let mut entries = Vec::new();
for stream in matches {
let events = self
.event_store
.read(&stream.stream_key)
.map_err(DurabilityError::from)?;
entries.extend(events.into_iter().map(StoredEntry::from));
}
Ok(entries)
}
async fn flush(&self) -> Result<(), DurabilityError> {
self.event_store.flush().map_err(DurabilityError::from)
}
}
#[derive(Debug)]
struct EphemeralGuard<S> {
store: Option<S>,
dir: Option<TempDir>,
}
impl<S> Drop for EphemeralGuard<S> {
fn drop(&mut self) {
let store = self.store.take();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(store)));
if let Err(panic) = outcome {
if let Some(dir) = self.dir.take() {
let leaked = dir.keep();
tracing::error!(
path = %leaked.display(),
"ephemeral store drop panicked; leaking its directory rather than \
removing it under possibly-live database workers"
);
}
std::panic::resume_unwind(panic);
}
if let Some(dir) = self.dir.take() {
let path = dir.path().to_path_buf();
if let Err(error) = dir.close() {
tracing::error!(
path = %path.display(),
%error,
"ephemeral store directory removal failed; residue remains at the \
logged path"
);
}
}
}
}
#[derive(Debug)]
pub struct EphemeralHaematiteStore {
guard: EphemeralGuard<HaematiteStore>,
}
impl EphemeralHaematiteStore {
fn new(database: Database, ephemeral_dir: TempDir) -> Self {
Self {
guard: EphemeralGuard {
store: Some(HaematiteStore::new(Arc::new(EventStore::new(database)))),
dir: Some(ephemeral_dir),
},
}
}
fn store(&self) -> Result<&HaematiteStore, DurabilityError> {
self.guard
.store
.as_ref()
.ok_or(DurabilityError::EphemeralStoreDetached)
}
#[cfg(test)]
pub(crate) fn ephemeral_dir_path(&self) -> Option<&Path> {
self.guard.dir.as_ref().map(TempDir::path)
}
}
#[async_trait::async_trait]
impl DurableStore for EphemeralHaematiteStore {
async fn append(
&self,
stream_key: &str,
payload: Vec<u8>,
expected_seq: u64,
) -> Result<u64, DurabilityError> {
self.store()?
.append(stream_key, payload, expected_seq)
.await
}
async fn read_from(
&self,
stream_key: &str,
offset: u64,
limit: usize,
) -> Result<Vec<StoredEntry>, DurabilityError> {
self.store()?.read_from(stream_key, offset, limit).await
}
async fn cas(&self, key: &str, old_value: u64, new_value: u64) -> Result<(), DurabilityError> {
self.store()?.cas(key, old_value, new_value).await
}
async fn read_value(&self, key: &str) -> Result<Option<u64>, DurabilityError> {
self.store()?.read_value(key).await
}
async fn scan(&self, prefix: &str) -> Result<Vec<StoredEntry>, DurabilityError> {
self.store()?.scan(prefix).await
}
async fn flush(&self) -> Result<(), DurabilityError> {
self.store()?.flush().await
}
}
pub fn open_ephemeral(shard_count: usize) -> Result<EphemeralHaematiteStore, DurabilityError> {
open_ephemeral_in(ephemeral_tempdir(None)?, shard_count)
}
#[cfg(any(test, feature = "test-support"))]
pub fn open_ephemeral_rooted(
root: &Path,
shard_count: usize,
) -> Result<EphemeralHaematiteStore, DurabilityError> {
open_ephemeral_in(ephemeral_tempdir(Some(root))?, shard_count)
}
fn ephemeral_tempdir(root: Option<&Path>) -> Result<TempDir, DurabilityError> {
let mut builder = tempfile::Builder::new();
builder.prefix("liminal-durability-");
root.map_or_else(|| builder.tempdir(), |root| builder.tempdir_in(root))
.map_err(|error| {
DurabilityError::EphemeralStoreOpen(format!(
"could not create temporary directory: {error}"
))
})
}
fn open_ephemeral_in(
ephemeral_dir: TempDir,
shard_count: usize,
) -> Result<EphemeralHaematiteStore, DurabilityError> {
let database = Database::create(DatabaseConfig {
data_dir: ephemeral_dir.path().to_path_buf(),
shard_count,
distributed: None,
executor_threads: None,
})
.map_err(|error| DurabilityError::EphemeralStoreOpen(error.to_string()))?;
Ok(EphemeralHaematiteStore::new(database, ephemeral_dir))
}
#[cfg(test)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct EngineReadAccounting {
pub(crate) calls: usize,
pub(crate) engine_entries: usize,
pub(crate) unbounded_calls: usize,
pub(crate) counter_overflow_observed: bool,
}
#[cfg(test)]
std::thread_local! {
static ENGINE_READ_ACCOUNTING: std::cell::RefCell<Option<EngineReadAccounting>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
pub(crate) struct EngineReadAccountingGuard {
_not_send: std::marker::PhantomData<*const ()>,
}
#[cfg(test)]
impl EngineReadAccountingGuard {
pub(crate) fn start() -> Self {
ENGINE_READ_ACCOUNTING.with(|accounting| {
*accounting.borrow_mut() = Some(EngineReadAccounting::default());
});
Self {
_not_send: std::marker::PhantomData,
}
}
#[allow(clippy::unused_self)]
pub(crate) fn snapshot(&self) -> EngineReadAccounting {
ENGINE_READ_ACCOUNTING
.with(|accounting| accounting.borrow().as_ref().copied().unwrap_or_default())
}
}
#[cfg(test)]
impl Drop for EngineReadAccountingGuard {
fn drop(&mut self) {
ENGINE_READ_ACCOUNTING.with(|accounting| {
*accounting.borrow_mut() = None;
});
}
}
#[cfg(test)]
fn account_engine_read(engine_entries: usize, unbounded: bool) {
ENGINE_READ_ACCOUNTING.with(|accounting| {
if let Some(active) = accounting.borrow_mut().as_mut() {
match (
active.calls.checked_add(1),
active.engine_entries.checked_add(engine_entries),
) {
(Some(calls), Some(entries)) => {
active.calls = calls;
active.engine_entries = entries;
}
_ => active.counter_overflow_observed = true,
}
if unbounded {
match active.unbounded_calls.checked_add(1) {
Some(unbounded_calls) => active.unbounded_calls = unbounded_calls,
None => active.counter_overflow_observed = true,
}
}
}
});
}
#[cfg(not(test))]
const fn account_engine_read(_engine_entries: usize, _unbounded: bool) {}
impl From<Event> for StoredEntry {
fn from(event: Event) -> Self {
Self {
payload: event.payload,
sequence: event.seq,
timestamp: event.timestamp,
}
}
}
impl From<ApiError> for DurabilityError {
fn from(error: ApiError) -> Self {
match error {
ApiError::SequenceConflict(conflict) => conflict.into(),
ApiError::CasMismatch(mismatch) => mismatch.into(),
other @ (ApiError::CorruptEvent(_)
| ApiError::Storage(_)
| ApiError::HistoryCompacted(_)) => Self::StoreError(other),
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod ephemeral_lifecycle_tests {
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use super::super::bridge::block_on;
use super::{
DurableStore, EphemeralGuard, open_ephemeral, open_ephemeral_in, open_ephemeral_rooted,
};
const TEST_SHARD_COUNT: usize = 2;
#[derive(Clone, Default)]
struct CapturedLog(Arc<Mutex<Vec<u8>>>);
impl CapturedLog {
fn text(&self) -> String {
let bytes = self
.0
.lock()
.expect("capture buffer is not poisoned")
.clone();
String::from_utf8(bytes).expect("tracing's fmt writer emits utf-8")
}
fn capturing<R>(&self, body: impl FnOnce() -> R) -> R {
static INSTALL: std::sync::Once = std::sync::Once::new();
struct ResetOnDrop;
impl Drop for ResetOnDrop {
fn drop(&mut self) {
ACTIVE_CAPTURE.with(|slot| *slot.borrow_mut() = None);
}
}
INSTALL.call_once(|| {
let subscriber = tracing_subscriber::fmt()
.with_writer(RoutedWriter)
.with_ansi(false)
.finish();
tracing::subscriber::set_global_default(subscriber)
.expect("no other global tracing subscriber is installed in this test binary");
});
ACTIVE_CAPTURE.with(|slot| *slot.borrow_mut() = Some(self.clone()));
let _reset = ResetOnDrop;
body()
}
}
thread_local! {
static ACTIVE_CAPTURE: std::cell::RefCell<Option<CapturedLog>> =
const { std::cell::RefCell::new(None) };
}
#[derive(Clone, Copy, Default)]
struct RoutedWriter;
impl std::io::Write for RoutedWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
ACTIVE_CAPTURE.with(|slot| {
if let Some(capture) = slot.borrow().as_ref() {
capture
.0
.lock()
.map_err(|_| std::io::Error::other("capture buffer poisoned"))?
.extend_from_slice(buf);
}
Ok(buf.len())
})
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'writer> tracing_subscriber::fmt::MakeWriter<'writer> for RoutedWriter {
type Writer = Self;
fn make_writer(&'writer self) -> Self::Writer {
*self
}
}
#[cfg(unix)]
fn set_mode(path: &Path, mode: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
.expect("test can set permissions on a directory it created");
}
struct OrderProbeStore {
dir: PathBuf,
}
impl Drop for OrderProbeStore {
fn drop(&mut self) {
assert!(
self.dir.exists(),
"the guard must drop the store BEFORE removing the directory"
);
}
}
struct PanickingProbeStore;
impl Drop for PanickingProbeStore {
fn drop(&mut self) {
panic!("injected store-drop panic");
}
}
fn write_one_event(store: &dyn DurableStore) {
block_on(store.append("lifecycle/probe", b"payload".to_vec(), 0))
.expect("bridge completes synchronously")
.expect("append to a fresh ephemeral stream succeeds");
block_on(store.flush())
.expect("bridge completes synchronously")
.expect("flush of a live ephemeral store succeeds");
}
#[test]
fn ephemeral_dir_removed_after_last_handle_drops() {
let store = open_ephemeral(TEST_SHARD_COUNT).expect("ephemeral open succeeds");
let dir = store
.ephemeral_dir_path()
.expect("ephemeral store carries a guard dir")
.to_path_buf();
assert!(
dir.exists(),
"the guard directory exists while the store is live"
);
write_one_event(&store);
drop(store);
assert!(
!dir.exists(),
"the guard directory is removed on normal drop"
);
}
#[test]
fn ephemeral_dir_survives_until_last_store_clone_drops() {
let store = open_ephemeral(TEST_SHARD_COUNT).expect("ephemeral open succeeds");
let dir = store
.ephemeral_dir_path()
.expect("ephemeral store carries a guard dir")
.to_path_buf();
write_one_event(&store);
let erased: Arc<dyn DurableStore> = Arc::new(store);
let clone_a = Arc::clone(&erased);
let clone_b = Arc::clone(&erased);
drop(erased);
assert!(
dir.exists(),
"directory survives while store clones remain alive"
);
drop(clone_a);
assert!(
dir.exists(),
"directory survives while one store clone remains alive"
);
drop(clone_b);
assert!(
!dir.exists(),
"the last store clone dropping removes the directory"
);
}
#[test]
fn ephemeral_open_failure_rolls_back_directory() {
let seeded = tempfile::Builder::new()
.prefix("liminal-durability-test-")
.tempdir()
.expect("test can create a temp dir");
let dir = seeded.path().to_path_buf();
std::fs::write(dir.join("config.json"), b"not-a-valid-config")
.expect("test can seed a conflicting config");
let result = open_ephemeral_in(seeded, TEST_SHARD_COUNT);
assert!(result.is_err(), "an injected open failure returns Err");
assert!(
!dir.exists(),
"the guard removes the directory on open failure — zero residue"
);
}
#[test]
fn repeated_ephemeral_cycles_each_own_distinct_dir_zero_residue() {
let mut seen: Vec<PathBuf> = Vec::new();
for _ in 0..5 {
let store = open_ephemeral(TEST_SHARD_COUNT).expect("ephemeral open succeeds");
let dir = store
.ephemeral_dir_path()
.expect("ephemeral store carries a guard dir")
.to_path_buf();
assert!(
dir.exists(),
"the cycle's directory exists while its store is live"
);
assert!(!seen.contains(&dir), "each cycle owns a distinct directory");
seen.push(dir.clone());
write_one_event(&store);
drop(store);
assert!(
!dir.exists(),
"the cycle's directory is removed after its store drops"
);
}
}
#[test]
fn guard_drops_store_before_removing_directory() {
let dir = tempfile::tempdir().expect("test can create a temp dir");
let path = dir.path().to_path_buf();
let guard = EphemeralGuard {
store: Some(OrderProbeStore { dir: path.clone() }),
dir: Some(dir),
};
drop(guard);
assert!(!path.exists(), "a clean drop still removes the directory");
}
#[test]
fn guard_leaks_directory_when_store_drop_panics() {
let dir = tempfile::tempdir().expect("test can create a temp dir");
let path = dir.path().to_path_buf();
let guard = EphemeralGuard {
store: Some(PanickingProbeStore),
dir: Some(dir),
};
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || drop(guard)));
assert!(unwound.is_err(), "the injected store-drop panic propagates");
assert!(
path.exists(),
"a panicking store drop leaks the directory instead of removing it"
);
std::fs::remove_dir_all(&path).expect("test cleans up the deliberately leaked directory");
}
#[test]
fn rooted_ephemeral_store_lives_and_dies_under_the_given_root() {
let root = tempfile::tempdir().expect("test can create a temp root");
let store =
open_ephemeral_rooted(root.path(), TEST_SHARD_COUNT).expect("rooted open succeeds");
let dir = store
.ephemeral_dir_path()
.expect("ephemeral store carries a guard dir")
.to_path_buf();
assert!(
dir.starts_with(root.path()),
"the guard directory is created under the supplied root"
);
write_one_event(&store);
drop(store);
assert!(!dir.exists(), "the rooted directory is removed on drop");
}
#[test]
fn ephemeral_dir_persists_across_unrelated_work_then_goes_on_clean_drop() {
let store = open_ephemeral(TEST_SHARD_COUNT).expect("ephemeral open succeeds");
let dir = store
.ephemeral_dir_path()
.expect("ephemeral store carries a guard dir")
.to_path_buf();
assert!(
dir.exists(),
"the directory exists as soon as the store does"
);
for round in 0..3_u64 {
block_on(store.append("clean-teardown/probe", b"payload".to_vec(), round))
.expect("bridge completes synchronously")
.expect("append to a live ephemeral store succeeds");
assert!(
dir.exists(),
"the directory is still there after append round {round}"
);
}
block_on(store.cas("clean-teardown/counter", 0, 7))
.expect("bridge completes synchronously")
.expect("cas on a live ephemeral store succeeds");
let entries = block_on(store.read_from("clean-teardown/probe", 0, 10))
.expect("bridge completes synchronously")
.expect("read from a live ephemeral store succeeds");
assert_eq!(entries.len(), 3, "every appended entry is readable back");
assert!(
dir.exists(),
"the directory is still there after unrelated cas and read work"
);
block_on(store.flush())
.expect("bridge completes synchronously")
.expect("flush of a live ephemeral store succeeds");
drop(store);
assert!(
!dir.exists(),
"the clean drop removes the directory it kept alive throughout"
);
}
#[cfg(unix)]
#[test]
fn clean_drop_removal_failure_is_logged_and_never_panics() {
let parent = tempfile::tempdir().expect("test can create a temp parent");
let dir = tempfile::Builder::new()
.prefix("liminal-durability-")
.tempdir_in(parent.path())
.expect("test can create a guard dir under the parent");
let path = dir.path().to_path_buf();
let guard = EphemeralGuard {
store: Some(()),
dir: Some(dir),
};
set_mode(parent.path(), 0o500);
let captured = CapturedLog::default();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
captured.capturing(|| drop(guard));
}));
set_mode(parent.path(), 0o700);
assert!(
outcome.is_ok(),
"a removal failure is reported, never raised as a panic"
);
let logged = captured.text();
assert!(
logged.contains("ERROR"),
"the removal failure is logged at error level; captured: {logged:?}"
);
assert!(
logged.contains(&path.display().to_string()),
"the log names the directory that survived; captured: {logged:?}"
);
assert!(
path.exists(),
"the residue is left where the log says it is, not silently claimed removed"
);
}
#[test]
fn clean_drop_that_succeeds_logs_nothing() {
let dir = tempfile::tempdir().expect("test can create a temp dir");
let path = dir.path().to_path_buf();
let guard = EphemeralGuard {
store: Some(()),
dir: Some(dir),
};
let captured = CapturedLog::default();
captured.capturing(|| drop(guard));
assert!(!path.exists(), "the successful clean drop removed the dir");
assert!(
captured.text().is_empty(),
"a successful removal is silent; captured: {:?}",
captured.text()
);
}
#[test]
fn panic_path_leak_is_logged_with_its_path() {
let dir = tempfile::tempdir().expect("test can create a temp dir");
let path = dir.path().to_path_buf();
let guard = EphemeralGuard {
store: Some(PanickingProbeStore),
dir: Some(dir),
};
let captured = CapturedLog::default();
let unwound = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
captured.capturing(|| drop(guard));
}));
assert!(unwound.is_err(), "the injected store-drop panic propagates");
let logged = captured.text();
assert!(
logged.contains("ERROR"),
"the sanctioned leak is logged at error level; captured: {logged:?}"
);
assert!(
logged.contains(&path.display().to_string()),
"the leak log names the leaked directory; captured: {logged:?}"
);
std::fs::remove_dir_all(&path).expect("test cleans up the deliberately leaked directory");
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod paged_read_shape_tests {
use super::{DurableStore, EngineReadAccountingGuard, open_ephemeral};
use crate::durability::bridge::block_on;
const PAGE: usize = 64;
const ROWS: u64 = 256;
const STREAM: &str = "liminal/p0-60/paged-read-shape";
fn seeded() -> Result<impl DurableStore, Box<dyn std::error::Error>> {
let store = open_ephemeral(1)?;
for sequence in 0..ROWS {
block_on(store.append(STREAM, sequence.to_be_bytes().to_vec(), sequence))??;
}
block_on(store.flush())??;
Ok(store)
}
fn read_whole_stream(
store: &impl DurableStore,
page: usize,
) -> Result<usize, Box<dyn std::error::Error>> {
let mut offset = 0_u64;
let mut seen = 0_usize;
loop {
let entries = block_on(store.read_from(STREAM, offset, page))??;
if entries.is_empty() {
return Ok(seen);
}
for entry in &entries {
assert_eq!(entry.sequence, offset, "paged read must stay contiguous");
offset += 1;
}
seen = seen
.checked_add(entries.len())
.ok_or("row counter overflowed")?;
}
}
#[test]
fn a_bounded_read_never_scans_beyond_its_page() -> Result<(), Box<dyn std::error::Error>> {
let store = seeded()?;
let accounting = EngineReadAccountingGuard::start();
let seen = read_whole_stream(&store, PAGE)?;
let walk = accounting.snapshot();
drop(accounting);
assert_eq!(
u64::try_from(seen)?,
ROWS,
"the walk must deliver every row"
);
assert!(
!walk.counter_overflow_observed,
"a saturated counter is not a measurement"
);
assert!(walk.calls > 0, "the walk must have reached the store");
assert_eq!(
u64::try_from(walk.engine_entries)?,
ROWS,
"a full stream read must scan each row exactly once instead of \
re-scanning every suffix once per page"
);
let accounting = EngineReadAccountingGuard::start();
let head = block_on(store.read_from(STREAM, 0, PAGE))??;
let head_read = accounting.snapshot();
drop(accounting);
assert_eq!(head.len(), PAGE, "a full page returns its limit");
assert_eq!(
head_read.engine_entries, PAGE,
"the engine must be asked for one page, not for the whole stream"
);
let middle_offset = ROWS / 2;
let accounting = EngineReadAccountingGuard::start();
let middle = block_on(store.read_from(STREAM, middle_offset, PAGE))??;
let middle_read = accounting.snapshot();
drop(accounting);
assert_eq!(
middle.len(),
PAGE,
"a full page mid-stream returns its limit"
);
assert_eq!(
middle_read.engine_entries, PAGE,
"a mid-stream page must not scan the rows that follow it"
);
let accounting = EngineReadAccountingGuard::start();
let past = block_on(store.read_from(STREAM, ROWS, PAGE))??;
let past_read = accounting.snapshot();
drop(accounting);
assert!(past.is_empty(), "past the head is end of stream");
assert_eq!(
past_read.engine_entries, 0,
"an end-of-stream page must not scan the stream"
);
Ok(())
}
#[test]
fn page_size_never_changes_the_answer() -> Result<(), Box<dyn std::error::Error>> {
let store = seeded()?;
let whole = block_on(store.read_from(STREAM, 0, usize::MAX))??;
assert_eq!(u64::try_from(whole.len())?, ROWS);
for page in [1_usize, 7, 64, 255, 256, 257] {
let mut offset = 0_u64;
let mut collected = Vec::new();
loop {
let entries = block_on(store.read_from(STREAM, offset, page))??;
if entries.is_empty() {
break;
}
assert!(entries.len() <= page, "a page never exceeds its limit");
offset = offset
.checked_add(u64::try_from(entries.len())?)
.ok_or("offset overflowed")?;
collected.extend(entries);
}
assert_eq!(collected, whole, "page size {page} changed the answer");
}
assert!(
block_on(store.read_from(STREAM, 0, 0))??.is_empty(),
"a zero limit reads nothing"
);
for offset in [0_u64, 1, 63, 64, 65, 128, 255] {
let suffix = block_on(store.read_from(STREAM, offset, usize::MAX))??;
assert_eq!(
suffix,
whole[usize::try_from(offset)?..],
"suffix from {offset} diverged"
);
}
Ok(())
}
}