use crate::{
Buf, Error, Handle, IoBufMut, IoBufs, IoBufsMut, ReadOptions, WriteOptions,
storage::{
Generation, Pending, Sender, Tracker,
hold::{Held, Hold},
},
};
use cfg_if::cfg_if;
use commonware_formatting::hex;
use commonware_utils::{
Widen,
channel::oneshot,
sync::{Mutex, MutexGuard},
};
use futures::{StreamExt as _, stream};
#[cfg(test)]
use std::sync::mpsc;
use std::{
alloc,
fs::File,
io::IoSlice,
ops::Deref,
os::{fd::AsRawFd, unix::fs::FileExt},
sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering},
},
};
use tokio::task;
const IOVEC_BATCH_SIZE: usize = 1024;
#[cfg(target_os = "linux")]
mod aio;
enum Cache {
Enabled,
Disabled,
}
impl Cache {
fn is_disabled(&self, supported: &AtomicBool) -> bool {
cfg!(target_os = "linux")
&& matches!(self, Self::Disabled)
&& supported.load(Ordering::Relaxed)
}
fn retry_cached(
&mut self,
supported: &AtomicBool,
err: &std::io::Error,
attempted_dont_cache: bool,
) -> bool {
if err.raw_os_error() != Some(libc::EOPNOTSUPP) || !attempted_dont_cache {
return false;
}
let Self::Disabled = std::mem::replace(self, Self::Enabled) else {
return false;
};
supported.store(false, Ordering::Relaxed);
true
}
}
struct Shared {
file: Held,
tracker: Tracker,
durability: Mutex<()>,
pending: Arc<Pending>,
key: (String, Vec<u8>),
promise: OnceLock<Sender>,
dont_cache_supported: AtomicBool,
#[cfg(target_os = "linux")]
direct: once_cell::sync::OnceCell<Option<File>>,
#[cfg(test)]
test: Hooks,
}
#[cfg(test)]
#[derive(Default)]
struct Hooks {
before_mutation: Mutex<Option<(oneshot::Sender<()>, mpsc::Receiver<()>)>>,
after_sync: Mutex<Option<(oneshot::Sender<()>, mpsc::Receiver<()>)>>,
after_start_sync: Mutex<Option<(oneshot::Sender<()>, mpsc::Receiver<()>)>>,
#[cfg(target_os = "linux")]
direct_reads: std::sync::atomic::AtomicUsize,
}
#[cfg(test)]
impl Shared {
fn wait_before_mutation(&self) {
let hook = self.test.before_mutation.lock().take();
if let Some((entered, release)) = hook {
let _ = entered.send(());
let _ = release.recv();
}
}
fn wait_after_sync(&self) {
let hook = self.test.after_sync.lock().take();
if let Some((entered, release)) = hook {
let _ = entered.send(());
let _ = release.recv();
}
}
}
impl Deref for Shared {
type Target = File;
fn deref(&self) -> &File {
&self.file
}
}
impl Drop for Shared {
fn drop(&mut self) {
let Some(sender) = self.promise.take() else {
return;
};
self.pending.settle(
&self.key,
sender,
self.tracker.is_dirty(),
self.tracker.failure(),
);
}
}
impl Shared {
fn durability(&self) -> Result<MutexGuard<'_, ()>, Error> {
let guard = self.durability.lock();
if let Some(error) = self.tracker.failure() {
return Err(error);
}
Ok(guard)
}
fn barrier(&self) -> Result<(), Error> {
cfg_if! {
if #[cfg(target_os = "linux")] {
let result = self.file.sync_data();
} else {
let result = self.file.sync_all();
}
}
let (partition, name) = &self.key;
let result =
result.map_err(|e| Error::BlobSyncFailed(partition.clone(), hex(name), e.into()));
#[cfg(test)]
let result = {
let result = self.pending.take_flush_failure().map_or(result, Err);
self.wait_after_sync();
result
};
result
}
fn flush(&self, seen: u64) -> Result<(), Error> {
let _durability = self.durability()?;
match self.barrier() {
Ok(()) => self.tracker.end_sync(seen),
Err(error) => {
self.tracker.poison(&error);
Err(error)
}
}
}
fn complete(&self) -> Result<(), Error> {
#[cfg(test)]
self.pending.before_complete();
let result = self.barrier();
if let Err(error) = &result {
self.tracker.poison(error);
}
#[cfg(test)]
if result.is_ok() {
self.pending.completed();
}
result
}
}
pub struct Blob {
shared: Arc<Shared>,
generation: Arc<Generation>,
data_offset: u64,
}
impl Drop for Blob {
fn drop(&mut self) {
if let Some(sender) = self.generation.release() {
let _ = self.shared.promise.set(sender);
}
}
}
impl Blob {
pub(super) async fn complete(&self) -> Result<u64, Error> {
let shared = self.shared.clone();
let identity = Arc::downgrade(&self.generation);
let offset = self.data_offset;
task::spawn_blocking(move || {
#[cfg(test)]
{
let hook = shared.pending.test.before_metadata.lock().take();
if let Some((entered, release)) = hook {
let _ = entered.send(());
let _ = release.recv();
}
}
if shared.pending.debt(&shared.key, &identity)? {
let result = shared.complete();
shared.pending.clear(&shared.key, &identity, &result);
result?;
}
shared
.metadata()
.map(|metadata| metadata.len() - offset)
.map_err(|_| Error::ReadFailed)
})
.await
.map_err(|_| Error::ReadFailed)?
}
pub(crate) fn new(
file: File,
data_offset: u64,
hold: Arc<Hold>,
generation: Arc<Generation>,
) -> Self {
let shared = Arc::new(Shared {
file: Held::new(file, hold),
tracker: Tracker::default(),
durability: Mutex::new(()),
pending: generation.pending.clone(),
key: generation.key.clone(),
promise: OnceLock::new(),
dont_cache_supported: AtomicBool::new(true),
#[cfg(target_os = "linux")]
direct: once_cell::sync::OnceCell::new(),
#[cfg(test)]
test: Hooks::default(),
});
Self {
shared,
generation,
data_offset,
}
}
#[cfg(test)]
pub(super) fn skipped_syncs(&self) -> u64 {
self.shared.tracker.skipped()
}
#[cfg(target_os = "linux")]
fn read_exact_at(
mut cache: Cache,
file: &Shared,
mut buf: &mut [u8],
mut offset: u64,
) -> Result<(), Error> {
if !cache.is_disabled(&file.dont_cache_supported) {
file.read_exact_at(buf, offset)?;
return Ok(());
}
while !buf.is_empty() {
let iovec = libc::iovec {
iov_base: buf.as_mut_ptr().cast(),
iov_len: buf.len(),
};
let ret = unsafe {
libc::preadv2(
file.as_raw_fd(),
&iovec,
1,
offset.try_into().map_err(|_| Error::OffsetOverflow)?,
libc::RWF_DONTCACHE,
)
};
if ret < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
if cache.retry_cached(&file.dont_cache_supported, &err, true) {
file.read_exact_at(buf, offset)?;
return Ok(());
}
return Err(err.into());
}
let bytes_read = ret as usize;
if bytes_read == 0 {
return Err(std::io::Error::from(std::io::ErrorKind::UnexpectedEof).into());
}
let (_, unread) = buf.split_at_mut(bytes_read);
buf = unread;
offset = offset
.checked_add(bytes_read as u64)
.ok_or(Error::OffsetOverflow)?;
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
fn read_exact_at(_: Cache, file: &Shared, buf: &mut [u8], offset: u64) -> Result<(), Error> {
file.read_exact_at(buf, offset)?;
Ok(())
}
fn write_single_at(file: &File, offset: u64, buf: &[u8]) -> Result<(), Error> {
file.write_all_at(buf, offset)?;
Ok(())
}
fn write_vectored_at(
mut cache: Cache,
file: &Shared,
mut offset: u64,
bufs: &mut IoBufs,
flags: Option<libc::c_int>,
) -> Result<(), Error> {
assert!(
flags.is_none() || bufs.chunk_count() <= IOVEC_BATCH_SIZE,
"durability flags on a multi-submission write serialize its batches"
);
while bufs.has_remaining() {
let mut io_slices = vec![IoSlice::new(&[]); bufs.chunk_count().min(IOVEC_BATCH_SIZE)];
let io_slices_len = bufs.chunks_vectored(&mut io_slices);
assert!(
io_slices_len > 0,
"chunks_vectored should produce at least one slice when bufs has remaining"
);
cfg_if! {
if #[cfg(target_os = "linux")] {
let attempted_dont_cache = cache.is_disabled(&file.dont_cache_supported);
let ret = unsafe {
libc::pwritev2(
file.as_raw_fd(),
io_slices.as_ptr().cast::<libc::iovec>(),
io_slices_len as i32,
offset.try_into().map_err(|_| Error::OffsetOverflow)?,
flags.unwrap_or(0)
| if attempted_dont_cache { libc::RWF_DONTCACHE } else { 0 },
)
};
} else {
let _ = &cache;
let attempted_dont_cache = false;
assert!(flags.is_none(), "flags are only supported on Linux");
let ret = unsafe {
libc::pwritev(
file.as_raw_fd(),
io_slices.as_ptr().cast::<libc::iovec>(),
io_slices_len as i32,
offset.try_into().map_err(|_| Error::OffsetOverflow)?,
)
};
}
}
if ret < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
if cache.retry_cached(&file.dont_cache_supported, &err, attempted_dont_cache) {
continue;
}
return Err(err.into());
}
let bytes_written = ret as usize;
if bytes_written == 0 {
return Err(Error::WriteFailed);
}
bufs.advance(bytes_written);
offset = offset
.checked_add(bytes_written as u64)
.ok_or(Error::OffsetOverflow)?;
}
Ok(())
}
}
fn physical_offset(data_offset: u64, offset: u64, len: usize) -> Option<u64> {
alloc::Layout::array::<u8>(len).ok()?;
offset.checked_add(data_offset)
}
impl crate::Blob for Blob {
async fn read_at(
&self,
offset: u64,
len: usize,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
alloc::Layout::array::<u8>(len).map_err(|_| Error::OffsetOverflow)?;
#[cfg(target_os = "linux")]
if options.contains(ReadOptions::DONT_CACHE) && len > 0 {
let offset = offset
.checked_add(self.data_offset)
.ok_or(Error::OffsetOverflow)?;
let file = self.shared.clone();
return task::spawn_blocking(move || {
if let Some(buf) = aio::read_direct(&file, offset, len) {
return Ok(buf.into());
}
let mut buf = IoBufMut::with_capacity(len);
unsafe { buf.set_len(len) };
Self::read_exact_at(Cache::Disabled, &file, buf.as_mut(), offset)?;
Ok(buf.into())
})
.await
.map_err(|_| Error::ReadFailed)?;
}
self.read_at_buf(offset, len, IoBufMut::with_capacity(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 bufs = bufs.into();
unsafe { bufs.set_len(len) };
let offset = offset
.checked_add(self.data_offset)
.ok_or(Error::OffsetOverflow)?;
if len == 0 {
return Ok(bufs);
}
let file = self.shared.clone();
let cache = if options.contains(ReadOptions::DONT_CACHE) {
Cache::Disabled
} else {
Cache::Enabled
};
task::spawn_blocking(move || {
#[cfg(target_os = "linux")]
if matches!(cache, Cache::Disabled)
&& let Some(buf) = aio::read_direct(&file, offset, len)
{
bufs.copy_from_slice(buf.as_ref());
return Ok(bufs);
}
if let Some(buf) = bufs.as_single_mut() {
Self::read_exact_at(cache, &file, buf.as_mut(), offset)?;
} else {
let mut temp = IoBufMut::with_capacity(len);
unsafe { temp.set_len(len) };
Self::read_exact_at(cache, &file, temp.as_mut(), offset)?;
bufs.copy_from_slice(temp.as_ref());
}
Ok(bufs)
})
.await
.map_err(|_| Error::ReadFailed)?
}
fn read_many(
&self,
ranges: &[(u64, usize)],
options: ReadOptions,
) -> impl futures::Stream<Item = Result<(usize, IoBufsMut), Error>> + Send {
#[cfg(target_os = "linux")]
if options.contains(ReadOptions::DONT_CACHE) {
return aio::read_many(&self.shared, self.data_offset, ranges);
}
if ranges
.iter()
.any(|&(offset, len)| physical_offset(self.data_offset, offset, len).is_none())
{
return stream::iter([Err(Error::OffsetOverflow)]).boxed();
}
crate::read_each(self, ranges, options).boxed()
}
async fn start_read_many(
&self,
ranges: &[(u64, usize)],
options: ReadOptions,
) -> Handle<Vec<IoBufsMut>> {
#[cfg(target_os = "linux")]
if options.contains(ReadOptions::DONT_CACHE) {
let reads = aio::read_many(&self.shared, self.data_offset, ranges);
return Handle::from_future(crate::collect_reads(reads, ranges.len()));
}
let mut offsets = Vec::with_capacity(ranges.len());
for &(offset, len) in ranges {
let Some(offset) = physical_offset(self.data_offset, offset, len) else {
return Handle::ready(Err(Error::OffsetOverflow));
};
offsets.push((offset, len));
}
let reads = offsets
.into_iter()
.map(|(offset, len)| {
let mut bufs: IoBufsMut = IoBufMut::with_capacity(len).into();
unsafe { bufs.set_len(len) };
if len == 0 {
return Handle::ready(Ok(bufs));
}
let cache = if options.contains(ReadOptions::DONT_CACHE) {
Cache::Disabled
} else {
Cache::Enabled
};
let (tx, rx) = oneshot::channel();
let file = self.shared.clone();
task::spawn_blocking(move || {
let buf = bufs
.as_single_mut()
.expect("the read destination must hold exactly one buffer");
let result = Self::read_exact_at(cache, &file, buf.as_mut(), offset);
let _ = tx.send(result.map(|()| bufs));
});
Handle::from_receiver(rx)
})
.collect();
crate::join_reads(reads)
}
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(());
}
offset
.checked_add(Widen::widen(bufs.len()))
.filter(|end| *end <= i64::MAX as u64)
.ok_or(Error::OffsetOverflow)?;
let file = self.shared.clone();
let sync = options.contains(WriteOptions::SYNC);
if sync && let Some(error) = self.shared.tracker.failure() {
return Err(error);
}
let cache = if options.contains(WriteOptions::DONT_CACHE) {
Cache::Disabled
} else {
Cache::Enabled
};
cfg_if! {
if #[cfg(target_os = "linux")] {
let flags = (sync && bufs.chunk_count() <= IOVEC_BATCH_SIZE)
.then_some(libc::RWF_DSYNC);
} else {
let flags = None;
}
}
let fused = flags.is_some();
if !fused {
self.shared.tracker.write();
}
task::spawn_blocking(move || {
let mut bufs = if !sync && !cache.is_disabled(&file.dont_cache_supported) {
match bufs.try_into_single() {
Ok(buf) => {
#[cfg(test)]
file.wait_before_mutation();
Self::write_single_at(&file, offset, buf.as_ref())?;
file.tracker.complete();
return Ok(());
}
Err(bufs) => bufs,
}
} else {
bufs
};
let _durability = if fused {
Some(file.durability()?)
} else {
None
};
let result = Self::write_vectored_at(cache, &file, offset, &mut bufs, flags);
#[cfg(test)]
if fused {
file.wait_after_sync();
}
if fused && let Err(error) = &result {
file.tracker.write();
let (partition, name) = &file.key;
let error = match error {
Error::Io(error) => {
Error::BlobSyncFailed(partition.clone(), hex(name), error.clone())
}
error => error.clone(),
};
file.tracker.poison(&error);
}
result?;
if !fused {
file.tracker.complete();
}
if sync && !fused {
let seen = file.tracker.begin_sync();
file.flush(seen)?;
}
Ok(())
})
.await
.map_err(|_| Error::WriteFailed)?
}
async fn resize(&self, len: u64) -> Result<(), Error> {
let file = self.shared.clone();
let len = len
.checked_add(self.data_offset)
.ok_or(Error::OffsetOverflow)?;
self.shared.tracker.write();
task::spawn_blocking(move || {
#[cfg(test)]
file.wait_before_mutation();
file.set_len(len)?;
file.tracker.complete();
Ok(())
})
.await
.map_err(|e| e.into())
.and_then(|r: std::io::Result<()>| r)
.map_err(|e| {
let (partition, name) = &self.shared.key;
Error::BlobResizeFailed(partition.clone(), hex(name), e.into())
})?;
Ok(())
}
async fn sync(&self) -> Result<(), Error> {
if let Some(error) = self.shared.tracker.failure() {
return Err(error);
}
if !self.shared.tracker.is_dirty() {
#[cfg(test)]
self.shared.tracker.skip_sync();
return Ok(());
}
let file = self.shared.clone();
let seen = self.shared.tracker.begin_sync();
task::spawn_blocking(move || file.flush(seen))
.await
.map_err(|e| {
let err: std::io::Error = e.into();
let (partition, name) = &self.shared.key;
Error::BlobSyncFailed(partition.clone(), hex(name), err.into())
})?
}
async fn start_sync(&self) -> Handle<()> {
if let Some(error) = self.shared.tracker.failure() {
return Handle::ready(Err(error));
}
if !self.shared.tracker.is_dirty() {
#[cfg(test)]
self.shared.tracker.skip_sync();
return Handle::ready(Ok(()));
}
let (tx, rx) = oneshot::channel();
let file = self.shared.clone();
let seen = self.shared.tracker.begin_sync();
#[cfg(test)]
let after_start_sync = file.test.after_start_sync.lock().take();
task::spawn_blocking(move || {
let result = file.flush(seen);
drop(file);
let _ = tx.send(result);
#[cfg(test)]
if let Some((entered, release)) = after_start_sync {
let _ = entered.send(());
let _ = release.recv();
}
});
Handle::from_receiver(rx)
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use super::*;
use crate::{
Blob as _, BlobVersion, Storage as _,
storage::{
Layout,
tokio::{Config, Storage},
},
};
use futures::{FutureExt as _, TryStreamExt as _};
use std::{env, ops::RangeInclusive, path::PathBuf, process, sync::mpsc};
#[cfg(target_os = "linux")]
use std::{os::unix::fs::OpenOptionsExt as _, path::Path};
fn storage_for_reopen_test(label: &str, layouts: RangeInclusive<Layout>) -> (Storage, PathBuf) {
let directory = env::temp_dir().join(format!("storage_tokio_{label}_{}", process::id()));
(
Storage::new(Config::new(directory.clone(), layouts)),
directory,
)
}
#[cfg(target_os = "linux")]
fn open_direct(path: &Path) -> File {
File::options()
.read(true)
.custom_flags(libc::O_DIRECT)
.open(path)
.expect("temporary directory must support O_DIRECT")
}
#[tokio::test]
async fn test_overlapping_durability_failure() {
#[derive(Clone, Copy, Debug)]
enum Operation {
Sync,
StartSync,
Write(usize),
}
async fn run(blob: &Blob, operation: Operation) -> Result<(), Error> {
match operation {
Operation::Sync => blob.sync().await,
Operation::StartSync => blob.start_sync().await.await,
Operation::Write(chunks) => {
let bufs = IoBufs::from(
(0..chunks)
.map(|_| crate::IoBuf::from(b"x"))
.collect::<Vec<_>>(),
);
blob.write_at(6, bufs, WriteOptions::SYNC).await
}
}
}
for operation in [
Operation::Sync,
Operation::StartSync,
Operation::Write(1),
Operation::Write(IOVEC_BATCH_SIZE + 1),
] {
for failure_first in [false, true] {
for cancel in [false, true] {
let (storage, directory) = storage_for_reopen_test(
&format!("overlapping_failure_{operation:?}_{failure_first}_{cancel}"),
Layout::ALL,
);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"prefix", WriteOptions::default())
.await
.unwrap();
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*blob.shared.test.after_sync.lock() = Some((entered, gate));
if failure_first {
*storage.pending.test.fail_flush.lock() = Some(Error::Closed);
}
let mut first = Box::pin(run(
&blob,
if failure_first {
Operation::Sync
} else {
operation
},
));
assert!((&mut first).now_or_never().is_none());
entering.await.unwrap();
assert!(
blob.shared.durability.try_lock().is_none(),
"barrier released admission before accounting"
);
let first = if cancel {
drop(first);
None
} else {
Some(first)
};
if !failure_first {
*storage.pending.test.fail_flush.lock() = Some(Error::Closed);
}
let mut second = Box::pin(run(
&blob,
if failure_first {
operation
} else {
Operation::Sync
},
));
assert!((&mut second).now_or_never().is_none());
let early = (&mut second).now_or_never();
release.send(()).unwrap();
assert!(
early.is_none(),
"barrier completed before predecessor accounting"
);
let first = futures::future::OptionFuture::from(first).await;
let second = second.await;
if failure_first {
assert!(matches!(first, None | Some(Err(Error::Closed))));
assert!(matches!(second, Err(Error::Closed)));
assert!(blob.shared.tracker.is_dirty());
} else {
if let Some(first) = first {
first.unwrap();
}
assert!(matches!(second, Err(Error::Closed)));
}
assert!(matches!(blob.shared.tracker.failure(), Some(Error::Closed)));
drop(blob);
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::Closed)
));
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
}
}
}
async fn check_reopen_after_gated_mutation(
replace: bool,
shrink: bool,
retire_before_admit: bool,
) {
let (storage, directory) = storage_for_reopen_test(
&format!("gated_reopen_{replace}_{shrink}_{retire_before_admit}"),
Layout::V0..=Layout::V0,
);
let mut reopening = None;
let mut paused = None;
if replace {
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_metadata.lock() = Some((entered, gate));
let mut open = Box::pin(storage.open("partition", b"blob"));
commonware_macros::select! {
entered = entering => entered.unwrap(),
_ = &mut open => panic!("reopen completed before its debt read"),
}
reopening = Some(open);
paused = Some(release);
storage.remove("partition", Some(b"blob")).await.unwrap();
}
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
if shrink {
blob.write_at(0, b"orphaned", WriteOptions::SYNC)
.await
.unwrap();
}
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*blob.shared.test.before_mutation.lock() = Some((entered, gate));
let mut mutation = Box::pin(async {
if shrink {
blob.resize(0).await
} else {
blob.write_at(0, b"orphaned", WriteOptions::default()).await
}
});
assert!((&mut mutation).now_or_never().is_none());
entering.await.unwrap();
drop(mutation);
drop(blob);
let wait = storage
.pending
.entries
.lock()
.get(&("partition".into(), b"blob".to_vec()))
.unwrap()
.settle
.clone();
let admission = if retire_before_admit {
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*storage.pending.test.before_admit.lock() = Some((entered, gate));
Some((entering, release))
} else {
None
};
let mut reopening =
reopening.unwrap_or_else(|| Box::pin(storage.open("partition", b"blob")));
let early = (&mut reopening).now_or_never();
if let Some((entering, release_admit)) = admission {
entering.await.unwrap();
release.send(()).unwrap();
Pending::wait(wait.clone()).await.unwrap();
assert_eq!(storage.pending.outstanding(), 0);
release_admit.send(()).unwrap();
} else {
storage.scan("partition").await.unwrap();
release.send(()).unwrap();
}
drop(paused);
let (reopened, size) = match early {
Some(result) => result,
None => reopening.as_mut().await,
}
.unwrap();
drop(reopening);
Pending::wait(wait).await.unwrap();
if replace {
assert_eq!(size, 3);
assert_eq!(
reopened
.read_at(0, 3, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"old"
);
} else {
let expected = if shrink { 0 } else { 8 };
let bytes = reopened
.read_at(0, expected, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(
bytes.as_ref(),
if shrink {
b"".as_slice()
} else {
b"orphaned".as_slice()
}
);
assert_eq!(
size, expected as u64,
"returned length must describe the settled captured file"
);
}
drop(reopened);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[tokio::test]
async fn test_reopen_replacement_after_gated_mutation() {
for shrink in [false, true] {
check_reopen_after_gated_mutation(true, shrink, false).await;
}
}
#[tokio::test]
async fn test_reopen_after_gated_mutation() {
for shrink in [false, true] {
check_reopen_after_gated_mutation(false, shrink, false).await;
}
}
#[tokio::test]
async fn test_reopen_after_retirement_before_admission() {
for shrink in [false, true] {
check_reopen_after_gated_mutation(false, shrink, true).await;
}
}
#[tokio::test]
async fn test_reopen_canceled_during_final_metadata() {
let (storage, directory) = storage_for_reopen_test("canceled_metadata", Layout::ALL);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"old", WriteOptions::default())
.await
.unwrap();
drop(blob);
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*storage.pending.test.before_metadata.lock() = Some((entered, gate));
let mut opening = Box::pin(storage.open("partition", b"blob"));
assert!((&mut opening).now_or_never().is_none());
storage.scan("partition").await.unwrap();
commonware_macros::select! {
entered = entering => entered.unwrap(),
_ = &mut opening => panic!("open must wait for metadata"),
}
assert_eq!(storage.pending.completions(), 0);
drop(opening);
let mut retry = Box::pin(storage.open("partition", b"blob"));
assert!((&mut retry).now_or_never().is_none());
storage.scan("partition").await.unwrap();
release.send(()).unwrap();
let (blob, size) = retry.await.unwrap();
assert_eq!(size, 3);
assert_eq!(storage.pending.completions(), 1);
assert!(!storage.pending.owes("partition", b"blob"));
assert_eq!(
blob.read_at(0, 3, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"old"
);
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[tokio::test]
async fn test_reopen_canceled_during_wait_keeps_debt() {
let (storage, directory) = storage_for_reopen_test("canceled_wait", Layout::ALL);
let key = ("partition".to_string(), b"blob".to_vec());
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*blob.shared.test.before_mutation.lock() = Some((entered, gate));
let mut mutation = Box::pin(blob.write_at(0, b"orphaned", WriteOptions::default()));
assert!((&mut mutation).now_or_never().is_none());
entering.await.unwrap();
drop(mutation);
drop(blob);
let wait = storage
.pending
.entries
.lock()
.get(&key)
.unwrap()
.settle
.clone();
assert!(wait.is_some());
assert_eq!(storage.pending.outstanding(), 1);
let mut opening = Box::pin(storage.open("partition", b"blob"));
assert!((&mut opening).now_or_never().is_none());
storage.scan("partition").await.unwrap();
assert!((&mut opening).now_or_never().is_none());
{
let entries = storage.pending.entries.lock();
let entry = entries.get(&key).unwrap();
assert_eq!(entry.identity.strong_count(), 1);
assert!(entry.settle.is_some());
assert!(!entry.dirty);
}
drop(opening);
{
let entries = storage.pending.entries.lock();
let entry = entries.get(&key).unwrap();
assert_eq!(entry.identity.strong_count(), 0);
assert!(entry.settle.is_some());
assert!(!entry.dirty);
assert!(entry.failed.is_none());
}
assert_eq!(storage.pending.outstanding(), 1);
assert_eq!(storage.pending.completions(), 0);
release.send(()).unwrap();
Pending::wait(wait).await.unwrap();
assert_eq!(storage.pending.outstanding(), 0);
assert!(storage.pending.owes("partition", b"blob"));
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, 8);
assert_eq!(storage.pending.completions(), 1);
assert!(!storage.pending.owes("partition", b"blob"));
assert_eq!(
blob.read_at(0, 8, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"orphaned"
);
drop(blob);
assert!(storage.pending.entries.lock().is_empty());
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[tokio::test]
async fn test_header_read_after_payload_shrink() {
let (storage, directory) =
storage_for_reopen_test("header_shrink", Layout::V0..=Layout::V0);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let mut file = File::open(directory.join("partition").join(hex(b"blob"))).unwrap();
for original in [8, 5000] {
for retained in [0, 3] {
blob.resize(original).await.unwrap();
let raw_len = file.metadata().unwrap().len();
blob.resize(retained).await.unwrap();
let (size, _, offset) = crate::storage::resolve_header(
&mut file,
raw_len,
&Layout::ALL,
&(BlobVersion::new(0)..=BlobVersion::new(0)),
"partition",
b"blob",
)
.unwrap()
.unwrap();
assert_eq!(offset, Layout::V0.data_offset());
assert_eq!(size, retained);
}
}
drop(file);
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[tokio::test]
async fn test_read_many_indexes_every_range() {
let (storage, directory) = storage_for_reopen_test("read_many", Layout::ALL);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
const BLOCK: usize = 4096;
const BLOCKS: u64 = 64;
let data: Vec<u8> = (0..BLOCKS)
.flat_map(|i| std::iter::repeat_n(i as u8, BLOCK))
.collect();
blob.write_at(0, data, WriteOptions::default())
.await
.unwrap();
let mut ranges: Vec<(u64, usize)> = Vec::new();
for i in (0..BLOCKS).rev() {
ranges.push((i * BLOCK as u64, BLOCK));
ranges.push((i * BLOCK as u64 + 7, 10));
if i > 0 {
ranges.push((i * BLOCK as u64 - 3, 6));
}
ranges.push((i * BLOCK as u64, 0));
if i + 2 <= BLOCKS {
ranges.push((i * BLOCK as u64 + 100, 2 * BLOCK - 200));
}
}
let mut bufs: Vec<(usize, IoBufsMut)> = blob
.read_many(&ranges, ReadOptions::DONT_CACHE)
.try_collect()
.await
.unwrap();
assert_eq!(bufs.len(), ranges.len());
bufs.sort_by_key(|(index, _)| *index);
for (i, ((offset, len), (index, bufs))) in ranges.iter().zip(bufs).enumerate() {
assert_eq!(index, i);
let expected: Vec<u8> = (*offset..*offset + *len as u64)
.map(|o| (o / BLOCK as u64) as u8)
.collect();
assert_eq!(bufs.coalesce().as_ref(), expected.as_slice(), "range {i}");
}
assert!(
blob.read_many(
&[(0, BLOCK), (BLOCKS * BLOCK as u64 - 1, 2)],
ReadOptions::DONT_CACHE,
)
.try_collect::<Vec<_>>()
.await
.is_err()
);
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "linux")]
#[test]
fn test_read_many_retired_runtime_reports_error() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let (storage, directory) = storage_for_reopen_test("read_many_retired", Layout::ALL);
let blob = runtime.block_on(async {
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, b"data", WriteOptions::SYNC).await.unwrap();
blob
});
if aio::direct(&blob.shared).is_none() {
drop(blob);
runtime.block_on(storage.remove("partition", None)).unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
return;
}
let handle = runtime.handle().clone();
drop(runtime);
let entered = handle.enter();
let result = futures::executor::block_on(
blob.read_many(&[(0, 4)], ReadOptions::DONT_CACHE)
.try_collect::<Vec<_>>(),
);
assert!(matches!(result, Err(Error::ReadFailed)));
drop(entered);
drop(blob);
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_read_many_at_signed_limit() {
let (storage, directory) =
storage_for_reopen_test("read_many_signed_limit", Layout::V1..=Layout::V1);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let len = i64::MAX as u64 - blob.data_offset;
if blob.resize(len).await.is_err() || aio::direct(&blob.shared).is_none() {
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
return;
}
let offset = len - 1;
for options in [ReadOptions::default(), ReadOptions::DONT_CACHE] {
assert_eq!(
blob.read_at(offset, 1, options)
.await
.unwrap()
.coalesce()
.as_ref(),
&[0]
);
}
let bufs: Vec<_> = blob
.read_many(&[(offset, 1)], ReadOptions::DONT_CACHE)
.try_collect()
.await
.unwrap();
assert_eq!(bufs.len(), 1);
let (index, buf) = bufs.into_iter().next().unwrap();
assert_eq!(index, 0);
assert_eq!(buf.coalesce().as_ref(), &[0]);
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "linux")]
#[tokio::test]
#[ignore = "allocates more than 4 GiB"]
async fn test_read_many_beyond_single_read_limit() {
let (storage, directory) = storage_for_reopen_test("read_many_large", Layout::ALL);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
assert!(
aio::direct(&blob.shared).is_some(),
"temporary directory must support O_DIRECT"
);
const LEN: usize = 1 << 31;
blob.write_at(LEN as u64 - 1, vec![1], WriteOptions::default())
.await
.unwrap();
let bufs: Vec<_> = blob
.read_many(&[(0, LEN)], ReadOptions::DONT_CACHE)
.try_collect()
.await
.unwrap();
assert_eq!(bufs.len(), 1);
let (index, buf) = bufs.into_iter().next().unwrap();
assert_eq!(index, 0);
let buf = buf.coalesce();
assert_eq!(buf.len(), LEN);
assert_eq!(buf.as_ref()[LEN - 1], 1);
drop(buf);
let (single, _) = storage.open("partition", b"single").await.unwrap();
let path = directory.join("partition").join(hex(b"blob"));
assert!(single.shared.direct.set(Some(open_direct(&path))).is_ok());
let buf = single
.read_at(0, LEN, ReadOptions::DONT_CACHE)
.await
.unwrap()
.coalesce();
assert_eq!(buf.len(), LEN);
assert_eq!(buf.as_ref()[LEN - 1], 1);
drop(single);
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_read_many_serves_unaligned_ranges_directly() {
const BLOCK: usize = 4096;
const LEN: usize = BLOCK + 1000;
let data: Vec<u8> = (0..LEN).map(|i| (i % 251) as u8).collect();
for layout in [Layout::V0, Layout::V1] {
let (storage, directory) = storage_for_reopen_test(
&format!("read_many_unaligned_{layout:?}"),
layout..=layout,
);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
assert!(
aio::direct(&blob.shared).is_some(),
"temporary directory must support O_DIRECT"
);
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
let ranges = [
(100u64, 50usize),
(BLOCK as u64 - 3, 6),
(BLOCK as u64 + 500, 500),
(LEN as u64 - 1, 1),
(0, LEN),
];
let before = blob.shared.test.direct_reads.load(Ordering::Relaxed);
let bufs: Vec<(usize, IoBufsMut)> = blob
.read_many(&ranges, ReadOptions::DONT_CACHE)
.try_collect()
.await
.unwrap();
assert_eq!(
blob.shared.test.direct_reads.load(Ordering::Relaxed) - before,
ranges.len()
);
assert_eq!(bufs.len(), ranges.len());
for (index, bufs) in bufs {
let (offset, len) = ranges[index];
let offset = usize::try_from(offset).unwrap();
assert_eq!(bufs.coalesce().as_ref(), &data[offset..offset + len]);
}
let result = blob
.read_many(&[(LEN as u64 - 100, 101)], ReadOptions::DONT_CACHE)
.try_collect::<Vec<_>>()
.await;
assert!(matches!(
result,
Err(Error::Io(ref err)) if err.kind() == std::io::ErrorKind::UnexpectedEof
));
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_read_many_sees_unsynced_buffered_writes() {
let (storage, directory) = storage_for_reopen_test("read_many_coherent", Layout::ALL);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
assert!(
aio::direct(&blob.shared).is_some(),
"temporary directory must support O_DIRECT"
);
const BLOCK: usize = 4096;
const RANGES: [(u64, usize); 3] =
[(0, BLOCK), (BLOCK as u64, BLOCK), (BLOCK as u64 + 100, 300)];
async fn read_all(blob: &Blob) -> Vec<Vec<u8>> {
let before = blob.shared.test.direct_reads.load(Ordering::Relaxed);
let mut bufs: Vec<(usize, IoBufsMut)> = blob
.read_many(&RANGES, ReadOptions::DONT_CACHE)
.try_collect()
.await
.unwrap();
assert_eq!(
blob.shared.test.direct_reads.load(Ordering::Relaxed) - before,
RANGES.len()
);
bufs.sort_by_key(|(index, _)| *index);
bufs.into_iter()
.map(|(_, bufs)| bufs.coalesce().as_ref().to_vec())
.collect()
}
for generation in [0x11u8, 0x22] {
let data = vec![generation; 2 * BLOCK];
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
let bufs = read_all(&blob).await;
for ((offset, len), buf) in RANGES.iter().zip(bufs) {
let offset = *offset as usize;
assert_eq!(
buf,
&data[offset..offset + len],
"generation {generation:#x}"
);
}
}
blob.write_at(BLOCK as u64 + 150, vec![0x33; 100], WriteOptions::default())
.await
.unwrap();
let bufs = read_all(&blob).await;
assert_eq!(&bufs[2][..50], &[0x22; 50]);
assert_eq!(&bufs[2][50..150], &[0x33; 100]);
assert_eq!(&bufs[2][150..], &[0x22; 150]);
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_read_many_reports_unservable_range() {
let (storage, directory) = storage_for_reopen_test("read_many_unservable", Layout::ALL);
let (with_direct, _) = storage.open("partition", b"direct").await.unwrap();
assert!(
aio::direct(&with_direct.shared).is_some(),
"temporary directory must support O_DIRECT"
);
let (without_direct, _) = storage.open("partition", b"buffered").await.unwrap();
assert!(without_direct.shared.direct.set(None).is_ok());
for blob in [&with_direct, &without_direct] {
blob.write_at(0, vec![0u8; 4096], WriteOptions::default())
.await
.unwrap();
for options in [ReadOptions::default(), ReadOptions::DONT_CACHE] {
for len in [usize::MAX - 10, 1usize << (usize::BITS - 1)] {
for ranges in [vec![(0, len)], vec![(0, 4096), (0, len)]] {
let results: Vec<_> = blob.read_many(&ranges, options).collect().await;
assert!(
matches!(results.as_slice(), [Err(Error::OffsetOverflow)]),
"{ranges:?} {options:?}"
);
let result = blob.start_read_many(&ranges, options).await.await;
assert!(
matches!(result, Err(Error::OffsetOverflow)),
"{ranges:?} {options:?}"
);
}
assert!(
matches!(
blob.read_at(0, len, options).await,
Err(Error::OffsetOverflow)
),
"{len} {options:?}"
);
}
}
}
drop(with_direct);
drop(without_direct);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_read_at_serves_uncached_reads_directly() {
const BLOCK: usize = 4096;
const LEN: usize = 2 * BLOCK + 1000;
let data: Vec<u8> = (0..LEN).map(|i| (i % 251) as u8).collect();
let other: Vec<u8> = (0..LEN).map(|i| (i % 241) as u8).collect();
for layout in [Layout::V0, Layout::V1] {
let (storage, directory) =
storage_for_reopen_test(&format!("read_at_direct_{layout:?}"), layout..=layout);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
let (source, _) = storage.open("partition", b"source").await.unwrap();
source
.write_at(0, other.clone(), WriteOptions::default())
.await
.unwrap();
let path = directory.join("partition").join(hex(b"source"));
assert!(blob.shared.direct.set(Some(open_direct(&path))).is_ok());
for (offset, len) in [
(0u64, BLOCK),
(100, 50),
(BLOCK as u64 - 3, 6),
(500, BLOCK + 1000),
(LEN as u64 - 300, 300),
(LEN as u64 - 1, 1),
(0, LEN),
] {
let range = offset as usize..offset as usize + len;
let cached = blob
.read_at(offset, len, ReadOptions::default())
.await
.unwrap();
assert_eq!(cached.coalesce().as_ref(), &data[range.clone()]);
let single = IoBufMut::with_capacity(len);
let ptr = single.as_ref().as_ptr();
let cached = blob
.read_at_buf(offset, len, single, ReadOptions::default())
.await
.unwrap();
let buf = cached.as_single().unwrap();
assert_eq!(buf.as_ref().as_ptr(), ptr);
assert_eq!(buf.as_ref(), &data[range.clone()]);
let read = blob
.read_at(offset, len, ReadOptions::DONT_CACHE)
.await
.unwrap();
let buf = read.as_single().unwrap();
assert_eq!(buf.as_ref(), &other[range.clone()]);
let physical = offset + blob.data_offset;
assert_eq!(
buf.as_ref().as_ptr().addr() % BLOCK,
physical as usize % BLOCK
);
let single = IoBufMut::with_capacity(len);
let ptr = single.as_ref().as_ptr();
let read = blob
.read_at_buf(offset, len, single, ReadOptions::DONT_CACHE)
.await
.unwrap();
let buf = read.as_single().unwrap();
assert_eq!(buf.as_ref().as_ptr(), ptr);
assert_eq!(buf.as_ref(), &other[range.clone()]);
let chunks = vec![IoBufMut::with_capacity(1), IoBufMut::with_capacity(len)];
let ptrs: Vec<_> = chunks.iter().map(|chunk| chunk.as_ref().as_ptr()).collect();
let mut read = blob
.read_at_buf(
offset,
len,
IoBufsMut::from(chunks),
ReadOptions::DONT_CACHE,
)
.await
.unwrap();
assert!(!read.is_single());
assert_eq!(read.chunk().as_ptr(), ptrs[0]);
assert_eq!(read.chunk(), &other[range.start..range.start + 1]);
read.advance(1);
if len > 1 {
assert_eq!(read.chunk().as_ptr(), ptrs[1]);
}
assert_eq!(read.chunk(), &other[range.start + 1..range.end]);
}
for (offset, len) in [(LEN as u64 - 100, 101), (LEN as u64 + 5000, 10)] {
for options in [ReadOptions::default(), ReadOptions::DONT_CACHE] {
assert!(
matches!(
blob.read_at(offset, len, options).await,
Err(Error::Io(ref err))
if err.kind() == std::io::ErrorKind::UnexpectedEof
),
"({offset}, {len}) {options:?}"
);
}
}
source
.write_at(BLOCK as u64 + 150, vec![0x33; 100], WriteOptions::default())
.await
.unwrap();
source
.write_at(LEN as u64, vec![0x44; 10], WriteOptions::default())
.await
.unwrap();
let mut expected = other[BLOCK..].to_vec();
expected[150..250].fill(0x33);
expected.extend([0x44; 10]);
let read = blob
.read_at(BLOCK as u64, expected.len(), ReadOptions::DONT_CACHE)
.await
.unwrap();
assert_eq!(read.coalesce().as_ref(), expected.as_slice());
drop(blob);
drop(source);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_read_at_falls_back_to_buffered_reads() {
const BLOCK: usize = 4096;
const OFFSET: u64 = 100;
let data: Vec<u8> = (0..3 * BLOCK).map(|i| (i % 251) as u8).collect();
let expected = &data[OFFSET as usize..OFFSET as usize + BLOCK];
let (storage, directory) =
storage_for_reopen_test("read_at_fallback", Layout::V1..=Layout::V1);
let header = Layout::V1.data_offset() as usize;
let path = |name: &str| directory.join(name);
std::fs::write(path("empty"), b"").unwrap();
std::fs::write(path("short"), vec![0xEE; header + 150]).unwrap();
std::fs::write(path("boundary"), vec![0xEE; header + BLOCK]).unwrap();
let unreadable = File::options()
.write(true)
.custom_flags(libc::O_DIRECT)
.open(path("empty"))
.expect("temporary directory must support O_DIRECT");
let cases = [
("rejected", None, false),
("unreadable", Some(unreadable), false),
("empty", Some(open_direct(&path("empty"))), false),
("short", Some(open_direct(&path("short"))), true),
("boundary", Some(open_direct(&path("boundary"))), true),
];
for (label, direct, holds_prefix) in cases {
let (blob, _) = storage.open("partition", label.as_bytes()).await.unwrap();
blob.write_at(0, data.clone(), WriteOptions::default())
.await
.unwrap();
let read = blob
.read_at(OFFSET, 0, ReadOptions::DONT_CACHE)
.await
.unwrap();
assert!(read.is_empty());
let read = blob
.read_at_buf(
OFFSET,
0,
IoBufMut::with_capacity(1),
ReadOptions::DONT_CACHE,
)
.await
.unwrap();
assert!(read.is_empty());
assert!(blob.shared.direct.set(direct).is_ok());
let read = blob
.read_at(OFFSET, BLOCK, ReadOptions::DONT_CACHE)
.await
.unwrap();
assert_eq!(read.coalesce().as_ref(), expected, "{label}");
let buf = IoBufMut::with_capacity(BLOCK);
let ptr = buf.as_ref().as_ptr();
let read = blob
.read_at_buf(OFFSET, BLOCK, buf, ReadOptions::DONT_CACHE)
.await
.unwrap();
let buf = read.as_single().unwrap();
assert_eq!(buf.as_ref().as_ptr(), ptr, "{label}");
assert_eq!(buf.as_ref(), expected, "{label}");
if holds_prefix {
let read = blob
.read_at(OFFSET, 50, ReadOptions::DONT_CACHE)
.await
.unwrap();
assert_eq!(read.coalesce().as_ref(), &[0xEE; 50], "{label}");
}
drop(blob);
}
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[tokio::test]
async fn test_oversized_missing_header_is_not_recreated() {
let (storage, directory) = storage_for_reopen_test("oversized_header", Layout::ALL);
let parent = directory.join("partition");
std::fs::create_dir_all(&parent).unwrap();
let path = parent.join(hex(b"blob"));
let raw = vec![0; 4097];
std::fs::write(&path, &raw).unwrap();
assert!(matches!(
storage.open("partition", b"blob").await,
Err(Error::BlobCorrupt(_, _, _))
));
assert_eq!(std::fs::read(&path).unwrap(), raw);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "linux")]
#[tokio::test]
async fn test_fused_write_error_is_recorded_before_buffer_drop() {
struct WriteErrorObserver {
shared: std::sync::Weak<Shared>,
accounted: Arc<AtomicBool>,
}
impl AsRef<[u8]> for WriteErrorObserver {
fn as_ref(&self) -> &[u8] {
b"x"
}
}
impl Drop for WriteErrorObserver {
fn drop(&mut self) {
self.accounted.store(
self.shared.upgrade().unwrap().tracker.is_dirty(),
Ordering::Release,
);
}
}
let (storage, directory) = storage_for_reopen_test("write_error_order", Layout::ALL);
let path = directory.join("readonly");
std::fs::write(&path, b"").unwrap();
let blob = Blob::new(
File::options().read(true).open(&path).unwrap(),
0,
storage.hold.clone(),
storage
.pending
.admit("partition", b"readonly", false)
.unwrap()
.0,
);
let accounted = Arc::new(AtomicBool::new(false));
let bufs = bytes::Bytes::from_owner(WriteErrorObserver {
shared: Arc::downgrade(&blob.shared),
accounted: accounted.clone(),
});
let result = blob.write_at(0, bufs, WriteOptions::SYNC).await;
let poisoned = blob.shared.tracker.failure().is_some();
drop(blob);
let retained = storage.pending.admit("partition", b"readonly", true);
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
assert!(
matches!(result, Err(Error::Io(error)) if error.raw_os_error() == Some(libc::EBADF))
);
assert!(accounted.load(Ordering::Acquire));
assert!(poisoned, "a failed fused write must poison the open");
assert!(
matches!(retained, Err(Error::BlobSyncFailed(partition, name, error))
if partition == "partition" && name == hex(b"readonly")
&& error.raw_os_error() == Some(libc::EBADF))
);
}
#[tokio::test]
async fn test_invalid_write_range_leaves_open_clean() {
for (case, layouts) in [Layout::V0..=Layout::V0, Layout::ALL]
.into_iter()
.enumerate()
{
let (storage, directory) =
storage_for_reopen_test(&format!("invalid_write_range_{case}"), layouts);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let header = blob.data_offset;
let last = i64::MAX as u64;
let completed = storage.pending.completions();
for options in [WriteOptions::default(), WriteOptions::SYNC] {
blob.write_at(u64::MAX - header, IoBufs::default(), options)
.await
.unwrap();
assert!(matches!(
blob.write_at(u64::MAX, IoBufs::default(), options).await,
Err(Error::OffsetOverflow)
));
for chunks in [1, 2, IOVEC_BATCH_SIZE + 1] {
let physical = last - (Widen::widen(chunks) - 1);
let bufs = (0..chunks)
.map(|_| crate::IoBuf::from(b"x"))
.collect::<IoBufs>();
assert!(
matches!(
blob.write_at(physical - header, bufs, options).await,
Err(Error::OffsetOverflow)
),
"chunks={chunks} options={options:?}"
);
assert!(!blob.shared.tracker.is_dirty());
assert!(blob.shared.tracker.failure().is_none());
}
}
drop(blob);
let (blob, size) = storage.open("partition", b"blob").await.unwrap();
assert_eq!(size, 0);
assert_eq!(storage.pending.completions(), completed);
blob.write_at(0, b"ok", WriteOptions::SYNC).await.unwrap();
drop(blob);
storage.remove("partition", None).await.unwrap();
drop(storage);
std::fs::remove_dir_all(directory).unwrap();
}
}
struct PanickingOwner {
entered: Option<oneshot::Sender<()>>,
release: mpsc::Receiver<()>,
}
impl AsRef<[u8]> for PanickingOwner {
fn as_ref(&self) -> &[u8] {
b"x"
}
}
impl Drop for PanickingOwner {
fn drop(&mut self) {
let _ = self.entered.take().unwrap().send(());
let _ = self.release.recv();
panic!("buffer owner dropped");
}
}
#[tokio::test]
async fn test_reopen_syncs_fallback_write_after_owner_panic() {
let storage_directory =
env::temp_dir().join(format!("storage_tokio_owner_panic_{}", process::id()));
let storage = Storage::new(Config::new(storage_directory.clone(), Layout::ALL));
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let (entered, entering) = oneshot::channel();
let (release, released) = mpsc::channel();
let mut chunks = vec![crate::IoBuf::from(bytes::Bytes::from_owner(
PanickingOwner {
entered: Some(entered),
release: released,
},
))];
for _ in 0..IOVEC_BATCH_SIZE {
chunks.push(crate::IoBuf::from(vec![b'x']));
}
let blob = Arc::new(blob);
let writing_blob = Arc::clone(&blob);
let writing =
::tokio::spawn(
async move { writing_blob.write_at(0, chunks, WriteOptions::SYNC).await },
);
entering.await.unwrap();
let bytes = blob.read_at(0, 1, ReadOptions::default()).await;
release.send(()).unwrap();
let result = writing.await.unwrap();
assert_eq!(bytes.unwrap().coalesce().as_ref(), b"x");
assert!(matches!(result, Err(Error::WriteFailed)));
let (entered, entering) = oneshot::channel();
let (release, gate) = mpsc::channel();
*storage.pending.test.before_complete.lock() = Some((entered, gate));
drop(blob);
let mut reopen = Box::pin(storage.open("partition", b"blob"));
commonware_macros::select! {
entered = entering => entered.unwrap(),
_ = &mut reopen => panic!("reopen returned before the failed write was made durable"),
}
assert!(
(&mut reopen).now_or_never().is_none(),
"reopen returned before the failed write was made durable"
);
release.send(()).unwrap();
let (blob, size) = reopen.await.unwrap();
assert!(size >= 1);
assert_eq!(
blob.read_at(0, 1, ReadOptions::default())
.await
.unwrap()
.coalesce()
.as_ref(),
b"x"
);
drop(blob);
let flushes = storage.pending.completions();
drop(storage);
let _ = std::fs::remove_dir_all(storage_directory);
assert_eq!(flushes, 1);
}
#[tokio::test]
async fn test_reopen_after_start_sync_completion() {
let (storage, storage_directory) = storage_for_reopen_test("sync_completion", Layout::ALL);
let (blob, _) = storage.open("partition", b"blob").await.unwrap();
let (entered, entering) = oneshot::channel();
let (release, released) = mpsc::channel();
*blob.shared.test.after_start_sync.lock() = Some((entered, released));
blob.write_at(0, b"before", WriteOptions::default())
.await
.unwrap();
let observed = async {
blob.start_sync().await.await?;
entering.await.expect("sync worker did not pause");
blob.write_at(0, b"after sync", WriteOptions::default())
.await?;
drop(blob);
let (reopened, len) = storage.open("partition", b"blob").await?;
let bytes = reopened
.read_at(0, len as usize, ReadOptions::default())
.await?;
Ok::<_, Error>((len, bytes.coalesce(), storage.pending.completions()))
}
.await;
let _ = release.send(());
let (len, bytes, syncs) = observed.unwrap();
let _ = std::fs::remove_dir_all(storage_directory);
assert_eq!(len, 10);
assert_eq!(bytes.as_ref(), b"after sync");
assert_eq!(syncs, 1, "reopen did not flush the later write");
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_cache_bypass_is_ignored_off_linux() {
let supported = AtomicBool::new(true);
let cache = Cache::Disabled;
assert!(!cache.is_disabled(&supported));
}
#[test]
fn test_cache_bypass_retry_decision() {
let supported = AtomicBool::new(true);
let mut cache = Cache::Disabled;
let mut sibling = Cache::Disabled;
let unsupported = std::io::Error::from_raw_os_error(libc::EOPNOTSUPP);
let invalid = std::io::Error::from_raw_os_error(libc::EINVAL);
assert!(!cache.retry_cached(&supported, &invalid, true));
assert!(supported.load(Ordering::Relaxed));
assert!(!cache.retry_cached(&supported, &unsupported, false));
assert!(supported.load(Ordering::Relaxed));
assert!(cache.retry_cached(&supported, &unsupported, true));
assert!(!supported.load(Ordering::Relaxed));
assert!(!sibling.is_disabled(&supported));
assert!(sibling.retry_cached(&supported, &unsupported, true));
assert!(!cache.retry_cached(&supported, &unsupported, true));
}
}