use super::{Header, Layout, hold::Hold, resolve_header, sync_dir};
use crate::{
BlobVersion, Buf, BufferPool, Error, Handle, IoBufs, IoBufsMut, ReadOptions, WriteOptions,
iouring::{self},
telemetry::metrics::Register,
utils,
};
use commonware_formatting::{from_hex, hex};
use commonware_utils::sync::Mutex;
use std::{
fs::{self, File},
io::{Error as IoError, Seek, SeekFrom, Write},
ops::RangeInclusive,
path::PathBuf,
sync::{Arc, atomic::AtomicBool},
};
#[derive(Clone, Debug)]
pub struct Config {
pub storage_directory: PathBuf,
pub blob_layouts: RangeInclusive<Layout>,
pub iouring_config: iouring::Config,
pub thread_stack_size: usize,
}
#[derive(Clone)]
pub struct Storage {
lock: Arc<Mutex<()>>,
storage_directory: PathBuf,
blob_layouts: RangeInclusive<Layout>,
io_handle: iouring::Handle,
pool: BufferPool,
}
impl Storage {
pub(crate) fn start(cfg: Config, registry: &mut impl Register, pool: BufferPool) -> Self {
let Config {
storage_directory,
blob_layouts,
mut iouring_config,
thread_stack_size,
} = cfg;
iouring_config.single_issuer = true;
iouring_config.shutdown_timeout = None;
let (io_handle, iouring_loop) = iouring::IoUringLoop::new(iouring_config, registry);
let hold = Hold::acquire(&storage_directory).unwrap_or_else(|e| {
panic!(
"failed to acquire storage directory hold ({}): {e}",
storage_directory.display()
)
});
let storage = Self {
lock: Arc::new(Mutex::new(())),
storage_directory,
blob_layouts,
io_handle,
pool,
};
utils::thread::spawn(thread_stack_size, move || {
let _hold = hold;
iouring_loop.run()
});
storage
}
}
impl crate::Storage for Storage {
type Blob = Blob;
async fn open_versioned(
&self,
partition: &str,
name: &[u8],
versions: RangeInclusive<BlobVersion>,
) -> Result<(Blob, u64, BlobVersion), Error> {
super::validate_partition_name(partition)?;
let _guard = self.lock.lock();
let path = self.storage_directory.join(partition).join(hex(name));
let parent = path
.parent()
.ok_or_else(|| Error::PartitionMissing(partition.into()))?;
fs::create_dir_all(parent).map_err(|_| Error::PartitionCreationFailed(partition.into()))?;
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.map_err(|e| Error::BlobOpenFailed(partition.into(), hex(name), e.into()))?;
let raw_len = file.metadata().map_err(|_| Error::ReadFailed)?.len();
let existing = resolve_header(
&mut file,
raw_len,
&self.blob_layouts,
&versions,
partition,
name,
)?;
let (logical_len, blob_version, data_offset) = match existing {
Some(resolved) => resolved,
None => {
sync_dir(parent)?;
sync_dir(&self.storage_directory)?;
let (region, blob_version) = Header::create(&self.blob_layouts, &versions);
let data_offset = region.len() as u64;
file.set_len(0)
.map_err(|e| Error::BlobResizeFailed(partition.into(), hex(name), e.into()))?;
file.seek(SeekFrom::Start(0))
.map_err(|_| Error::WriteFailed)?;
file.write_all(®ion).map_err(|_| Error::WriteFailed)?;
file.sync_all()
.map_err(|e| Error::BlobSyncFailed(partition.into(), hex(name), e.into()))?;
(0, blob_version, data_offset)
}
};
let blob = Blob::new(
partition.into(),
name,
file,
self.io_handle.clone(),
self.pool.clone(),
data_offset,
);
Ok((blob, logical_len, blob_version))
}
async fn remove(&self, partition: &str, name: Option<&[u8]>) -> Result<(), Error> {
super::validate_partition_name(partition)?;
let _guard = self.lock.lock();
let path = self.storage_directory.join(partition);
if let Some(name) = name {
let blob_path = path.join(hex(name));
fs::remove_file(blob_path)
.map_err(|_| Error::BlobMissing(partition.into(), hex(name)))?;
sync_dir(&path)?;
} else {
fs::remove_dir_all(&path).map_err(|_| Error::PartitionMissing(partition.into()))?;
sync_dir(&self.storage_directory)?;
}
Ok(())
}
async fn scan(&self, partition: &str) -> Result<Vec<Vec<u8>>, Error> {
super::validate_partition_name(partition)?;
let _guard = self.lock.lock();
let path = self.storage_directory.join(partition);
let entries =
std::fs::read_dir(&path).map_err(|_| Error::PartitionMissing(partition.into()))?;
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.into()));
}
if let Some(name) = entry.file_name().to_str() {
let decoded = from_hex(name).ok_or(Error::PartitionCorrupt(partition.into()))?;
if hex(&decoded) != name {
return Err(Error::PartitionCorrupt(partition.into()));
}
blobs.push(decoded);
}
}
Ok(blobs)
}
}
pub struct Blob {
partition: String,
name: Vec<u8>,
file: Arc<File>,
io_handle: iouring::Handle,
pool: BufferPool,
data_offset: u64,
dont_cache_supported: Arc<AtomicBool>,
}
impl Clone for Blob {
fn clone(&self) -> Self {
Self {
partition: self.partition.clone(),
name: self.name.clone(),
file: self.file.clone(),
io_handle: self.io_handle.clone(),
pool: self.pool.clone(),
data_offset: self.data_offset,
dont_cache_supported: self.dont_cache_supported.clone(),
}
}
}
impl Blob {
fn new(
partition: String,
name: &[u8],
file: File,
io_handle: iouring::Handle,
pool: BufferPool,
data_offset: u64,
) -> Self {
Self {
partition,
name: name.to_vec(),
file: Arc::new(file),
io_handle,
pool,
data_offset,
dont_cache_supported: Arc::new(AtomicBool::new(true)),
}
}
}
impl crate::Blob for Blob {
async fn read_at(
&self,
offset: u64,
len: usize,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
self.read_at_buf(offset, len, self.pool.alloc(len), options)
.await
}
async fn read_at_buf(
&self,
offset: u64,
len: usize,
bufs: impl Into<IoBufsMut> + Send,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
let mut input_bufs = bufs.into();
unsafe { input_bufs.set_len(len) };
let (io_buf, original_bufs) = if input_bufs.is_single() {
(input_bufs.coalesce(), None)
} else {
let tmp = unsafe { self.pool.alloc_len(len) };
(tmp, Some(input_bufs))
};
let offset = offset
.checked_add(self.data_offset)
.ok_or(Error::OffsetOverflow)?;
if len == 0 {
return Ok(original_bufs.unwrap_or_else(|| io_buf.into()));
}
let cache = if options.contains(ReadOptions::DONT_CACHE) {
iouring::Cache::Disabled(self.dont_cache_supported.clone())
} else {
iouring::Cache::Enabled
};
let io_buf = self
.io_handle
.read_at(self.file.clone(), offset, len, io_buf, cache)
.await
.map_err(|(_, err)| err)?;
match original_bufs {
None => Ok(io_buf.into()),
Some(mut bufs) => {
bufs.copy_from_slice(io_buf.as_ref());
Ok(bufs)
}
}
}
async fn write_at(
&self,
offset: u64,
bufs: impl Into<IoBufs> + Send,
options: WriteOptions,
) -> Result<(), Error> {
let bufs = bufs.into();
let offset = offset
.checked_add(self.data_offset)
.ok_or(Error::OffsetOverflow)?;
if !bufs.has_remaining() {
return Ok(());
}
let cache = if options.contains(WriteOptions::DONT_CACHE) {
iouring::Cache::Disabled(self.dont_cache_supported.clone())
} else {
iouring::Cache::Enabled
};
self.io_handle
.write_at(self.file.clone(), offset, bufs, options, cache)
.await
}
async fn resize(&self, len: u64) -> Result<(), Error> {
let len = len
.checked_add(self.data_offset)
.ok_or(Error::OffsetOverflow)?;
self.file.set_len(len).map_err(|e| {
Error::BlobResizeFailed(
self.partition.clone(),
hex(&self.name),
IoError::other(e).into(),
)
})
}
async fn sync(&self) -> Result<(), Error> {
self.io_handle
.sync(self.file.clone())
.await
.map_err(|err| match err {
Error::Io(e) => Error::BlobSyncFailed(self.partition.clone(), hex(&self.name), e),
err => err,
})
}
async fn start_sync(&self) -> Handle<()> {
let partition = self.partition.clone();
let name = self.name.clone();
let receiver = self.io_handle.start_sync(self.file.clone()).await;
Handle::from_future(async move {
match receiver.await {
Ok(Ok(())) => Ok(()),
Ok(Err(Error::Io(e))) => Err(Error::BlobSyncFailed(partition, hex(&name), e)),
Ok(Err(err)) => Err(err),
Err(_) => Err(Error::Closed),
}
})
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::{Header, *};
use crate::{
Blob as _, BufferPool, BufferPoolConfig, IoBuf, IoBufMut, Storage as _,
storage::{Layout, tests::run_storage_tests},
telemetry::metrics::Registry,
utils::thread,
};
use std::{
env,
ffi::OsString,
os::{
fd::{FromRawFd, IntoRawFd},
unix::{ffi::OsStringExt, net::UnixStream},
},
sync::atomic::{AtomicU64, Ordering},
};
static NEXT_STORAGE_TEST_DIR: AtomicU64 = AtomicU64::new(0);
fn test_pool(scope: &mut impl Register) -> BufferPool {
BufferPool::new(BufferPoolConfig::for_storage(), scope)
}
fn create_test_storage() -> (Storage, PathBuf) {
let storage_directory = env::temp_dir().join(format!(
"commonware_iouring_storage_{}_{}",
std::process::id(),
NEXT_STORAGE_TEST_DIR.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&storage_directory);
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let storage = Storage::start(
Config {
storage_directory: storage_directory.clone(),
blob_layouts: Layout::ALL,
iouring_config: Default::default(),
thread_stack_size: thread::system_thread_stack_size(),
},
&mut registry.sub_registry("storage"),
pool,
);
(storage, storage_directory)
}
fn create_test_directory() -> PathBuf {
let storage_directory = env::temp_dir().join(format!(
"commonware_iouring_storage_{}_{}",
std::process::id(),
NEXT_STORAGE_TEST_DIR.fetch_add(1, Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&storage_directory);
std::fs::create_dir_all(&storage_directory).unwrap();
storage_directory
}
#[tokio::test]
async fn test_hold_retained_by_open_blob() {
let (storage, storage_directory) = create_test_storage();
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
drop(storage);
let dir = storage_directory.clone();
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let second = Storage::start(
Config {
storage_directory: dir,
blob_layouts: Layout::ALL,
iouring_config: Default::default(),
thread_stack_size: thread::system_thread_stack_size(),
},
&mut registry.sub_registry("storage"),
pool,
);
tx.send(()).unwrap();
drop(second);
});
match rx.recv_timeout(std::time::Duration::from_millis(200)) {
Err(std::sync::mpsc::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);
}
#[tokio::test]
async fn test_v1_paged_alignment() {
let (storage, storage_directory) = create_test_storage();
const PHYSICAL_PAGE_SIZE: u64 = 4096;
let logical = crate::buffer::paged::page_size(PHYSICAL_PAGE_SIZE as u32);
let mut registry = Registry::default();
let cache = crate::buffer::paged::CacheRef::new(
test_pool(&mut registry.sub_registry("pool")),
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.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 pages = (logical_size as usize).div_ceil(logical.get() as usize);
assert_eq!(raw.len() as u64 % PHYSICAL_PAGE_SIZE, 0);
assert_eq!(
raw.len() as u64,
Layout::V1.data_offset() + pages as u64 * PHYSICAL_PAGE_SIZE
);
for page in 0..pages {
let start = Layout::V1.data_offset() as usize + page * PHYSICAL_PAGE_SIZE as usize;
let physical = &raw[start..start + PHYSICAL_PAGE_SIZE as usize];
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_iouring_storage() {
let (storage, storage_directory) = create_test_storage();
run_storage_tests(storage).await;
let _ = std::fs::remove_dir_all(storage_directory);
}
#[tokio::test]
async fn test_blob_header_handling() {
let (storage, storage_directory) = create_test_storage();
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.to_vec(), 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()
.coalesce();
assert_eq!(read_buf, 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".to_vec(), 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()
.coalesce();
assert_eq!(read_buf, 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_blob_magic_mismatch() {
let (storage, storage_directory) = create_test_storage();
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 err = storage
.open("partition", b"bad_magic")
.await
.err()
.expect("bad magic should fail");
assert!(
err.to_string()
.starts_with("blob corrupt: partition/6261645f6d61676963 reason: invalid magic")
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_partial_header_reset() {
let (storage, storage_directory) = create_test_storage();
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_vectored_write_partial_progress() {
let (storage, storage_directory) = create_test_storage();
let (blob, _) = storage.open("partition", b"vectest").await.unwrap();
blob.resize(200).await.unwrap();
let mut bufs = crate::IoBufs::default();
bufs.append(crate::IoBuf::from(vec![0xAAu8; 80]));
bufs.append(crate::IoBuf::from(vec![0xBBu8; 80]));
blob.write_at(0, bufs, WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let data = blob
.read_at(0, 160, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(&data.as_ref()[..80], &[0xAAu8; 80]);
assert_eq!(&data.as_ref()[80..], &[0xBBu8; 80]);
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_read_at_reports_eof_when_blob_is_too_short() {
let (storage, storage_directory) = create_test_storage();
let (blob, _) = storage.open("partition", b"short").await.unwrap();
blob.write_at(0, b"abc".to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let err = blob
.read_at(0, 5, ReadOptions::DONT_CACHE)
.await
.unwrap_err();
assert_eq!(err.to_string(), "blob insufficient length");
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_read_at_buf_preserves_multichunk_layout() {
let (storage, storage_directory) = create_test_storage();
let (blob, _) = storage.open("partition", b"multichunk").await.unwrap();
blob.write_at(0, b"hello world".to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let bufs = IoBufsMut::from(vec![IoBufMut::with_capacity(5), IoBufMut::with_capacity(6)]);
let read = blob
.read_at_buf(0, 11, bufs, ReadOptions::DONT_CACHE)
.await
.unwrap();
assert!(!read.is_single());
assert_eq!(read.coalesce(), b"hello world");
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_zero_length_read_and_write_short_circuit() {
let (storage, storage_directory) = create_test_storage();
let (blob, size) = storage.open("partition", b"empty").await.unwrap();
assert_eq!(size, 0);
blob.write_at(0, IoBufs::default(), WriteOptions::default())
.await
.unwrap();
blob.write_at(0, IoBuf::default(), WriteOptions::default())
.await
.unwrap();
blob.write_at(0, Vec::<u8>::new(), WriteOptions::default())
.await
.unwrap();
let empty = blob.read_at(1, 0, ReadOptions::DONT_CACHE).await.unwrap();
assert!(empty.is_empty());
let _ = blob
.read_at_buf(
0,
0,
IoBufsMut::from(IoBufMut::with_capacity(8)),
ReadOptions::DONT_CACHE,
)
.await
.unwrap();
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_scan_rejects_non_file_entries() {
let (storage, storage_directory) = create_test_storage();
let partition = storage_directory.join("partition");
std::fs::create_dir_all(partition.join("nested")).unwrap();
let err = storage.scan("partition").await.unwrap_err();
assert_eq!(err.to_string(), "partition corrupt: partition");
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_remove_reports_missing_targets() {
let (storage, storage_directory) = create_test_storage();
let err = storage.remove("missing", None).await.unwrap_err();
assert_eq!(err.to_string(), "partition missing: missing");
std::fs::create_dir_all(storage_directory.join("partition")).unwrap();
let err = storage
.remove("partition", Some(b"missing"))
.await
.unwrap_err();
assert_eq!(err.to_string(), "blob missing: partition/6d697373696e67");
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_scan_ignores_non_utf8_file_names() {
let (storage, storage_directory) = create_test_storage();
let partition = storage_directory.join("partition");
std::fs::create_dir_all(&partition).unwrap();
let invalid_name = OsString::from_vec(vec![0xff, 0xfe, 0xfd]);
std::fs::write(partition.join(invalid_name), []).unwrap();
let scanned = storage.scan("partition").await.unwrap();
assert!(scanned.is_empty());
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_scan_rejects_non_hex_file_names() {
let (storage, storage_directory) = create_test_storage();
let partition = storage_directory.join("partition");
std::fs::create_dir_all(&partition).unwrap();
std::fs::write(partition.join("not-hex"), []).unwrap();
let err = storage.scan("partition").await.unwrap_err();
assert_eq!(err.to_string(), "partition corrupt: partition");
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, storage_directory) = create_test_storage();
let partition = storage_directory.join("partition");
std::fs::create_dir_all(&partition).unwrap();
std::fs::write(partition.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_open_reports_partition_creation_failure() {
let storage_directory = create_test_directory();
std::fs::write(storage_directory.join("partition"), b"not a directory").unwrap();
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let storage = Storage::start(
Config {
storage_directory: storage_directory.clone(),
blob_layouts: Layout::ALL,
iouring_config: Default::default(),
thread_stack_size: utils::thread::system_thread_stack_size(),
},
&mut registry.sub_registry("storage"),
pool,
);
let err = storage
.open("partition", b"blob")
.await
.err()
.expect("occupied partition path should fail");
assert_eq!(err.to_string(), "partition creation failed: partition");
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_open_reports_blob_open_failure_for_directory_path() {
let storage_directory = create_test_directory();
let partition = storage_directory.join("partition");
let blob_name = hex(b"blob");
std::fs::create_dir_all(partition.join(&blob_name)).unwrap();
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let storage = Storage::start(
Config {
storage_directory: storage_directory.clone(),
blob_layouts: Layout::ALL,
iouring_config: Default::default(),
thread_stack_size: utils::thread::system_thread_stack_size(),
},
&mut registry.sub_registry("storage"),
pool,
);
let err = storage
.open("partition", b"blob")
.await
.err()
.expect("opening a directory as a blob should fail");
assert!(
err.to_string()
.starts_with(&format!("blob open failed: partition/{blob_name} error:"))
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_offset_overflow_guards() {
let (storage, storage_directory) = create_test_storage();
let (blob, _) = storage.open("partition", b"overflow").await.unwrap();
assert_eq!(
blob.read_at(u64::MAX, 1, ReadOptions::default())
.await
.unwrap_err()
.to_string(),
"offset overflow"
);
assert_eq!(
blob.read_at(u64::MAX, 0, ReadOptions::DONT_CACHE)
.await
.unwrap_err()
.to_string(),
"offset overflow"
);
assert_eq!(
blob.read_at(i64::MAX as u64, 1, ReadOptions::DONT_CACHE)
.await
.unwrap_err()
.to_string(),
"read failed"
);
assert_eq!(
blob.write_at(u64::MAX, b"x".to_vec(), WriteOptions::default())
.await
.unwrap_err()
.to_string(),
"offset overflow"
);
assert_eq!(
blob.resize(u64::MAX).await.unwrap_err().to_string(),
"offset overflow"
);
drop(blob);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_read_and_write_report_handle_disconnect() {
let storage_directory = create_test_directory();
let path = storage_directory.join("disconnected");
let file = File::create(&path).unwrap();
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let (submitter, io_loop) = iouring::IoUringLoop::new(
iouring::Config::default(),
&mut registry.sub_registry("iouring"),
);
drop(io_loop);
let blob = Blob::new(
"partition".into(),
b"blob",
file,
submitter,
pool,
Layout::V0.data_offset(),
);
let empty = blob.read_at(0, 0, ReadOptions::DONT_CACHE).await.unwrap();
assert!(empty.is_empty());
assert!(
blob.dont_cache_supported
.load(std::sync::atomic::Ordering::Relaxed)
);
assert_eq!(
blob.read_at(0, 1, ReadOptions::default())
.await
.unwrap_err()
.to_string(),
"read failed"
);
assert_eq!(
blob.write_at(0, b"x".to_vec(), WriteOptions::default())
.await
.unwrap_err()
.to_string(),
"write failed"
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_sync_dir_reports_missing_directory() {
let storage_directory = create_test_directory();
let missing = storage_directory.join("missing");
let err = sync_dir(&missing).expect_err("missing directory should fail");
assert!(err.to_string().starts_with(&format!(
"blob open failed: {}/directory error:",
missing.to_string_lossy()
)));
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_sync_reports_handle_disconnect() {
let storage_directory = create_test_directory();
let path = storage_directory.join("disconnected");
let file = File::create(&path).unwrap();
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let (submitter, io_loop) = iouring::IoUringLoop::new(
iouring::Config::default(),
&mut registry.sub_registry("iouring"),
);
drop(io_loop);
let blob = Blob::new(
"partition".into(),
b"blob",
file,
submitter,
pool,
Layout::V0.data_offset(),
);
let err = blob
.sync()
.await
.expect_err("sync should fail without a loop");
assert_eq!(
err.to_string(),
format!(
"blob sync failed: partition/{} error: failed to send work",
hex(b"blob")
)
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_start_sync_reports_handle_disconnect() {
let storage_directory = create_test_directory();
let path = storage_directory.join("disconnected_start_sync");
let file = File::create(&path).unwrap();
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let (submitter, io_loop) = iouring::IoUringLoop::new(
iouring::Config::default(),
&mut registry.sub_registry("iouring"),
);
drop(io_loop);
let blob = Blob::new(
"partition".into(),
b"blob",
file,
submitter,
pool,
Layout::V0.data_offset(),
);
let err = blob
.start_sync()
.await
.await
.expect_err("start_sync should fail without a loop");
assert_eq!(
err.to_string(),
format!(
"blob sync failed: partition/{} error: failed to send work",
hex(b"blob")
)
);
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_resize_reports_kernel_error() {
let storage_directory = create_test_directory();
let (socket, _peer) = UnixStream::pair().unwrap();
let file = unsafe { File::from_raw_fd(socket.into_raw_fd()) };
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let (submitter, io_loop) = iouring::IoUringLoop::new(
iouring::Config::default(),
&mut registry.sub_registry("iouring"),
);
drop(io_loop);
let blob = Blob::new(
"partition".into(),
b"blob",
file,
submitter,
pool,
Layout::V0.data_offset(),
);
let err = blob
.resize(0)
.await
.expect_err("resize should fail on a socket fd");
assert!(err.to_string().starts_with(&format!(
"blob resize failed: partition/{} error:",
hex(b"blob")
)));
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_sync_reports_kernel_error() {
let storage_directory = create_test_directory();
let (socket, _peer) = UnixStream::pair().unwrap();
let file = unsafe { File::from_raw_fd(socket.into_raw_fd()) };
let mut registry = Registry::default();
let pool = test_pool(&mut registry.sub_registry("pool"));
let (submitter, io_loop) = iouring::IoUringLoop::new(
iouring::Config::default(),
&mut registry.sub_registry("iouring"),
);
let handle = std::thread::spawn(move || io_loop.run());
let blob = Blob::new(
"partition".into(),
b"blob",
file,
submitter.clone(),
pool,
Layout::V0.data_offset(),
);
let err = blob
.sync()
.await
.expect_err("sync should fail on a socket fd");
let message = err.to_string();
assert!(message.starts_with(&format!(
"blob sync failed: partition/{} error:",
hex(b"blob")
)));
assert_ne!(
message,
format!(
"blob sync failed: partition/{} error: failed to send work",
hex(b"blob")
)
);
drop(blob);
drop(submitter);
handle.join().unwrap();
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_torn_creation_recovers() {
let (storage, storage_directory) = create_test_storage();
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 states = [region[..10].to_vec()];
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.sync().await.unwrap();
drop(blob);
let (blob, size) = storage.open("partition", b"torn").await.unwrap();
assert_eq!(size, 0);
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(Error::BlobCorrupt(_, _, _))));
let _ = std::fs::remove_dir_all(&storage_directory);
}
#[tokio::test]
async fn test_blob_v1_rejects_nonzero_header_padding() {
let (storage, storage_directory) = create_test_storage();
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(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, storage_directory) = create_test_storage();
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);
}
}