use super::{Layout, Pending, create_header, hold::Hold, resolve_header, sync_dir};
use crate::{BlobVersion, Error};
use commonware_formatting::{from_hex, hex};
use commonware_utils::channel::oneshot;
#[cfg(not(target_os = "linux"))]
use std::collections::HashSet;
use std::{
fs,
io::ErrorKind,
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
};
use tokio::sync::Mutex;
mod blob;
#[derive(Clone)]
pub struct Config {
pub storage_directory: PathBuf,
pub blob_layouts: RangeInclusive<Layout>,
}
impl Config {
pub const fn new(storage_directory: PathBuf, blob_layouts: RangeInclusive<Layout>) -> Self {
Self {
storage_directory,
blob_layouts,
}
}
}
#[derive(Clone)]
pub struct Storage {
lock: Arc<Mutex<Partitions>>,
cfg: Config,
hold: Arc<Hold>,
pending: Arc<Pending>,
}
#[derive(Default)]
struct Partitions {
#[cfg(not(target_os = "linux"))]
synced: HashSet<PathBuf>,
#[cfg(all(test, not(target_os = "linux")))]
sync_hook: tests::SyncHook,
}
impl Partitions {
fn sync_root(&mut self, root: &Path) -> Result<(), Error> {
#[cfg(all(test, not(target_os = "linux")))]
self.sync_hook.run()?;
sync_dir(root)
}
fn sync(
&mut self,
path: impl AsRef<Path> + Into<PathBuf>,
root: Option<&Path>,
) -> Result<(), Error> {
#[cfg(all(test, not(target_os = "linux")))]
self.sync_hook.run()?;
sync_dir(path.as_ref())?;
if let Some(root) = root {
self.sync_root(root)?;
}
#[cfg(not(target_os = "linux"))]
self.synced.insert(path.into());
Ok(())
}
#[allow(clippy::missing_const_for_fn)]
fn invalidate(&mut self, _path: &Path) {
#[cfg(not(target_os = "linux"))]
self.synced.remove(_path);
}
#[allow(clippy::missing_const_for_fn)]
fn sync_once(&mut self, _path: &Path) -> Result<(), Error> {
#[cfg(not(target_os = "linux"))]
{
if self.synced.contains(_path) {
return Ok(());
}
self.sync(_path, None)?;
}
Ok(())
}
}
impl Storage {
pub fn new(cfg: Config) -> Self {
let hold = Hold::acquire(&cfg.storage_directory).unwrap_or_else(|e| {
panic!(
"failed to acquire storage directory hold ({}): {e}",
cfg.storage_directory.display()
)
});
Self {
lock: Arc::new(Mutex::new(Partitions::default())),
cfg,
hold,
pending: Arc::new(Pending::default()),
}
}
async fn dispatch<T: Send + 'static>(
&self,
f: impl FnOnce(&mut Partitions) -> Result<T, Error> + Send + 'static,
) -> Result<T, Error> {
let guard = self.lock.clone().lock_owned().await;
let hold = self.hold.clone();
let (sender, receiver) = oneshot::channel();
#[cfg(test)]
let after_dispatch = self.pending.test.after_dispatch.lock().take();
let task = tokio::task::spawn_blocking(move || {
{
let _hold = hold;
let mut guard = guard;
let _ = sender.send(f(&mut guard));
}
#[cfg(test)]
if let Some((entered, released)) = after_dispatch {
let _ = entered.send(());
let _ = released.recv();
}
});
match task.await {
Ok(()) => receiver.await.map_err(|_| Error::Closed)?,
Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()),
Err(_) => Err(Error::Closed),
}
}
}
impl crate::Storage for Storage {
type Blob = blob::Blob;
async fn open_versioned(
&self,
partition: &str,
name: &[u8],
versions: RangeInclusive<BlobVersion>,
) -> Result<(Self::Blob, u64, BlobVersion), Error> {
super::validate_partition_name(partition)?;
let path = self.cfg.storage_directory.join(partition).join(hex(name));
let storage_directory = self.cfg.storage_directory.clone();
let partition = partition.to_string();
let name = name.to_vec();
let blob_layouts = self.cfg.blob_layouts.clone();
let hold = self.hold.clone();
let pending = self.pending.clone();
let (blob, mut logical_size, blob_version, wait, owed) = self
.dispatch(move |partitions| {
let parent = match path.parent() {
Some(parent) => parent,
None => return Err(Error::PartitionCreationFailed(partition)),
};
fs::create_dir_all(parent)
.map_err(|_| Error::PartitionCreationFailed(partition.clone()))?;
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|e| Error::BlobOpenFailed(partition.clone(), hex(&name), e.into()))?;
let raw_len = file.metadata().map_err(|_| Error::ReadFailed)?.len();
let existing = resolve_header(
&mut file,
raw_len,
&blob_layouts,
&versions,
&partition,
&name,
)?;
let (generation, wait, owed) =
pending.admit(&partition, &name, existing.is_some())?;
let (mut logical_size, blob_version, data_offset) = match existing {
Some(resolved) => {
partitions.sync_once(parent)?;
resolved
}
None => {
partitions.sync(parent, Some(&storage_directory))?;
create_header(&mut file, &blob_layouts, &versions, &generation)
.inspect_err(|error| pending.fail(&generation, error.clone()))?
}
};
if !owed {
logical_size =
file.metadata().map_err(|_| Error::ReadFailed)?.len() - data_offset;
}
let blob = Self::Blob::new(file, data_offset, hold, generation);
Ok((blob, logical_size, blob_version, wait, owed))
})
.await?;
if owed {
Pending::wait(wait).await?;
logical_size = blob.complete().await?;
}
Ok((blob, logical_size, blob_version))
}
async fn remove(&self, partition: &str, name: Option<&[u8]>) -> Result<(), Error> {
super::validate_partition_name(partition)?;
let path = self.cfg.storage_directory.join(partition);
let storage_directory = self.cfg.storage_directory.clone();
let partition = partition.to_string();
let name = name.map(<[u8]>::to_vec);
let pending = self.pending.clone();
self.dispatch(move |partitions| {
partitions.invalidate(&path);
if let Some(name) = &name {
let blob_path = path.join(hex(name));
fs::remove_file(blob_path)
.map_err(|_| Error::BlobMissing(partition.clone(), hex(name)))?;
pending.forget(&partition, Some(name));
partitions.sync(path, None)
} else {
fs::remove_dir_all(&path).map_err(|error| match error.kind() {
ErrorKind::NotFound => Error::PartitionMissing(partition.clone()),
_ => Error::Io(error.into()),
})?;
pending.forget(&partition, None);
partitions.sync_root(&storage_directory)
}
})
.await
}
async fn scan(&self, partition: &str) -> Result<Vec<Vec<u8>>, Error> {
super::validate_partition_name(partition)?;
let path = self.cfg.storage_directory.join(partition);
let partition = partition.to_string();
self.dispatch(move |partitions| {
let entries = fs::read_dir(&path).map_err(|error| match error.kind() {
ErrorKind::NotFound => Error::PartitionMissing(partition.clone()),
_ => Error::ReadFailed,
})?;
partitions.sync_once(&path)?;
let mut blobs = Vec::new();
for entry in entries {
let entry = entry.map_err(|_| Error::ReadFailed)?;
let file_type = entry.file_type().map_err(|_| Error::ReadFailed)?;
if !file_type.is_file() {
return Err(Error::PartitionCorrupt(partition));
}
if let Some(name) = entry.file_name().to_str() {
let decoded =
from_hex(name).ok_or_else(|| Error::PartitionCorrupt(partition.clone()))?;
if hex(&decoded) != name {
return Err(Error::PartitionCorrupt(partition));
}
blobs.push(decoded);
}
}
Ok(blobs)
})
.await
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use crate::{
BLOB_PAGE_SIZE, Blob, ReadOptions, Runner as _, Storage as _, WriteOptions,
storage::{Header, Layout, shared, tests::run_storage_tests},
tokio::Runner,
};
use commonware_utils::{Widen, sys_rng};
use futures::FutureExt as _;
use rand::RngExt as _;
use std::{
env,
future::Future,
pin::Pin,
sync::mpsc::{self, RecvTimeoutError},
task::{Context, Poll},
};
#[cfg(not(target_os = "linux"))]
#[derive(Default)]
pub(super) struct SyncHook {
pause: Option<(oneshot::Sender<()>, mpsc::Receiver<()>)>,
fail: Option<Error>,
}
#[cfg(not(target_os = "linux"))]
impl SyncHook {
pub(super) fn run(&mut self) -> Result<(), Error> {
if let Some((entered, released)) = self.pause.take() {
let _ = entered.send(());
let _ = released.recv();
}
if let Some(error) = self.fail.take() {
return Err(error);
}
Ok(())
}
}
fn random_suffix() -> u64 {
let mut rng = sys_rng();
rng.random()
}
#[tokio::test]
async fn test_hold_waits_for_straggling_remove() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_hold_remove_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config.clone());
let partition_path = storage_directory.join("partition");
std::fs::create_dir_all(&partition_path).unwrap();
for i in 0..5_000u64 {
std::fs::write(partition_path.join(hex(&i.to_be_bytes())), b"x").unwrap();
}
{
let mut remove = Box::pin(storage.remove("partition", None));
assert!(
(&mut remove).now_or_never().is_none(),
"removal completed before it could straggle"
);
}
drop(storage);
let storage = Storage::new(config);
let result = storage.scan("partition").await;
assert!(
matches!(result, Err(Error::PartitionMissing(_))),
"{result:?}"
);
drop(storage);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_hold_retained_by_open_blob() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_hold_blob_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config.clone());
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
drop(storage);
let config_second = config.clone();
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
let second = Storage::new(config_second);
tx.send(()).unwrap();
drop(second);
});
match rx.recv_timeout(std::time::Duration::from_millis(200)) {
Err(RecvTimeoutError::Timeout) => {}
other => panic!("second instance did not stay blocked on the hold: {other:?}"),
}
drop(blob);
rx.recv_timeout(std::time::Duration::from_secs(10))
.expect("second instance did not acquire the hold after the blob dropped");
handle.join().unwrap();
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[test]
fn test_hold_blocks_second_instance() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_hold_block_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let first = Storage::new(config.clone());
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
let second = Storage::new(config);
tx.send(()).unwrap();
drop(second);
});
match rx.recv_timeout(std::time::Duration::from_millis(200)) {
Err(RecvTimeoutError::Timeout) => {}
other => panic!("second instance did not stay blocked on the hold: {other:?}"),
}
drop(first);
rx.recv_timeout(std::time::Duration::from_secs(10))
.expect("second instance did not acquire the hold after the first released it");
handle.join().unwrap();
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[test]
fn test_storage() {
Runner::default().start(|context| async move {
let mut rng = sys_rng();
let storage_directory =
env::temp_dir().join(format!("storage_tokio_{}", rng.random::<u64>()));
let config = Config::new(storage_directory, Layout::ALL);
let storage = Storage::new(config);
run_storage_tests(context, storage).await;
});
}
#[tokio::test]
async fn test_start_sync_dropped_receiver() {
let mut rng = sys_rng();
let storage_directory =
env::temp_dir().join(format!("storage_tokio_start_sync_{}", rng.random::<u64>()));
let config = Config::new(storage_directory, Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"test_blob").await.unwrap();
blob.write_at(0, b"hello world", WriteOptions::default())
.await
.unwrap();
drop(blob.start_sync().await);
blob.start_sync().await.await.unwrap();
drop(blob);
let (blob, len) = storage.open("partition", b"test_blob").await.unwrap();
assert_eq!(len, 11);
let read = blob
.read_at(0, 11, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"hello world");
}
#[tokio::test]
async fn test_blob_header_handling() {
let mut rng = sys_rng();
let storage_directory =
env::temp_dir().join(format!("storage_tokio_header_{}", rng.random::<u64>()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, size) = storage.open("partition", b"test").await.unwrap();
assert_eq!(size, 0, "new blob should have logical size 0");
let data_offset = Layout::V1.data_offset();
let file_path = storage_directory.join("partition").join(hex(b"test"));
let metadata = std::fs::metadata(&file_path).unwrap();
assert_eq!(
metadata.len(),
data_offset,
"raw file should have a full header page"
);
let data = b"hello world";
blob.write_at(0, data, WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let metadata = std::fs::metadata(&file_path).unwrap();
assert_eq!(metadata.len(), data_offset + data.len() as u64);
let raw_content = std::fs::read(&file_path).unwrap();
assert_eq!(&raw_content[..Header::MAGIC_LENGTH], &Layout::V1.magic());
assert_eq!(
&raw_content[4..6],
&Layout::V1.layout_version().to_be_bytes()
);
assert_eq!(&raw_content[data_offset as usize..], data);
let read_buf = blob
.read_at(0, data.len(), ReadOptions::default())
.await
.unwrap();
assert_eq!(read_buf.coalesce(), data);
blob.resize(5).await.unwrap();
blob.sync().await.unwrap();
let metadata = std::fs::metadata(&file_path).unwrap();
assert_eq!(
metadata.len(),
data_offset + 5,
"resize(5) should leave 5 raw bytes past the header page"
);
blob.resize(0).await.unwrap();
blob.sync().await.unwrap();
let metadata = std::fs::metadata(&file_path).unwrap();
assert_eq!(
metadata.len(),
data_offset,
"resize(0) should leave only the header page"
);
blob.write_at(0, b"test data", WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
let (blob2, size2) = storage.open("partition", b"test").await.unwrap();
assert_eq!(size2, 9, "reopened blob should have logical size 9");
let read_buf = blob2.read_at(0, 9, ReadOptions::default()).await.unwrap();
assert_eq!(read_buf.coalesce(), b"test data");
drop(blob2);
let corrupted_path = storage_directory.join("partition").join(hex(b"corrupted"));
std::fs::write(&corrupted_path, vec![0u8; 4]).unwrap();
let (blob3, size3) = storage.open("partition", b"corrupted").await.unwrap();
assert_eq!(size3, 0, "corrupted blob should return logical size 0");
let metadata = std::fs::metadata(&corrupted_path).unwrap();
assert_eq!(
metadata.len(),
Layout::V1.data_offset(),
"corrupted blob should be reset to header-only"
);
drop(blob3);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_v1_paged_alignment() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_aligned_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let logical = crate::buffer::paged::page_size(BLOB_PAGE_SIZE);
let cache =
crate::buffer::paged::CacheRef::new(logical, std::num::NonZeroUsize::new(16).unwrap());
let (blob, size) = storage.open("partition", b"aligned").await.unwrap();
let mut writer = crate::buffer::paged::Writer::new(blob, size, 1024, cache)
.await
.unwrap();
let item: Vec<u8> = (0..1000u32).flat_map(|i| i.to_be_bytes()).collect();
for _ in 0..12 {
(writer, _) = writer.append(&item).await.unwrap();
}
let logical_size = writer.size();
writer.sync().await.unwrap();
let file_path = storage_directory.join("partition").join(hex(b"aligned"));
let raw = std::fs::read(&file_path).unwrap();
let raw_len: u64 = Widen::widen(raw.len());
let pages = logical_size.div_ceil(logical.widen());
assert_eq!(raw_len % u64::from(BLOB_PAGE_SIZE), 0);
assert_eq!(
raw_len,
Layout::V1.data_offset() + pages * u64::from(BLOB_PAGE_SIZE)
);
let data = &raw[usize::try_from(Layout::V1.data_offset()).unwrap()..];
for (page, physical) in data.chunks_exact(Widen::widen(BLOB_PAGE_SIZE)).enumerate() {
assert!(
crate::buffer::paged::validate_page_for_tests(physical),
"page {page} failed CRC validation at aligned boundary"
);
}
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_torn_creation_recovers() {
let storage_directory =
env::temp_dir().join(format!("test_torn_creation_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let (blob, _) = storage.open("partition", b"torn").await.unwrap();
blob.sync().await.unwrap();
drop(blob);
let path = storage_directory.join("partition").join(hex(b"torn"));
let region = std::fs::read(&path).unwrap();
let mut torn_content = vec![0u8; region.len()];
torn_content[..10].copy_from_slice(®ion[..10]);
let states = [region[..10].to_vec(), torn_content];
for state in states {
std::fs::write(&path, &state).unwrap();
let (blob, size) = storage.open("partition", b"torn").await.unwrap();
assert_eq!(size, 0);
blob.write_at(0, b"data".to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
let (blob, size) = storage.open("partition", b"torn").await.unwrap();
assert_eq!(size, 4);
let read = blob.read_at(0, 4, ReadOptions::default()).await.unwrap();
assert_eq!(read.coalesce(), b"data");
drop(blob);
}
let mut corrupt = vec![0u8; region.len()];
corrupt[..10].copy_from_slice(®ion[..10]);
corrupt[100] = 0xFF;
std::fs::write(&path, &corrupt).unwrap();
let result = storage.open("partition", b"torn").await;
assert!(matches!(result, Err(crate::Error::BlobCorrupt(_, _, _))));
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_canceled_open_retires_result_before_retry() {
let directory = env::temp_dir().join(format!("storage_tokio_delivery_{}", random_suffix()));
for layout in [Layout::V0, Layout::V1] {
for seeded in [false, true] {
let storage = Storage::new(Config::new(directory.clone(), layout..=layout));
let expected = if seeded { b"saved".as_slice() } else { &[] };
if seeded {
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, expected, WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
}
let (entered, entering) = oneshot::channel();
let (release, released) = std::sync::mpsc::channel();
*storage.pending.test.after_dispatch.lock() = Some((entered, released));
let mut first = Box::pin(storage.open("partition", b"blob"));
assert!(futures::poll!(&mut first).is_pending());
entering.await.unwrap();
let independent = storage.open("partition", b"independent").await;
drop(first);
let retry = std::panic::AssertUnwindSafe(storage.open("partition", b"blob"))
.catch_unwind()
.await;
drop(release);
drop(independent.unwrap());
let (blob, size) = retry
.expect("retry panicked")
.expect("canceled output retained the name");
assert_eq!(size, expected.len() as u64);
if !expected.is_empty() {
assert_eq!(
blob.read_at(0, expected.len(), ReadOptions::default())
.await
.unwrap()
.coalesce(),
expected,
);
}
blob.write_at(0, b"next", WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, expected.len().max(4) as u64);
assert_eq!(
blob.read_at(0, 4, ReadOptions::default())
.await
.unwrap()
.coalesce(),
b"next"
);
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(&directory).unwrap();
}
}
}
#[tokio::test]
async fn test_open_dropped_mid_creation() {
struct DropAfter<F: Future + Unpin> {
inner: Option<F>,
remaining: usize,
}
impl<F: Future + Unpin> Future for DropAfter<F> {
type Output = Option<F::Output>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.remaining == 0 {
self.inner = None;
return Poll::Ready(None);
}
self.remaining -= 1;
match self.inner.as_mut().unwrap().poll_unpin(cx) {
Poll::Ready(output) => Poll::Ready(Some(output)),
Poll::Pending => Poll::Pending,
}
}
}
let storage_directory =
env::temp_dir().join(format!("test_dropped_open_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
for depth in 0..64 {
let name = format!("blob{depth}");
let name = name.as_bytes();
let dropped = DropAfter {
inner: Some(Box::pin(storage.open("partition", name))),
remaining: depth,
}
.await;
let completed = dropped.is_some();
drop(dropped);
let (blob, size) = storage.open("partition", name).await.unwrap();
assert_eq!(size, 0);
blob.write_at(0, b"data".to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
let (blob, size) = storage.open("partition", name).await.unwrap();
assert_eq!(size, 4);
let read = blob.read_at(0, 4, ReadOptions::default()).await.unwrap();
assert_eq!(read.coalesce(), b"data");
drop(blob);
if completed {
let _ = std::fs::remove_dir_all(&storage_directory);
return;
}
}
panic!("open never completed within the poll budget");
}
#[tokio::test]
async fn test_blob_v1_rejects_nonzero_header_padding() {
let storage_directory =
env::temp_dir().join(format!("test_v1_header_padding_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let partition_dir = storage_directory.join("partition");
std::fs::create_dir_all(&partition_dir).unwrap();
let path = partition_dir.join(hex(b"dirty_padding"));
let mut raw = crate::storage::header::tests::v1_blob_bytes(0, b"payload");
raw[Header::PARSE_LEN] = 0xFF;
std::fs::write(&path, raw).unwrap();
let result = storage.open("partition", b"dirty_padding").await;
assert!(
matches!(result, Err(crate::Error::BlobCorrupt(_, _, reason)) if reason.contains("header padding"))
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_v0_legacy_read() {
let storage_directory =
env::temp_dir().join(format!("test_v0_legacy_read_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let payload = b"hello world";
let partition_dir = storage_directory.join("partition");
std::fs::create_dir_all(&partition_dir).unwrap();
let file_path = partition_dir.join(hex(b"v0"));
std::fs::write(&file_path, crate::storage::tests::v0_blob_bytes(0, payload)).unwrap();
let (blob, size) = storage.open("partition", b"v0").await.unwrap();
assert_eq!(size, payload.len() as u64);
assert_eq!(
blob.read_at(0, payload.len(), ReadOptions::default())
.await
.unwrap()
.coalesce(),
payload
);
blob.write_at(size, b"!".to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
let raw_content = std::fs::read(&file_path).unwrap();
assert_eq!(raw_content.len(), Header::PRELUDE_SIZE + payload.len() + 1);
assert_eq!(&raw_content[..Header::MAGIC_LENGTH], &Layout::V0.magic());
assert_eq!(&raw_content[Header::PRELUDE_SIZE..], b"hello world!");
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_magic_mismatch() {
let storage_directory =
env::temp_dir().join(format!("test_magic_mismatch_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let partition_path = storage_directory.join("partition");
std::fs::create_dir_all(&partition_path).unwrap();
let bad_magic_path = partition_path.join(hex(b"bad_magic"));
std::fs::write(&bad_magic_path, b"XXXXXXXX").unwrap();
let result = storage.open("partition", b"bad_magic").await;
assert!(
matches!(result, Err(crate::Error::BlobCorrupt(_, _, reason)) if reason.contains("invalid magic"))
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_partial_header_reset() {
let storage_directory =
env::temp_dir().join(format!("test_partial_header_reset_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let partition_path = storage_directory.join("partition");
std::fs::create_dir_all(&partition_path).unwrap();
for prefix_len in 0..Header::PRELUDE_SIZE {
let name = format!("short_{prefix_len}");
let path = partition_path.join(hex(name.as_bytes()));
std::fs::write(&path, vec![0u8; prefix_len]).unwrap();
let (blob, size) = storage
.open("partition", name.as_bytes())
.await
.expect("interrupted create should recover, not fail");
assert_eq!(size, 0, "recovered blob should be empty");
drop(blob);
let raw = std::fs::read(&path).unwrap();
assert_eq!(
raw.len(),
Layout::V1.data_offset() as usize,
"recovered blob should be header-only"
);
assert_eq!(&raw[..Header::MAGIC_LENGTH], &Layout::V1.magic());
storage
.open("partition", name.as_bytes())
.await
.expect("reopen after recovery should succeed");
}
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_scan_rejects_non_canonical_hex_file_names() {
for bad_name in ["0x626c6f62", "0X626C6F62", " 626c6f62", "626C6F62"] {
let storage_directory = env::temp_dir().join(format!(
"test_scan_non_canonical_{}_{}",
bad_name.replace([' ', '0', 'x', 'X'], "_"),
random_suffix()
));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let partition_path = storage_directory.join("partition");
std::fs::create_dir_all(&partition_path).unwrap();
std::fs::write(partition_path.join(bad_name), []).unwrap();
let err = match storage.scan("partition").await {
Ok(_) => panic!("scan should have failed for filename {bad_name:?}"),
Err(err) => err,
};
assert_eq!(
err.to_string(),
"partition corrupt: partition",
"filename {bad_name:?} should be rejected as corrupt",
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
}
#[tokio::test]
async fn test_partition_failures_are_not_absence() {
let directory = env::temp_dir().join(format!("storage_tokio_enotdir_{}", random_suffix()));
fs::create_dir_all(&directory).unwrap();
let partition = directory.join("partition");
fs::write(&partition, b"not a directory").unwrap();
let storage = Storage::new(Config::new(directory.clone(), Layout::ALL));
assert!(matches!(
storage.scan("partition").await,
Err(Error::ReadFailed)
));
assert!(matches!(
storage.scan("missing").await,
Err(Error::PartitionMissing(_))
));
assert!(matches!(
storage.remove("partition", None).await,
Err(Error::Io(_))
));
assert!(partition.exists());
assert!(matches!(
storage.remove("missing", None).await,
Err(Error::PartitionMissing(_))
));
drop(storage);
fs::remove_dir_all(directory).unwrap();
}
#[cfg(not(target_os = "linux"))]
#[rstest::rstest]
#[case(false)]
#[case(true)]
#[tokio::test]
async fn test_partition_scan_waits_for_sync(#[case] cancel: bool) {
tokio::time::timeout(std::time::Duration::from_secs(10), async {
let directory =
env::temp_dir().join(format!("storage_tokio_scan_sync_{}", random_suffix()));
let partition = directory.join("partition");
let other = directory.join("other");
fs::create_dir_all(&partition).unwrap();
fs::create_dir_all(&other).unwrap();
let storage = Storage::new(Config::new(directory.clone(), Layout::ALL));
crate::storage::sync(&directory).unwrap();
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
storage.lock.lock().await.sync_hook.pause = Some((entered, gate));
{
let mut scan = Box::pin(storage.scan("partition"));
assert!((&mut scan).now_or_never().is_none());
entering.await.unwrap();
assert!((&mut scan).now_or_never().is_none());
let mut next = Box::pin(storage.scan("partition"));
assert!((&mut next).now_or_never().is_none());
let scan = if cancel {
drop(scan);
None
} else {
Some(scan)
};
release.send(()).unwrap();
if let Some(scan) = scan {
assert!(scan.await.unwrap().is_empty());
}
assert!(next.await.unwrap().is_empty());
}
assert!(storage.lock.lock().await.synced.contains(&partition));
storage.lock.lock().await.sync_hook.fail = Some(Error::WriteFailed);
assert!(storage.scan("partition").await.unwrap().is_empty());
assert!(matches!(
storage.scan("other").await,
Err(Error::WriteFailed)
));
assert!(storage.scan("other").await.unwrap().is_empty());
assert!(matches!(
storage.scan("missing").await,
Err(Error::PartitionMissing(_))
));
assert_eq!(storage.lock.lock().await.synced.len(), 2);
drop(storage);
fs::remove_dir_all(directory).unwrap();
})
.await
.unwrap();
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_partition_scan_sync_failure() {
let directory =
env::temp_dir().join(format!("storage_tokio_scan_fail_{}", random_suffix()));
let partition = directory.join("partition");
fs::create_dir_all(&partition).unwrap();
let storage = Storage::new(Config::new(directory.clone(), Layout::ALL));
crate::storage::sync(&directory).unwrap();
storage.lock.lock().await.sync_hook.fail = Some(Error::WriteFailed);
assert!(matches!(
storage.scan("partition").await,
Err(Error::WriteFailed)
));
assert!(!storage.lock.lock().await.synced.contains(&partition));
assert!(storage.scan("partition").await.unwrap().is_empty());
assert!(storage.lock.lock().await.synced.contains(&partition));
drop(storage);
fs::remove_dir_all(directory).unwrap();
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_partition_open_syncs_once() {
let directory =
env::temp_dir().join(format!("storage_tokio_open_dir_sync_{}", random_suffix()));
let partition = directory.join("partition");
let config = Config::new(directory.clone(), Layout::ALL);
{
let storage = Storage::new(config.clone());
drop(storage.open("partition", b"blob").await.unwrap());
}
let storage = Storage::new(config);
crate::storage::sync(&directory).unwrap();
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, 0);
assert!(storage.lock.lock().await.synced.contains(&partition));
drop(blob);
storage.lock.lock().await.sync_hook.fail = Some(Error::WriteFailed);
drop(storage.open("partition", b"blob").await.unwrap());
assert!(storage.lock.lock().await.sync_hook.fail.is_some());
drop(storage);
fs::remove_dir_all(directory).unwrap();
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_partition_sync_creation_and_removal() {
let directory =
env::temp_dir().join(format!("storage_tokio_dir_lifecycle_{}", random_suffix()));
let created = directory.join("created");
let inherited = directory.join("inherited");
let retired = directory.join("retired");
let missing = directory.join("missing");
let config = Config::new(directory.clone(), Layout::ALL);
{
let storage = Storage::new(config.clone());
for partition in ["inherited", "retired"] {
drop(storage.open(partition, b"blob").await.unwrap());
}
}
let storage = Storage::new(config);
crate::storage::sync(&directory).unwrap();
drop(storage.open("created", b"blob").await.unwrap());
assert!(storage.lock.lock().await.synced.contains(&created));
assert_eq!(
storage.scan("created").await.unwrap(),
vec![b"blob".to_vec()]
);
storage.remove("inherited", Some(b"blob")).await.unwrap();
assert!(storage.lock.lock().await.synced.contains(&inherited));
assert!(storage.scan("inherited").await.unwrap().is_empty());
assert_eq!(
storage.scan("retired").await.unwrap(),
vec![b"blob".to_vec()]
);
storage.remove("retired", None).await.unwrap();
assert!(matches!(
storage.scan("retired").await,
Err(Error::PartitionMissing(_))
));
assert!(!storage.lock.lock().await.synced.contains(&retired));
drop(storage.open("retired", b"new").await.unwrap());
assert!(storage.lock.lock().await.synced.contains(&retired));
assert_eq!(
storage.scan("retired").await.unwrap(),
vec![b"new".to_vec()]
);
assert!(matches!(
storage.remove("missing", Some(b"blob")).await,
Err(Error::BlobMissing(_, _))
));
assert!(!storage.lock.lock().await.synced.contains(&missing));
drop(storage);
fs::remove_dir_all(directory).unwrap();
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_blob_remove_sync_failure_retires_partition() {
let directory =
env::temp_dir().join(format!("storage_tokio_remove_fail_{}", random_suffix()));
let partition = directory.join("partition");
let config = Config::new(directory.clone(), Layout::ALL);
{
let storage = Storage::new(config.clone());
drop(storage.open("partition", b"blob").await.unwrap());
}
let storage = Storage::new(config);
crate::storage::sync(&directory).unwrap();
assert_eq!(
storage.scan("partition").await.unwrap(),
vec![b"blob".to_vec()]
);
assert!(storage.lock.lock().await.synced.contains(&partition));
storage.lock.lock().await.sync_hook.fail = Some(Error::WriteFailed);
assert!(matches!(
storage.remove("partition", Some(b"blob")).await,
Err(Error::WriteFailed)
));
assert!(!partition.join(hex(b"blob")).exists());
assert!(!storage.lock.lock().await.synced.contains(&partition));
assert!(storage.scan("partition").await.unwrap().is_empty());
assert!(storage.lock.lock().await.synced.contains(&partition));
drop(storage);
fs::remove_dir_all(directory).unwrap();
}
async fn settle(storage: &Storage) {
while storage.pending.outstanding() > 0 {
tokio::task::yield_now().await;
}
}
#[tokio::test]
async fn test_reopen_flushes_dirty_predecessor() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_reopen_flush_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
drop(blob);
assert!(storage.pending.owes("partition", b"blob"));
assert_eq!(storage.pending.completions(), 0);
let (blob, len) = storage.open("partition", b"blob").await.unwrap();
assert!(!storage.pending.owes("partition", b"blob"));
assert_eq!(storage.pending.completions(), 1);
assert_eq!(len, 5);
let read = blob
.read_at(0, 5, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"hello");
drop(blob);
settle(&storage).await;
assert!(!storage.pending.owes("partition", b"blob"));
drop(storage.open("partition", b"blob").await.unwrap());
assert_eq!(storage.pending.completions(), 1);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_first_open_flushes_existing_blob() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_first_open_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
{
let storage = Storage::new(config.clone());
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::SYNC)
.await
.unwrap();
drop(blob);
settle(&storage).await;
assert_eq!(storage.pending.completions(), 0);
}
let storage = Storage::new(config);
let (blob, len) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(len, 5);
assert_eq!(storage.pending.completions(), 1);
drop(blob);
settle(&storage).await;
drop(storage.open("partition", b"blob").await.unwrap());
assert_eq!(storage.pending.completions(), 1);
drop(storage.open("partition", b"fresh").await.unwrap());
settle(&storage).await;
drop(storage.open("partition", b"fresh").await.unwrap());
assert_eq!(storage.pending.completions(), 1);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_partition_open_sync_failure_keeps_content_debt() {
let directory =
env::temp_dir().join(format!("storage_tokio_open_dir_fail_{}", random_suffix()));
let config = Config::new(directory.clone(), Layout::ALL);
{
let storage = Storage::new(config.clone());
drop(storage.open("partition", b"blob").await.unwrap());
settle(&storage).await;
}
let storage = Storage::new(config);
crate::storage::sync(&directory).unwrap();
storage.lock.lock().await.sync_hook.fail = Some(Error::WriteFailed);
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::WriteFailed)
));
assert!(storage.lock.lock().await.synced.is_empty());
assert_eq!(storage.pending.completions(), 0);
assert!(storage.pending.owes("partition", b"blob"));
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, 0);
assert!(
storage
.lock
.lock()
.await
.synced
.contains(&directory.join("partition"))
);
assert_eq!(storage.pending.completions(), 1);
assert!(!storage.pending.owes("partition", b"blob"));
drop(blob);
settle(&storage).await;
drop(storage);
fs::remove_dir_all(directory).unwrap();
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_remove_forgets_first_open_names() {
let storage_directory = env::temp_dir().join(format!(
"storage_tokio_first_open_remove_{}",
random_suffix()
));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
for (partition, name) in [("a", b"1"), ("a", b"2"), ("b", b"1")] {
drop(storage.open(partition, name).await.unwrap());
}
assert_eq!(storage.pending.flushed.lock().len(), 3);
storage.remove("a", Some(b"1")).await.unwrap();
assert!(
!storage
.pending
.flushed
.lock()
.contains(&("a".to_owned(), b"1".to_vec()))
);
drop(storage.open("a", b"1").await.unwrap());
drop(storage.open("a", b"1").await.unwrap());
assert_eq!(storage.pending.completions(), 0);
assert_eq!(storage.pending.flushed.lock().len(), 3);
storage.remove("a", None).await.unwrap();
assert_eq!(storage.pending.flushed.lock().len(), 1);
assert!(
storage
.pending
.flushed
.lock()
.contains(&("b".to_owned(), b"1".to_vec()))
);
storage.remove("b", None).await.unwrap();
assert!(storage.pending.flushed.lock().is_empty());
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[cfg(not(target_os = "linux"))]
#[tokio::test]
async fn test_first_open_flush_failure_is_retained() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_first_open_fail_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
{
let storage = Storage::new(config.clone());
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::SYNC)
.await
.unwrap();
drop(blob);
settle(&storage).await;
}
let storage = Storage::new(config);
*storage.pending.test.fail_flush.lock() = Some(Error::Closed);
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::Closed)
));
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::Closed)
));
assert_eq!(storage.pending.completions(), 0);
storage.remove("partition", Some(b"blob")).await.unwrap();
let (_, len) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(len, 0);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_synced_drop_leaves_no_debt() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_synced_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"sync").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
let (blob, _) = storage.open("partition", b"start_sync").await.unwrap();
blob.resize(16).await.unwrap();
blob.start_sync().await.await.unwrap();
drop(blob);
let (blob, _) = storage.open("partition", b"sync_write").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::SYNC)
.await
.unwrap();
drop(blob);
settle(&storage).await;
for name in [b"sync".as_slice(), b"start_sync", b"sync_write"] {
assert!(!storage.pending.owes("partition", name));
drop(storage.open("partition", name).await.unwrap());
}
assert_eq!(storage.pending.completions(), 0);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_shared_blob_dirty_state() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_shared_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let blob = Arc::new(blob);
let retained = Arc::clone(&blob);
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
drop(blob);
assert_eq!(storage.pending.outstanding(), 0);
assert!(!storage.pending.owes("partition", b"blob"));
retained.sync().await.unwrap();
drop(retained);
settle(&storage).await;
assert!(!storage.pending.owes("partition", b"blob"));
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let blob = Arc::new(blob);
let retained = Arc::clone(&blob);
blob.resize(0).await.unwrap();
drop(blob);
assert!(!storage.pending.owes("partition", b"blob"));
drop(retained);
settle(&storage).await;
assert!(storage.pending.owes("partition", b"blob"));
drop(storage.open("partition", b"blob").await.unwrap());
assert_eq!(storage.pending.completions(), 1);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recreate_reopen_flushes_replacement() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_recreate_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
shared::check_recreate_reopen(&storage, &storage.pending).await;
shared::check_recreate_clean(&storage, &storage.pending).await;
drop(storage);
let _ = std::fs::remove_dir_all(storage_directory);
}
#[tokio::test]
async fn test_failed_creation_does_not_publish_unsynced_header() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_creation_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
shared::check_failed_creation(&storage, &storage.pending).await;
drop(storage);
let _ = std::fs::remove_dir_all(storage_directory);
}
#[tokio::test]
async fn test_remove_live_dirty_owner_leaves_no_debt() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_remove_live_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
shared::check_remove_live_dirty_owner(&storage, &storage.pending).await;
drop(storage);
let _ = std::fs::remove_dir_all(storage_directory);
}
#[tokio::test]
async fn test_durable_writes_need_no_reopen_flush() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_durable_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
shared::check_sync_writes(&storage, &storage.pending).await;
drop(storage);
let _ = std::fs::remove_dir_all(storage_directory);
}
#[tokio::test]
async fn test_orphaned_write_lands_before_reopen() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_orphaned_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
shared::check_orphaned_write(&storage).await;
drop(storage);
let _ = std::fs::remove_dir_all(storage_directory);
}
#[tokio::test]
async fn test_untouched_creation_needs_no_reopen_flush() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_eager_creation_{}", random_suffix()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
shared::check_untouched_creation_leaves_no_debt(&storage, &storage.pending).await;
drop(storage);
std::fs::remove_dir_all(storage_directory).unwrap();
}
#[tokio::test]
async fn test_clean_sync_is_skipped() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_clean_sync_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.sync().await.unwrap();
blob.start_sync().await.await.unwrap();
assert_eq!(blob.skipped_syncs(), 2);
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
assert_eq!(blob.skipped_syncs(), 2);
blob.sync().await.unwrap();
blob.resize(3).await.unwrap();
blob.start_sync().await.await.unwrap();
blob.start_sync().await.await.unwrap();
assert_eq!(blob.skipped_syncs(), 4);
drop(blob);
settle(&storage).await;
assert!(!storage.pending.owes("partition", b"blob"));
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_flush_failure_is_retained() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_flush_failure_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
*storage.pending.test.fail_flush.lock() = Some(Error::Closed);
assert!(matches!(blob.sync().await, Err(Error::Closed)));
assert!(matches!(blob.sync().await, Err(Error::Closed)));
assert!(matches!(blob.start_sync().await.await, Err(Error::Closed)));
drop(blob);
for _ in 0..2 {
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::Closed)
));
}
storage.remove("partition", Some(b"blob")).await.unwrap();
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, 0);
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_durable_write_retains_prior_flush_failure() {
let storage_directory = env::temp_dir().join(format!(
"storage_tokio_durable_write_failure_{}",
random_suffix()
));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"before", WriteOptions::default())
.await
.unwrap();
*storage.pending.test.fail_flush.lock() = Some(Error::Closed);
assert!(matches!(blob.sync().await, Err(Error::Closed)));
let result = blob.write_at(6, b"after", WriteOptions::SYNC).await;
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(storage_directory).unwrap();
assert!(matches!(result, Err(Error::Closed)), "{result:?}");
}
#[tokio::test]
async fn test_completion_failure_is_retained() {
let storage_directory = env::temp_dir().join(format!(
"storage_tokio_completion_failure_{}",
random_suffix()
));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
drop(blob);
*storage.pending.test.fail_flush.lock() = Some(Error::WriteFailed);
for _ in 0..2 {
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::WriteFailed)
));
}
assert_eq!(storage.pending.completions(), 0);
storage.remove("partition", None).await.unwrap();
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, 0);
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_cancelled_completion_failure_is_retained() {
let storage_directory = env::temp_dir().join(format!(
"storage_tokio_cancelled_completion_{}",
random_suffix()
));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
drop(blob);
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*storage.pending.test.before_complete.lock() = Some((entered, gate));
let mut cancelled = Box::pin(storage.open("partition", b"blob"));
commonware_macros::select! {
entered = entering => entered.unwrap(),
_ = &mut cancelled => panic!("reopen must block in its flush"),
}
drop(cancelled);
let mut successor = Box::pin(storage.open("partition", b"blob"));
assert!((&mut successor).now_or_never().is_none());
storage.scan("partition").await.unwrap();
*storage.pending.test.fail_flush.lock() = Some(Error::WriteFailed);
release.send(()).unwrap();
assert!(matches!(successor.await, Err(Error::WriteFailed)));
assert_eq!(storage.pending.completions(), 0);
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::WriteFailed)
));
storage.remove("partition", None).await.unwrap();
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_removed_incarnation_completion_publishes_nothing() {
let storage_directory = env::temp_dir().join(format!(
"storage_tokio_removed_completion_{}",
random_suffix()
));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
let (old, _) = storage.open("partition", b"blob").await.unwrap();
old.write_at(0, b"old", WriteOptions::default())
.await
.unwrap();
drop(old);
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*storage.pending.test.before_complete.lock() = Some((entered, gate));
let mut reopen = Box::pin(storage.open("partition", b"blob"));
commonware_macros::select! {
entered = entering => entered.unwrap(),
_ = &mut reopen => panic!("reopen must block in its flush"),
}
storage.remove("partition", Some(b"blob")).await.unwrap();
let (current, len) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(len, 0);
current
.write_at(0, b"new", WriteOptions::default())
.await
.unwrap();
drop(current);
release.send(()).unwrap();
let (removed, size) = reopen.await.unwrap();
assert_eq!(size, 3);
assert_eq!(
removed
.read_at(0, 3, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"old"
);
drop(removed);
assert!(storage.pending.owes("partition", b"blob"));
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, 3);
assert_eq!(
blob.read_at(0, 3, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"new"
);
assert_eq!(storage.pending.completions(), 2);
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_remove_forgets_debt() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_forget_{}", random_suffix()));
let config = Config::new(storage_directory.clone(), Layout::ALL);
let storage = Storage::new(config);
for name in [b"a".as_slice(), b"b".as_slice()] {
let (blob, _) = storage.open("partition", name).await.unwrap();
blob.write_at(0, b"hello", WriteOptions::default())
.await
.unwrap();
drop(blob);
assert!(storage.pending.owes("partition", name));
}
storage.remove("partition", Some(b"a")).await.unwrap();
assert!(!storage.pending.owes("partition", b"a"));
assert!(storage.pending.owes("partition", b"b"));
storage.remove("partition", None).await.unwrap();
assert!(!storage.pending.owes("partition", b"b"));
assert!(storage.pending.entries.lock().is_empty());
let _ = std::fs::remove_dir_all(&storage_directory);
}
}