use crate::{Blob as _, Error, ReadOptions, WriteOptions, buffer::Write, storage::Pending};
use commonware_utils::{NZUsize, channel::oneshot};
use futures::FutureExt as _;
use std::sync::{Arc, mpsc};
pub(crate) async fn check_untouched_creation_leaves_no_debt<S: crate::Storage>(
storage: &S,
pending: &Pending,
) {
let before = pending.completions();
let (blob, _) = storage.open("durable_creation", b"blob").await.unwrap();
drop(blob);
let owed = pending.owes("durable_creation", b"blob");
let (blob, size) = storage.open("durable_creation", b"blob").await.unwrap();
drop(blob);
let completions = pending.completions() - before;
storage.remove("durable_creation", None).await.unwrap();
assert!(!owed, "successful creation must leave no durability debt");
assert_eq!(size, 0);
assert_eq!(completions, 0, "untouched creation needs no reopen flush");
}
pub(crate) async fn check_failed_creation<S: crate::Storage>(storage: &S, pending: &Pending) {
let completions = pending.completions();
for partial in [true, false] {
*pending.test.fail_creation_after.lock() = Some(if partial { 1 } else { usize::MAX });
assert!(matches!(
storage.open("failed_creation", b"blob").await,
Err(Error::Closed)
));
if !partial {
for _ in 0..2 {
assert!(
matches!(
storage.open("failed_creation", b"blob").await,
Err(Error::Closed)
),
"a parseable header must not hide the failed creation barrier"
);
}
storage
.remove("failed_creation", Some(b"blob"))
.await
.unwrap();
}
let (blob, size) = storage.open("failed_creation", b"blob").await.unwrap();
assert_eq!(size, 0);
drop(blob);
storage.remove("failed_creation", None).await.unwrap();
}
assert_eq!(
pending.completions(),
completions,
"failed creation must not leave debt"
);
}
pub(crate) async fn check_remove_live_dirty_owner<S: crate::Storage>(
storage: &S,
pending: &Pending,
) {
for by_name in [true, false] {
for unlink_first in [false, true] {
let partition = "remove_live_dirty";
let name = b"blob";
let (blob, size) = storage.open(partition, name).await.unwrap();
let writer = Write::new(blob, size, NZUsize!(1));
let writer = writer.write_at(0, b"dirty").await.unwrap();
let writer = writer.wait_for_sync().await.unwrap();
let completions = pending.completions();
let target = by_name.then_some(name.as_slice());
if unlink_first {
storage.remove(partition, target).await.unwrap();
assert_eq!(
writer.read_at(0, 5).await.unwrap().coalesce().as_ref(),
b"dirty",
);
drop(writer);
} else {
drop(writer);
storage.remove(partition, target).await.unwrap();
}
assert!(
!pending.owes(partition, name),
"removal must forget the name's debt"
);
let (blob, size) = storage.open(partition, name).await.unwrap();
assert_eq!(size, 0);
drop(blob);
assert_eq!(pending.completions(), completions);
storage.remove(partition, None).await.unwrap();
}
}
}
pub(crate) async fn check_orphaned_write<S: crate::Storage>(storage: &S) {
let (blob, _) = storage.open("orphaned_write", b"blob").await.unwrap();
let mut write = Box::pin(blob.write_at(0, b"orphaned", WriteOptions::default()));
let _ = futures::poll!(write.as_mut());
drop(write);
drop(blob);
let (blob, len) = storage.open("orphaned_write", b"blob").await.unwrap();
assert_eq!(len, 8);
let read = blob.read_at(0, 8, ReadOptions::default()).await.unwrap();
assert_eq!(read.coalesce().as_ref(), b"orphaned");
drop(blob);
storage.remove("orphaned_write", None).await.unwrap();
}
pub(crate) async fn check_sync_writes<S: crate::Storage>(storage: &S, pending: &Pending) {
for (case, options) in [
WriteOptions::SYNC,
WriteOptions::SYNC | WriteOptions::DONT_CACHE,
]
.into_iter()
.enumerate()
{
let before = pending.completions();
let (blob, _) = storage.open("durable_writes", &[case as u8]).await.unwrap();
blob.write_at(0, b"first", options).await.unwrap();
blob.write_at(5, b"second", options).await.unwrap();
drop(blob);
let (blob, size) = storage.open("durable_writes", &[case as u8]).await.unwrap();
assert_eq!(size, 11);
assert_eq!(
blob.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"firstsecond"
);
drop(blob);
assert_eq!(
pending.completions(),
before,
"successful durable writes need no reopen flush"
);
}
for plain_first in [false, true] {
let before = pending.completions();
let name = [2, u8::from(plain_first)];
let (blob, _) = storage.open("durable_writes", &name).await.unwrap();
let (first, second) = if plain_first {
(WriteOptions::default(), WriteOptions::SYNC)
} else {
(WriteOptions::SYNC, WriteOptions::default())
};
blob.write_at(0, b"first", first).await.unwrap();
blob.write_at(5, b"second", second).await.unwrap();
drop(blob);
let (blob, size) = storage.open("durable_writes", &name).await.unwrap();
assert_eq!(size, 11);
assert_eq!(
blob.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"firstsecond"
);
drop(blob);
let needs_flush = !plain_first || cfg!(target_os = "linux");
assert_eq!(
pending.completions() - before,
u64::from(needs_flush),
"plain_first={plain_first}"
);
}
}
pub(crate) async fn check_recreate_reopen<S: crate::Storage>(storage: &S, pending: &Pending) {
let (ready, _) = storage.open("independent", b"ready").await.unwrap();
ready.sync().await.unwrap();
drop(ready);
for remove_name in [true, false] {
let partition = "recreate_pending";
let name = b"blob";
let (old, _) = storage.open(partition, name).await.unwrap();
old.write_at(0, b"old", WriteOptions::default())
.await
.unwrap();
storage
.remove(partition, remove_name.then_some(name.as_slice()))
.await
.unwrap();
assert_eq!(
old.read_at(0, 3, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"old"
);
let (current, len) = storage.open(partition, name).await.unwrap();
assert_eq!(len, 0);
let current = Arc::new(current);
let reader = current.clone();
current
.write_at(0, b"new", WriteOptions::default())
.await
.unwrap();
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*pending.test.before_complete.lock() = Some((entered, gate));
let completions = pending.completions();
drop(current);
drop(old);
drop(reader);
let mut reopen = Box::pin(storage.open(partition, name));
commonware_macros::select! {
entered = entering => entered.expect("reopen dropped its flush gate"),
_ = &mut reopen => panic!("reopen completed before its flush"),
}
assert!(
(&mut reopen).now_or_never().is_none(),
"reopen exposed the replacement before its flush"
);
let clean_progress = async {
let names = storage.scan("independent").await?;
let (blob, len) = storage.open("independent", b"ready").await?;
drop(blob);
Ok::<_, Error>((names, len))
}
.await;
drop(release);
let (reopened, len) = reopen.await.unwrap();
let bytes = reopened
.read_at(0, 3, ReadOptions::default())
.await
.unwrap()
.coalesce();
drop(reopened);
storage.remove(partition, None).await.unwrap();
assert_eq!(len, 3);
assert_eq!(bytes.as_ref(), b"new");
assert_eq!(
pending.completions() - completions,
1,
"the replacement's debt is flushed once"
);
let (names, len) = clean_progress.unwrap();
assert_eq!(names, vec![b"ready".to_vec()]);
assert_eq!(len, 0);
}
}
pub(crate) async fn check_recreate_clean<S: crate::Storage>(storage: &S, pending: &Pending) {
for remove_name in [true, false] {
let partition = "recreate_clean";
let name = b"blob";
let (old, _) = storage.open(partition, name).await.unwrap();
old.write_at(0, b"old", WriteOptions::default())
.await
.unwrap();
storage
.remove(partition, remove_name.then_some(name.as_slice()))
.await
.unwrap();
let (current, _) = storage.open(partition, name).await.unwrap();
current
.write_at(0, b"new", WriteOptions::SYNC)
.await
.unwrap();
let completions = pending.completions();
drop(old);
drop(current);
let (reopened, len) = storage.open(partition, name).await.unwrap();
drop(reopened);
storage.remove(partition, None).await.unwrap();
assert_eq!(len, 3);
assert_eq!(
pending.completions(),
completions,
"the removed open recorded debt for its replacement"
);
}
}