use super::{
Sealed,
read::{PageReader, Replay},
tip::Buffer,
view::{Tail, View},
};
use crate::{
Blob, Error, Handle, IoBuf, IoBufMut, IoBufs, ReadOptions, WriteOptions,
buffer::{
SyncState,
paged::{ActiveChecksum, CHECKSUM_SIZE, CacheRef, Checksum, Slot},
},
};
use bytes::BufMut;
use commonware_codec::{EncodeSize, Write};
use commonware_cryptography::Crc32;
use commonware_utils::Widen;
use std::{
marker::PhantomData,
num::{NonZeroU16, NonZeroUsize},
sync::Arc,
};
use tracing::warn;
fn adjusted_capacity(capacity: usize, page_size: u64) -> usize {
let page_size = page_size as usize;
let rounded = capacity.next_multiple_of(page_size);
let floor = page_size * 2;
if rounded < floor {
warn!(
floor,
"requested buffer capacity is too low, increasing it to floor"
);
}
rounded.max(floor)
}
const fn too_big_for_buffer(
buffer_len: usize,
buffer_capacity: usize,
append_len: usize,
page_size: usize,
) -> bool {
let fill = buffer_len.next_multiple_of(page_size) - buffer_len;
let overflows_capacity = buffer_len + append_len > buffer_capacity;
let has_full_page_after_fill = append_len >= fill + page_size;
overflows_capacity && has_full_page_after_fill
}
pub struct Append;
pub struct Recovering;
pub type Recovery<B> = Writer<B, Recovering>;
pub struct Writer<B: Blob, Phase = Append> {
phase: PhantomData<Phase>,
blob: Arc<B>,
current_page: u64,
partial_page_state: Option<ActiveChecksum>,
durable_page_state: Option<ActiveChecksum>,
sync_state: SyncState,
id: u64,
cache_ref: CacheRef,
buffer: Buffer,
}
impl<B: Blob> Recovery<B> {
pub async fn open(
blob: B,
original_blob_size: u64,
capacity: usize,
cache_ref: CacheRef,
) -> Result<Self, Error> {
let blob = Arc::new(blob);
let page_size: u64 = cache_ref.page_size().widen();
let (partial_page_state, pages, invalid_data_found) =
Writer::<B>::read_last_valid_page(&blob, original_blob_size, page_size).await?;
if invalid_data_found {
let new_blob_size = pages * (page_size + CHECKSUM_SIZE);
warn!(
original_blob_size,
new_blob_size, "truncating blob to remove invalid data"
);
blob.resize(new_blob_size).await?;
blob.sync().await?;
}
let capacity = adjusted_capacity(capacity, page_size);
let needs_sync = !invalid_data_found;
let (current_page, partial_page_state, partial_data) = match partial_page_state {
Some((partial_page, crc_record)) => (pages - 1, Some(crc_record), Some(partial_page)),
None => (pages, None, None),
};
let buffer = Buffer::from(
current_page * page_size,
partial_data.unwrap_or_default().as_ref(),
capacity,
page_size as usize,
);
Ok(Self {
phase: PhantomData,
blob,
current_page,
partial_page_state,
durable_page_state: partial_page_state,
sync_state: if needs_sync {
SyncState::Dirty
} else {
SyncState::Clean
},
id: cache_ref.next_id(),
cache_ref,
buffer,
})
}
async fn sync_shrunk_page(
&mut self,
page: u64,
page_size: u64,
new_len: u16,
new_crc: u32,
old_checksum: &ActiveChecksum,
) -> Result<ActiveChecksum, Error> {
let physical_page_size = page_size
.checked_add(CHECKSUM_SIZE)
.ok_or(Error::OffsetOverflow)?;
let crc_start = page
.checked_mul(physical_page_size)
.and_then(|start| start.checked_add(page_size))
.ok_or(Error::OffsetOverflow)?;
let old_slot = old_checksum.slot;
let new_slot = old_slot.other();
let new_slot_offset = crc_start
.checked_add(new_slot.offset() as u64)
.ok_or(Error::OffsetOverflow)?;
let staged_slot = Checksum::slot_bytes(0, new_crc);
self.sync_state
.write_at(
&self.blob,
new_slot_offset,
staged_slot.to_vec(),
WriteOptions::SYNC | WriteOptions::DONT_CACHE,
)
.await?;
let published_len = Checksum::slot_len_bytes(new_len);
self.sync_state
.write_at(
&self.blob,
new_slot_offset,
published_len.to_vec(),
WriteOptions::SYNC | WriteOptions::DONT_CACHE,
)
.await?;
let old_slot_offset = crc_start
.checked_add(old_slot.offset() as u64)
.ok_or(Error::OffsetOverflow)?;
self.sync_state
.write_at(
&self.blob,
old_slot_offset,
Checksum::slot_bytes(0, 0).to_vec(),
WriteOptions::SYNC | WriteOptions::DONT_CACHE,
)
.await?;
Ok(ActiveChecksum::new(new_slot, new_len, new_crc))
}
async fn shrink(mut self, target_size: u64) -> Result<Self, Error> {
let page_size: u64 = self.cache_ref.page_size().widen();
let physical_page_size = page_size
.checked_add(CHECKSUM_SIZE)
.ok_or(Error::OffsetOverflow)?;
self = self.sync().await?;
let full_pages = target_size / page_size;
let partial_bytes = target_size % page_size;
let physical_pages = full_pages
.checked_add(u64::from(partial_bytes > 0))
.ok_or(Error::OffsetOverflow)?;
let new_physical_size = physical_pages
.checked_mul(physical_page_size)
.ok_or(Error::OffsetOverflow)?;
let tail_offset = full_pages
.checked_mul(page_size)
.ok_or(Error::OffsetOverflow)?;
let current_physical_size = if self.partial_page_state.is_some() {
self.current_page
.checked_add(1)
.and_then(|pages| pages.checked_mul(physical_page_size))
.ok_or(Error::OffsetOverflow)?
} else {
self.current_page
.checked_mul(physical_page_size)
.ok_or(Error::OffsetOverflow)?
};
if new_physical_size != current_physical_size {
self.sync_state
.resize(&self.blob, new_physical_size)
.await?;
}
if partial_bytes > 0 {
self.shrink_to_partial(full_pages, partial_bytes, page_size, tail_offset)
.await?;
return Ok(self);
}
self.partial_page_state = None;
self.durable_page_state = None;
self.current_page = full_pages;
self.buffer.replace(tail_offset, &[]);
Ok(self)
}
async fn shrink_to_partial(
&mut self,
full_pages: u64,
partial_bytes: u64,
page_size: u64,
tail_offset: u64,
) -> Result<(), Error> {
self.current_page = full_pages;
let (page_data, old_checksum) = super::get_page_with_checksum_from_blob(
&self.blob,
full_pages,
page_size,
ReadOptions::DONT_CACHE,
)
.await?;
if (page_data.len() as u64) < partial_bytes {
return Err(Error::InvalidChecksum);
}
let new_data = &page_data.as_ref()[..partial_bytes as usize];
self.buffer.replace(tail_offset, new_data);
let final_record = self
.sync_shrunk_page(
full_pages,
page_size,
partial_bytes as u16,
Crc32::checksum(new_data),
&old_checksum,
)
.await?;
self.partial_page_state = Some(final_record);
self.durable_page_state = Some(final_record);
Ok(())
}
pub async fn truncate(mut self, size: u64) -> Result<Self, Error> {
if size < self.size() {
self = self.shrink(size).await?;
}
self.sync().await
}
}
impl<B: Blob> From<Recovery<B>> for Writer<B> {
fn from(recovery: Recovery<B>) -> Self {
Self {
phase: PhantomData,
blob: recovery.blob,
current_page: recovery.current_page,
partial_page_state: recovery.partial_page_state,
durable_page_state: recovery.durable_page_state,
sync_state: recovery.sync_state,
id: recovery.id,
cache_ref: recovery.cache_ref,
buffer: recovery.buffer,
}
}
}
impl<B: Blob> Writer<B> {
pub async fn new(
blob: B,
size: u64,
capacity: usize,
cache_ref: CacheRef,
) -> Result<Self, Error> {
Ok(Recovery::open(blob, size, capacity, cache_ref)
.await?
.into())
}
async fn read_last_valid_page(
blob: &B,
blob_size: u64,
page_size: u64,
) -> Result<(Option<(IoBuf, ActiveChecksum)>, u64, bool), Error> {
let physical_page_size = page_size + CHECKSUM_SIZE;
let partial_bytes = blob_size % physical_page_size;
let mut last_page_end = blob_size - partial_bytes;
let mut invalid_data_found = partial_bytes != 0;
while last_page_end != 0 {
let page_start = last_page_end - physical_page_size;
let buf = blob
.read_at(
page_start,
physical_page_size as usize,
ReadOptions::default(),
)
.await?
.coalesce()
.freeze();
match Checksum::validate_page(buf.as_ref()) {
Some(checksum) => {
let len = checksum.len as u64;
if len != page_size {
let logical_bytes = buf.slice(..len as usize);
return Ok((
Some((logical_bytes, checksum)),
last_page_end / physical_page_size,
invalid_data_found,
));
}
return Ok((None, last_page_end / physical_page_size, invalid_data_found));
}
None => {
last_page_end = page_start;
invalid_data_found = true;
}
}
}
Ok((None, 0, invalid_data_found))
}
pub async fn snapshot(mut self) -> Result<(Self, Sealed<B>), Error> {
self.flush_internal(false, false).await?;
let sealed = self.sealed_handle(self.id);
Ok((self, sealed))
}
pub fn try_snapshot(&self) -> Option<Sealed<B>> {
if self.has_flush_work(false) {
return None;
}
Some(self.sealed_handle(self.id))
}
async fn read_page(
blob: &B,
page: u64,
page_size: u64,
read_options: ReadOptions,
) -> Result<(IoBuf, u64, u64), Error> {
let (logical, _) =
super::get_page_with_checksum_from_blob(blob, page, page_size, read_options).await?;
let start = page.checked_mul(page_size).ok_or(Error::OffsetOverflow)?;
let len: u64 = Widen::widen(logical.len());
let end = start.checked_add(len).ok_or(Error::OffsetOverflow)?;
Ok((logical, start, end))
}
pub async fn read_range(
blob: &B,
logical_page_size: NonZeroU16,
offset: u64,
len: usize,
read_options: ReadOptions,
) -> Result<IoBufs, Error> {
let len_u64: u64 = Widen::widen(len);
let end = offset.checked_add(len_u64).ok_or(Error::OffsetOverflow)?;
if len == 0 {
return Ok(IoBufs::default());
}
let logical_page_size: u64 = logical_page_size.widen();
let first_page = offset / logical_page_size;
let last_page = (end - 1) / logical_page_size;
let mut out = IoBufs::default();
for page in first_page..=last_page {
let (logical, page_start, page_end) =
Self::read_page(blob, page, logical_page_size, read_options).await?;
let overlap_start = offset.max(page_start);
let overlap_end = end.min(page_end);
if overlap_start >= overlap_end {
return Err(Error::BlobInsufficientLength);
}
let start = (overlap_start - page_start) as usize;
let end = (overlap_end - page_start) as usize;
out.append(logical.slice(start..end));
}
if out.len() != len {
return Err(Error::BlobInsufficientLength);
}
Ok(out)
}
pub async fn read_tail(
blob: &B,
physical_size: u64,
logical_page_size: NonZeroU16,
len: usize,
read_options: ReadOptions,
) -> Result<(u64, IoBufs), Error> {
if physical_size == 0 {
return if len == 0 {
Ok((0, IoBufs::default()))
} else {
Err(Error::BlobInsufficientLength)
};
}
let page_size: u64 = logical_page_size.widen();
let physical_page_size = page_size
.checked_add(CHECKSUM_SIZE)
.ok_or(Error::OffsetOverflow)?;
if !physical_size.is_multiple_of(physical_page_size) {
return Err(Error::BlobInsufficientLength);
}
let page = physical_size / physical_page_size - 1;
let (tail, page_start, logical_size) =
Self::read_page(blob, page, page_size, read_options).await?;
let len_u64: u64 = Widen::widen(len);
let offset = logical_size
.checked_sub(len_u64)
.ok_or(Error::BlobInsufficientLength)?;
let mut out = if offset < page_start {
let prefix_len =
usize::try_from(page_start - offset).map_err(|_| Error::OffsetOverflow)?;
Self::read_range(blob, logical_page_size, offset, prefix_len, read_options).await?
} else {
IoBufs::default()
};
let tail_start = usize::try_from(offset.max(page_start) - page_start)
.map_err(|_| Error::OffsetOverflow)?;
out.append(tail.slice(tail_start..));
assert_eq!(out.len(), len);
Ok((logical_size, out))
}
fn sealed_handle(&self, id: u64) -> Sealed<B> {
let page_size: u64 = self.cache_ref.page_size().widen();
let full_pages = self.current_page;
assert_eq!(
full_pages.checked_mul(page_size),
Some(self.buffer.offset),
"flushed page count is inconsistent with the buffer offset"
);
let partial_page = if self.buffer.is_empty() {
None
} else {
Some(IoBuf::copy_from_slice(self.buffer.partial()))
};
Sealed::new(
self.blob.clone(),
self.buffer.size(),
partial_page,
self.cache_ref.clone(),
id,
)
}
pub async fn seal(mut self) -> Result<(Sealed<B>, Handle<()>), Error> {
self.sync_state.wait_for_pending().await?;
self.flush_internal(true, false).await?;
let handle = self.sync_state.start_sync(&self.blob).await;
Ok((self.sealed_handle(self.id), handle))
}
}
impl<B: Blob, Phase> Writer<B, Phase> {
pub async fn append(mut self, buf: &[u8]) -> Result<(Self, u64), Error> {
let page_size: usize = self.cache_ref.page_size().widen();
if too_big_for_buffer(
self.buffer.len(),
self.buffer.capacity,
buf.len(),
page_size,
) {
return self.append_owned(IoBuf::copy_from_slice(buf)).await;
}
let offset = self.buffer.size();
if self.buffer.append(buf) {
self.flush_internal(false, false).await?;
}
Ok((self, offset))
}
pub fn try_append(&mut self, buf: &[u8]) -> Option<u64> {
self.buffer.try_append(buf)
}
pub fn try_append_value<T: EncodeSize + Write>(&mut self, value: &T) -> Option<u64> {
self.buffer.try_append_value(value)
}
pub async fn append_owned(mut self, buf: IoBuf) -> Result<(Self, u64), Error> {
let page_size: usize = self.cache_ref.page_size().widen();
let offset = self.buffer.size();
if !too_big_for_buffer(
self.buffer.len(),
self.buffer.capacity,
buf.len(),
page_size,
) {
if self.buffer.append(buf.as_ref()) {
self.flush_internal(false, false).await?;
}
return Ok((self, offset));
}
let fill = self.buffer.len().next_multiple_of(page_size) - self.buffer.len();
if fill > 0 {
self.buffer.append(&buf.as_ref()[..fill]);
}
let boundary = self.buffer.size();
if !self.buffer.is_empty() {
self.flush_internal(false, false).await?;
assert!(
self.buffer.size() == boundary && self.buffer.is_empty(),
"flush left unexpected buffered bytes before a direct-path append"
);
}
let bulk_len = (buf.len() - fill) / page_size * page_size;
let bulk = buf.slice(fill..fill + bulk_len);
let mut physical_pages = IoBufs::default();
self.append_full_pages(std::iter::once(&bulk), None, &mut physical_pages);
assert!(
self.partial_page_state.is_none(),
"an empty tip implies no partial page state"
);
self.sync_state.wait_for_pending().await?;
let chunk_len = self.buffer.capacity;
let mut cache_offset = boundary;
for chunk in bulk.as_ref().chunks(chunk_len) {
let remaining = self.cache_ref.cache(self.id, chunk, cache_offset);
assert_eq!(remaining, 0, "cached bulk pages must be page-aligned");
cache_offset += chunk.len() as u64;
}
self.current_page += (bulk_len / page_size) as u64;
self.buffer
.replace(boundary + bulk_len as u64, &buf.as_ref()[fill + bulk_len..]);
let page_size: u64 = self.cache_ref.page_size().widen();
assert_eq!(self.current_page * page_size, self.buffer.offset);
let physical_page_size = page_size + CHECKSUM_SIZE;
let write_at_offset = boundary / page_size * physical_page_size;
self.sync_state
.write_at(
&self.blob,
write_at_offset,
physical_pages,
WriteOptions::DONT_CACHE,
)
.await?;
Ok((self, offset))
}
fn partial_page_dirty(
full_pages: usize,
partial_len: usize,
flushed: Option<&ActiveChecksum>,
) -> bool {
partial_len != 0
&& (full_pages > 0 || flushed.is_none_or(|state| state.len as usize != partial_len))
}
fn has_flush_work(&self, write_partial_page: bool) -> bool {
let page_size: usize = self.cache_ref.page_size().widen();
let full_pages = self.buffer.len() / page_size;
if full_pages > 0 {
return true;
}
write_partial_page
&& Self::partial_page_dirty(
full_pages,
self.buffer.len() % page_size,
self.partial_page_state.as_ref(),
)
}
async fn flush_internal(
&mut self,
write_partial_page: bool,
sync: bool,
) -> Result<bool, Error> {
if !self.has_flush_work(write_partial_page) {
return Ok(false);
}
self.sync_state.wait_for_pending().await?;
if self.sync_state.is_clean() {
self.durable_page_state = self.partial_page_state;
}
let page_size: usize = self.cache_ref.page_size().widen();
let pages_to_cache = self.buffer.len() / page_size;
let cache_offset = self.buffer.offset;
let full_pages = self.buffer.drain_full_pages();
let new_offset = self.buffer.offset;
let (physical_pages, partial_page_state) = self.to_physical_pages(
&full_pages,
self.buffer.partial(),
write_partial_page,
self.partial_page_state.as_ref(),
self.durable_page_state.as_ref(),
);
assert!(
!physical_pages.is_empty(),
"flush work predicate must match physical page construction"
);
if pages_to_cache > 0 {
let cached = self.cache_ref.cache_pages(
self.id,
full_pages
.iter()
.flat_map(|chunk| chunk.as_ref().chunks_exact(page_size)),
cache_offset,
);
assert_eq!(cached, pages_to_cache * page_size);
}
let physical_page_size = page_size + CHECKSUM_SIZE as usize;
let write_at_offset = self.current_page * physical_page_size as u64;
self.current_page += pages_to_cache as u64;
self.partial_page_state = partial_page_state;
if pages_to_cache > 0 {
self.durable_page_state = None;
}
let page_size: u64 = self.cache_ref.page_size().widen();
assert_eq!(self.current_page * page_size, new_offset);
if sync {
self.sync_state
.write_at(
&self.blob,
write_at_offset,
physical_pages,
WriteOptions::SYNC | WriteOptions::DONT_CACHE,
)
.await?;
self.durable_page_state = partial_page_state;
} else {
self.sync_state
.write_at(
&self.blob,
write_at_offset,
physical_pages,
WriteOptions::DONT_CACHE,
)
.await?;
}
Ok(sync)
}
pub const fn size(&self) -> u64 {
self.buffer.size()
}
pub fn needs_sync(&self) -> bool {
!self.sync_state.is_clean() || self.has_flush_work(true)
}
const fn view(&self) -> View<'_, B> {
View {
blob: &self.blob,
cache_ref: &self.cache_ref,
id: self.id,
size: self.buffer.size(),
tail_offset: self.buffer.offset,
tail: Tail::Buffered(&self.buffer),
}
}
pub fn try_read_sync_into(&self, buf: &mut [u8], offset: u64) -> bool {
self.view().try_read_sync_into(buf, offset)
}
pub async fn read_at(&self, offset: u64, len: usize) -> Result<IoBufs, Error> {
self.view().read_at(offset, len).await
}
pub async fn read_up_to(
&self,
offset: u64,
len: usize,
bufs: impl Into<IoBufMut> + Send,
) -> Result<(IoBufMut, usize), Error> {
self.view().read_up_to(offset, len, bufs).await
}
pub async fn read_many_into(
&self,
buf: &mut [u8],
offsets: &[u64],
item_size: NonZeroUsize,
) -> Result<usize, Error> {
self.view().read_many_into(buf, offsets, item_size).await
}
pub async fn read_ranges_into(
&self,
buf: &mut [u8],
ranges: &[(u64, usize)],
) -> Result<usize, Error> {
self.view().read_ranges_into(buf, ranges).await
}
pub fn try_read_many_sync_into(
&self,
buf: &mut [u8],
offsets: &[u64],
item_size: NonZeroUsize,
) -> Vec<usize> {
self.view().try_read_many_sync_into(buf, offsets, item_size)
}
pub fn try_read_ranges_sync_into(&self, buf: &mut [u8], ranges: &[(u64, usize)]) -> Vec<usize> {
self.view().try_read_ranges_sync_into(buf, ranges)
}
pub async fn read_into(&self, buf: &mut [u8], offset: u64) -> Result<(), Error> {
self.view().read_into(buf, offset).await
}
fn to_physical_pages(
&self,
full_pages: &IoBufs,
partial_page: &[u8],
include_partial_page: bool,
flushed: Option<&ActiveChecksum>,
durable: Option<&ActiveChecksum>,
) -> (IoBufs, Option<ActiveChecksum>) {
let page_size: usize = self.cache_ref.page_size().widen();
let physical_page_size = page_size + CHECKSUM_SIZE as usize;
let pages_to_write = full_pages.len() / page_size;
let mut write_buffer = IoBufs::default();
if pages_to_write > 0 {
self.append_full_pages(full_pages.iter(), durable, &mut write_buffer);
}
if !include_partial_page {
return (write_buffer, None);
}
if !Self::partial_page_dirty(pages_to_write, partial_page.len(), flushed) {
return (write_buffer, None);
}
let partial_len = partial_page.len();
let crc = Crc32::checksum(partial_page);
let durable = if pages_to_write == 0 { durable } else { None };
let (crc_record, active_checksum) =
Self::build_crc_record(partial_len as u16, crc, durable);
let mut padded = IoBufMut::with_capacity(physical_page_size);
padded.put_slice(partial_page);
let zero_count = page_size - partial_len;
if zero_count > 0 {
padded.put_bytes(0, zero_count);
}
padded.put_slice(&crc_record.to_bytes());
write_buffer.append(padded.freeze());
(write_buffer, Some(active_checksum))
}
fn append_full_pages<'a>(
&self,
data: impl Iterator<Item = &'a IoBuf> + Clone,
old_checksum: Option<&ActiveChecksum>,
write_buffer: &mut IoBufs,
) {
let page_size: usize = self.cache_ref.page_size().widen();
let pages = data
.clone()
.map(|chunk| {
assert!(chunk.len().is_multiple_of(page_size));
chunk.len() / page_size
})
.sum::<usize>();
assert!(pages > 0);
let page_size_u16 =
u16::try_from(page_size).expect("page size must fit in u16 for CRC record");
let mut crcs = IoBufMut::with_capacity(CHECKSUM_SIZE as usize * pages);
for (page, logical_page) in data
.clone()
.flat_map(|chunk| chunk.as_ref().chunks_exact(page_size))
.enumerate()
{
let crc = Crc32::checksum(logical_page);
let old_checksum = if page == 0 { old_checksum } else { None };
let (crc_record, _) = Self::build_crc_record(page_size_u16, crc, old_checksum);
crcs.put_slice(&crc_record.to_bytes());
}
let crc_blob = crcs.freeze();
let mut crc_start = 0;
for chunk in data {
for start in (0..chunk.len()).step_by(page_size) {
write_buffer.append(chunk.slice(start..start + page_size));
write_buffer.append(crc_blob.slice(crc_start..crc_start + CHECKSUM_SIZE as usize));
crc_start += CHECKSUM_SIZE as usize;
}
}
}
const fn build_crc_record(
new_len: u16,
new_crc: u32,
old_checksum: Option<&ActiveChecksum>,
) -> (Checksum, ActiveChecksum) {
let Some(old_checksum) = old_checksum else {
return (
Checksum::new(new_len, new_crc),
ActiveChecksum::new(Slot::First, new_len, new_crc),
);
};
let new_slot = old_checksum.slot.other();
let record = match old_checksum.slot {
Slot::First => Checksum {
len1: old_checksum.len,
crc1: old_checksum.crc,
len2: new_len,
crc2: new_crc,
},
Slot::Second => Checksum {
len1: new_len,
crc1: new_crc,
len2: old_checksum.len,
crc2: old_checksum.crc,
},
};
(record, ActiveChecksum::new(new_slot, new_len, new_crc))
}
pub async fn replay(
mut self,
buffer_size: NonZeroUsize,
read_options: ReadOptions,
) -> Result<(Self, Replay<B>), Error> {
self.flush_internal(true, false).await?;
let page_size = self.cache_ref.page_size();
let logical_size = self.size();
let physical_page_size = u64::from(page_size.get()) + CHECKSUM_SIZE;
let pages = logical_size.div_ceil(u64::from(page_size.get()));
let physical_size = pages
.checked_mul(physical_page_size)
.ok_or(Error::OffsetOverflow)?;
let buffer_pages = (buffer_size.get() / physical_page_size as usize).max(1);
let partial_page = (logical_size > self.buffer.offset)
.then(|| IoBuf::copy_from_slice(self.buffer.partial()));
let replay = Replay::new(PageReader::new(
self.blob.clone(),
physical_size,
logical_size,
partial_page,
buffer_pages,
page_size,
read_options,
));
Ok((self, replay))
}
pub async fn sync(mut self) -> Result<Self, Error> {
if self.flush_internal(true, true).await? {
return Ok(self);
}
self.sync_state.sync(&self.blob).await?;
self.durable_page_state = self.partial_page_state;
Ok(self)
}
pub async fn start_sync(mut self) -> Result<(Self, Handle<()>), Error> {
self.flush_internal(true, false).await?;
let handle = self.sync_state.start_sync(&self.blob).await;
Ok((self, handle))
}
pub async fn recoverable_prefix_len(
&self,
proven: u64,
buffer_size: NonZeroUsize,
read_options: ReadOptions,
) -> Result<u64, Error> {
self.recoverable_prefix_len_at_most(proven, u64::MAX, buffer_size, read_options)
.await
}
pub async fn recoverable_prefix_len_at_most(
&self,
proven: u64,
max_size: u64,
buffer_size: NonZeroUsize,
read_options: ReadOptions,
) -> Result<u64, Error> {
let logical_page_size: u64 = self.cache_ref.page_size().widen();
let total_pages = (self.current_page + u64::from(self.partial_page_state.is_some()))
.min(max_size.div_ceil(logical_page_size));
let physical_page_size = logical_page_size
.checked_add(CHECKSUM_SIZE)
.ok_or(Error::OffsetOverflow)?;
let physical_page_size_usize =
usize::try_from(physical_page_size).map_err(|_| Error::OffsetOverflow)?;
let max_batch_pages: u64 =
Widen::widen((buffer_size.get() / physical_page_size_usize).max(1));
let start_page = (proven / logical_page_size)
.min(self.current_page)
.min(total_pages);
let mut valid_len = start_page
.checked_mul(logical_page_size)
.ok_or(Error::OffsetOverflow)?;
let mut page = start_page;
while page < total_pages {
let batch_pages = max_batch_pages.min(total_pages - page);
let batch_end = page.checked_add(batch_pages).ok_or(Error::OffsetOverflow)?;
let physical_offset = page
.checked_mul(physical_page_size)
.ok_or(Error::OffsetOverflow)?;
let physical_len = batch_pages
.checked_mul(physical_page_size)
.ok_or(Error::OffsetOverflow)?;
let physical_len = usize::try_from(physical_len).map_err(|_| Error::OffsetOverflow)?;
let physical = self
.blob
.read_at(physical_offset, physical_len, read_options)
.await?
.coalesce();
for physical_page in physical.as_ref().chunks_exact(physical_page_size_usize) {
let Some(checksum) = Checksum::validate_page(physical_page) else {
return Ok(valid_len.min(max_size));
};
let len = u64::from(checksum.len);
valid_len = valid_len.checked_add(len).ok_or(Error::OffsetOverflow)?;
if len < logical_page_size {
return Ok(valid_len.min(max_size));
}
}
page = batch_end;
}
Ok(valid_len.min(max_size))
}
pub async fn wait_for_sync(mut self) -> Result<Self, Error> {
self.sync_state.wait_for_pending().await?;
Ok(self)
}
}
#[cfg(test)]
#[commonware_macros::test_group("miri")]
mod tests {
use super::*;
use crate::{
Buf, Handle, IoBufsMut, Runner as _, Spawner as _, Storage as _, Supervisor as _,
buffer::{paged::CHECKSUM_SLOT_SIZE, tests::SyncTrackingBlob},
deterministic,
mocks::{
DelayedSyncBlob, RecordingContext, WriteFaultContext, WriteFaults, next_pending_sync,
},
};
use commonware_codec::{Copying, FixedSize, ReadExt};
use commonware_macros::test_traced;
use commonware_utils::{NZU16, NZUsize, channel::oneshot, sync::Mutex};
use futures::FutureExt as _;
use std::{
cell::Cell,
num::NonZeroU16,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
impl<B: Blob, Phase> Writer<B, Phase> {
pub(in super::super) const fn cache_id(&self) -> u64 {
self.id
}
}
const PAGE_SIZE: NonZeroU16 = NZU16!(103); const BUFFER_SIZE: usize = PAGE_SIZE.get() as usize * 2;
#[test_traced]
fn test_try_append_capacity() {
deterministic::Runner::default().start(|context| async move {
let (blob, size) = context.open("try_append", b"blob").await.unwrap();
let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
let mut writer = Writer::new(blob, size, 32, cache).await.unwrap();
assert_eq!(writer.try_append(&[1; 20]), Some(0));
assert_eq!(writer.try_append(&[2; 12]), Some(20));
assert_eq!(writer.size(), 32);
assert_eq!(writer.try_append(&[3]), None);
assert_eq!(writer.size(), 32);
let (writer, offset) = writer.append(&[3]).await.unwrap();
assert_eq!(offset, 32);
let read = writer.read_at(0, 33).await.unwrap().coalesce();
assert_eq!(&read.as_ref()[..20], &[1; 20]);
assert_eq!(&read.as_ref()[20..32], &[2; 12]);
assert_eq!(read.as_ref()[32], 3);
});
}
#[test_traced]
fn test_append_value_capacity_snapshot_and_recovery() {
deterministic::Runner::default().start(|context| async move {
struct MustNotEncode<const SIZE: usize>;
impl<const SIZE: usize> FixedSize for MustNotEncode<SIZE> {
const SIZE: usize = SIZE;
}
impl<const SIZE: usize> Write for MustNotEncode<SIZE> {
fn write(&self, _: &mut impl BufMut) {
panic!("insufficient capacity must not invoke the encoder");
}
}
let (blob, size) = context.open("append_value", b"blob").await.unwrap();
let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
let mut writer = Writer::new(blob, size, 32, cache.clone()).await.unwrap();
assert_eq!(writer.try_append_value(&0u64), Some(0));
assert_eq!(writer.try_append_value(&MustNotEncode::<25>), None);
let (mut writer, snapshot) = writer.snapshot().await.unwrap();
for i in 1..4u64 {
assert_eq!(writer.try_append_value(&i), Some(i * 8));
}
assert_eq!(writer.try_append_value(&MustNotEncode::<8>), None);
assert_eq!(writer.size(), 32);
assert_eq!(
snapshot.read_at(0, 8).await.unwrap().coalesce().as_ref(),
&0u64.to_be_bytes()
);
let (writer, offset) = writer.append(&4u64.to_be_bytes()).await.unwrap();
assert_eq!(offset, 32);
let writer = writer.sync().await.unwrap();
drop(snapshot);
drop(writer);
let (blob, size) = context.open("append_value", b"blob").await.unwrap();
let writer = Writer::new(blob, size, 32, cache).await.unwrap();
assert_eq!(writer.size(), 40);
let expected: Vec<_> = (0..5u64).flat_map(u64::to_be_bytes).collect();
assert_eq!(
writer.read_at(0, 40).await.unwrap().coalesce().as_ref(),
expected
);
});
}
#[test_traced]
fn test_append_variable_value_encodes_in_place() {
struct Value {
size: usize,
size_calls: Cell<usize>,
writes: Cell<usize>,
destination: Cell<usize>,
}
impl EncodeSize for Value {
fn encode_size(&self) -> usize {
self.size_calls.set(self.size_calls.get() + 1);
self.size
}
}
impl Write for Value {
fn write(&self, buf: &mut impl BufMut) {
self.writes.set(self.writes.get() + 1);
if self.size > 0 {
self.destination.set(buf.chunk_mut().as_mut_ptr() as usize);
}
buf.put_bytes(7, self.size);
}
}
deterministic::Runner::default().start(|context| async move {
let (blob, size) = context.open("variable_value", b"blob").await.unwrap();
let cache = CacheRef::new(NZU16!(16), NZUsize!(4));
let mut writer = Writer::new(blob, size, 64, cache.clone()).await.unwrap();
let mut value = Value {
size: 3,
size_calls: Cell::new(0),
writes: Cell::new(0),
destination: Cell::new(0),
};
assert_eq!(writer.try_append_value(&value), Some(0));
let tail = writer.buffer.parts().1.as_ptr() as usize;
assert_eq!(value.destination.get(), tail);
value.size = 5;
assert_eq!(writer.try_append_value(&value), Some(3));
assert_eq!(value.destination.get(), tail + 3);
value.size = 17;
assert_eq!(writer.try_append_value(&value), Some(8));
assert_eq!(value.destination.get(), tail + 8);
assert_eq!(
writer.buffer.parts().0.chunk_at(0).unwrap().as_ptr() as usize,
tail
);
value.size = 0;
assert_eq!(writer.try_append_value(&value), Some(25));
value.size = 39;
assert_eq!(writer.try_append_value(&value), Some(25));
for size in [1, usize::MAX] {
value.size = size;
assert_eq!(writer.try_append_value(&value), None);
}
value.size = 0;
assert_eq!(writer.try_append_value(&value), Some(64));
assert_eq!(value.size_calls.get(), 8);
assert_eq!(value.writes.get(), 6);
assert_eq!(writer.size(), 64);
writer = writer.sync().await.unwrap();
drop(writer);
let (blob, size) = context.open("variable_value", b"blob").await.unwrap();
let writer = Writer::new(blob, size, 64, cache).await.unwrap();
assert_eq!(writer.size(), 64);
assert_eq!(
writer.read_at(0, 64).await.unwrap().coalesce().as_ref(),
&[7; 64]
);
});
}
#[test_traced("DEBUG")]
fn test_writes_use_uncached_hint() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
writer = writer.sync().await.unwrap();
(writer, _) = writer.append(b"first").await.unwrap();
writer = writer.sync().await.unwrap();
assert_eq!(blob.uncached_snapshot(), (0, 1));
(writer, _) = writer.append(b"second").await.unwrap();
let (_, sync) = writer.seal().await.unwrap();
sync.await.unwrap();
assert_eq!(blob.uncached_snapshot(), (1, 1));
});
}
#[test_traced("DEBUG")]
fn test_unsynced_flushes_preserve_durable_checksum_slot() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let page_size = PAGE_SIZE.get() as usize;
let physical_page = page_size + CHECKSUM_SIZE as usize;
let (blob, blob_size) = context
.open("test_partition", b"replay_torn")
.await
.unwrap();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let synced: Vec<u8> = (1u8..=24).collect();
(writer, _) = writer.append(&synced).await.unwrap();
writer = writer.sync().await.unwrap();
let durable_image = blob
.read_at(0, physical_page, ReadOptions::default())
.await
.unwrap()
.coalesce();
(writer, _) = writer.append(&[25u8; 8]).await.unwrap();
(writer, _) = writer
.replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap();
(writer, _) = writer.append(&[26u8; 8]).await.unwrap();
writer
.replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap();
let torn_image = blob
.read_at(0, physical_page, ReadOptions::default())
.await
.unwrap()
.coalesce();
let mut crash = durable_image.as_ref().to_vec();
crash[page_size..].copy_from_slice(&torn_image.as_ref()[page_size..]);
let (crashed, _) = context
.open("test_partition", b"replay_crash")
.await
.unwrap();
crashed
.write_at(0, crash, WriteOptions::default())
.await
.unwrap();
crashed.sync().await.unwrap();
let recovered = Writer::new(crashed, physical_page as u64, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(recovered.size(), synced.len() as u64);
let read = recovered.read_at(0, synced.len()).await.unwrap().coalesce();
assert_eq!(read.as_ref(), synced.as_slice());
});
}
#[test_traced("DEBUG")]
fn test_recoverable_prefix_len_clean() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (context, recordings) = RecordingContext::new(context);
let (blob, blob_size) = context
.open("test_partition", b"prefix_clean")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(
writer
.recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap(),
0
);
let total = PAGE_SIZE.get() as usize * 2 + 50;
let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
recordings.clear();
assert_eq!(
writer
.recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap(),
total as u64
);
let reads = recordings.snapshot().reads;
assert!(!reads.is_empty());
assert!(
reads
.iter()
.all(|options| *options == ReadOptions::default())
);
drop(writer);
let (blob, blob_size) = context
.open("test_partition", b"prefix_clean")
.await
.unwrap();
recordings.clear();
let _recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let reads = recordings.snapshot().reads;
assert!(!reads.is_empty());
assert!(
reads
.iter()
.all(|options| *options == ReadOptions::default())
);
});
}
#[test_traced("DEBUG")]
fn test_recoverable_prefix_len_torn_interior_page() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"prefix_torn")
.await
.unwrap();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let total = PAGE_SIZE.get() as usize * 3 + 10;
let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
let offset = physical_page_size + 7;
let byte = blob
.read_at(offset, 1, ReadOptions::default())
.await
.unwrap()
.coalesce();
blob.write_at(
offset,
vec![byte.as_ref()[0] ^ 0xFF],
WriteOptions::default(),
)
.await
.unwrap();
blob.sync().await.unwrap();
assert_eq!(
writer
.recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap(),
PAGE_SIZE.get() as u64
);
});
}
#[test_traced("DEBUG")]
fn test_recoverable_prefix_len_proven_prefix() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"prefix_proven")
.await
.unwrap();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let total = PAGE_SIZE.get() as usize * 4;
let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
let physical_page_size = PAGE_SIZE.get() as u64 + CHECKSUM_SIZE;
let offset = physical_page_size + 7;
let byte = blob
.read_at(offset, 1, ReadOptions::default())
.await
.unwrap()
.coalesce();
blob.write_at(
offset,
vec![byte.as_ref()[0] ^ 0xFF],
WriteOptions::default(),
)
.await
.unwrap();
blob.sync().await.unwrap();
let page = u64::from(PAGE_SIZE.get());
for (proven, expected) in [
(0, page),
(page, page),
(2 * page, 4 * page),
(2 * page + 3, 4 * page),
(total as u64, 4 * page),
] {
assert_eq!(
writer
.recoverable_prefix_len(
proven,
NZUsize!(BUFFER_SIZE),
ReadOptions::default()
)
.await
.unwrap(),
expected,
"proven {proven}"
);
}
assert_eq!(
writer
.recoverable_prefix_len(
100 * page,
NZUsize!(BUFFER_SIZE),
ReadOptions::default()
)
.await
.unwrap(),
4 * page
);
(writer, _) = writer.append(&data[..20]).await.unwrap();
writer = writer.sync().await.unwrap();
for proven in [4 * page + 20, 5 * page, 100 * page] {
assert_eq!(
writer
.recoverable_prefix_len(
proven,
NZUsize!(BUFFER_SIZE),
ReadOptions::default()
)
.await
.unwrap(),
4 * page + 20,
"proven {proven}"
);
}
});
}
#[test_traced("DEBUG")]
fn test_recoverable_prefix_len_stale_short_interior_page() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"prefix_stale")
.await
.unwrap();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let total = PAGE_SIZE.get() as usize * 2;
let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
(writer, _) = writer.append(&data[..20]).await.unwrap();
writer = writer.sync().await.unwrap();
let physical_page_size = (PAGE_SIZE.get() as u64 + CHECKSUM_SIZE) as usize;
let stale = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let stale = stale.as_ref().to_vec();
(writer, _) = writer.append(&data[20..]).await.unwrap();
writer = writer.sync().await.unwrap();
blob.write_at(0, stale, WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
assert_eq!(
writer
.recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap(),
20
);
});
}
#[test_traced("DEBUG")]
fn test_recoverable_prefix_len_batches_reads() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (context, recordings) = RecordingContext::new(context);
let (blob, blob_size) = context
.open("test_partition", b"prefix_batches")
.await
.unwrap();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let total = PAGE_SIZE.get() as usize * 8 + 10;
let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
recordings.clear();
assert_eq!(
writer
.recoverable_prefix_len(0, NZUsize!(1), ReadOptions::default())
.await
.unwrap(),
total as u64
);
assert_eq!(recordings.snapshot().reads.len(), 9);
recordings.clear();
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let scan_buffer = NonZeroUsize::new(physical_page_size * 3).unwrap();
assert_eq!(
writer
.recoverable_prefix_len(0, scan_buffer, ReadOptions::default())
.await
.unwrap(),
total as u64
);
assert_eq!(recordings.snapshot().reads.len(), 3);
});
}
#[test_traced("DEBUG")]
fn test_recoverable_prefix_len_at_most() {
deterministic::Runner::default().start(|context| async move {
let (context, recordings) = RecordingContext::new(context);
let (blob, size) = context.open("bounded_prefix", b"blob").await.unwrap();
let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache).await.unwrap();
let page = u64::from(PAGE_SIZE.get());
let total = page * 8 + 10;
(writer, _) = writer.append(&vec![7; total as usize]).await.unwrap();
writer = writer.sync().await.unwrap();
for (cap, proven, expected_reads) in [
(0, 0, 0),
(1, 0, 1),
(page, 0, 1),
(page + 1, 0, 2),
(page * 3, page, 2),
(1, u64::MAX, 0),
(total - 1, 0, 9),
(total, 0, 9),
(total + 1, 0, 9),
(u64::MAX, 0, 9),
(u64::MAX, u64::MAX, 1),
] {
recordings.clear();
assert_eq!(
writer
.recoverable_prefix_len_at_most(
proven,
cap,
NZUsize!(1),
ReadOptions::DONT_CACHE,
)
.await
.unwrap(),
cap.min(total),
"cap {cap}, proven {proven}",
);
let reads = recordings.snapshot().reads;
assert_eq!(reads.len(), expected_reads, "cap {cap}, proven {proven}");
assert!(
reads
.iter()
.all(|options| *options == ReadOptions::DONT_CACHE)
);
}
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_empty() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(append, _) = append.append(&[0u8; 8]).await.unwrap();
assert_eq!(append.size(), 8);
let mut buf = [];
append
.read_many_into(&mut buf, &[], NZUsize!(4))
.await
.unwrap();
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_all_in_tip() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0..20).collect();
(append, _) = append.append(&data).await.unwrap();
assert_eq!(append.size(), 20);
let offsets = [0u64, 4, 8, 12, 16];
let mut buf = vec![0u8; 5 * 4];
append
.read_many_into(&mut buf, &offsets, NZUsize!(4))
.await
.unwrap();
for (i, &off) in offsets.iter().enumerate() {
assert_eq!(
&buf[i * 4..(i + 1) * 4],
&data[off as usize..off as usize + 4],
);
}
});
}
#[test_traced("DEBUG")]
fn test_try_read_sync_all_in_tip() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"try_read_sync_tip")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0..20).collect();
(append, _) = append.append(&data).await.unwrap();
let mut buf = vec![0u8; data.len()];
assert!(append.try_read_sync_into(&mut buf, 0));
assert_eq!(buf, data);
});
}
#[test_traced("DEBUG")]
fn test_try_read_sync_cache_miss() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"try_read_sync_cache_miss")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(1));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let page_size = PAGE_SIZE.get() as usize;
let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
let _ = append.read_at(0, page_size).await.unwrap();
let mut buf = vec![0xAA; 4];
assert!(!append.try_read_sync_into(&mut buf, (page_size - 2) as u64));
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_all_from_cache() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0..20).collect();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
assert_eq!(append.size(), 20);
let offsets = [0u64, 8, 16];
let mut buf = vec![0u8; 3 * 4];
append
.read_many_into(&mut buf, &offsets, NZUsize!(4))
.await
.unwrap();
for (i, &off) in offsets.iter().enumerate() {
assert_eq!(
&buf[i * 4..(i + 1) * 4],
&data[off as usize..off as usize + 4],
);
}
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_mixed_tip_and_cache() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let first: Vec<u8> = (0..16).collect();
(append, _) = append.append(&first).await.unwrap();
append = append.sync().await.unwrap();
let second: Vec<u8> = (16..32).collect();
(append, _) = append.append(&second).await.unwrap();
assert_eq!(append.size(), 32);
let offsets = [0u64, 4, 16, 24];
let mut buf = vec![0u8; 4 * 4];
append
.read_many_into(&mut buf, &offsets, NZUsize!(4))
.await
.unwrap();
let all: Vec<u8> = (0..32).collect();
for (i, &off) in offsets.iter().enumerate() {
assert_eq!(
&buf[i * 4..(i + 1) * 4],
&all[off as usize..off as usize + 4],
);
}
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_out_of_bounds() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(append, _) = append.append(&[0u8; 8]).await.unwrap();
assert_eq!(append.size(), 8);
let mut buf = vec![0u8; 4];
let err = append
.read_many_into(&mut buf, &[8], NZUsize!(4))
.await
.unwrap_err();
assert!(matches!(err, Error::BlobInsufficientLength));
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_single_item() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data = vec![0xAA; 8];
(append, _) = append.append(&data).await.unwrap();
assert_eq!(append.size(), 8);
let mut buf = vec![0u8; 8];
append
.read_many_into(&mut buf, &[0], NZUsize!(8))
.await
.unwrap();
assert_eq!(&buf, &data);
});
}
#[test_traced("DEBUG")]
#[should_panic(expected = "buf must hold one slot per range totaling its length")]
fn test_read_many_into_rejects_invalid_buffer_len() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0..16).collect();
(append, _) = append.append(&data).await.unwrap();
let offsets = [0u64, 4];
let mut buf = vec![0u8; 7];
let _ = append.read_many_into(&mut buf, &offsets, NZUsize!(4)).await;
});
}
#[test_traced("DEBUG")]
#[should_panic(expected = "ranges must be sorted and non-overlapping")]
fn test_read_many_into_rejects_unsorted_offsets() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0..16).collect();
(append, _) = append.append(&data).await.unwrap();
let mut buf = vec![0u8; 8];
let _ = append.read_many_into(&mut buf, &[8, 4], NZUsize!(4)).await;
});
}
#[test_traced("DEBUG")]
#[should_panic(expected = "ranges must be sorted and non-overlapping")]
fn test_read_many_into_rejects_overlapping_offsets() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0..16).collect();
(append, _) = append.append(&data).await.unwrap();
let mut buf = vec![0u8; 8];
let _ = append.read_many_into(&mut buf, &[2, 4], NZUsize!(4)).await;
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_rejects_offset_overflow() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0..16).collect();
(append, _) = append.append(&data).await.unwrap();
let mut buf = vec![0u8; 8];
let err = append
.read_many_into(&mut buf, &[u64::MAX - 1, 4], NZUsize!(4))
.await
.unwrap_err();
assert!(matches!(err, Error::OffsetOverflow));
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_matches_read_at() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0u8..=255).cycle().take(300).collect();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
let more: Vec<u8> = (0u8..50).collect();
(append, _) = append.append(&more).await.unwrap();
assert_eq!(append.size(), 350);
let item_size = 10;
let offsets: Vec<u64> = (0..35).map(|i| i * item_size as u64).collect();
let mut batch_buf = vec![0u8; offsets.len() * item_size];
append
.read_many_into(&mut batch_buf, &offsets, NZUsize!(item_size))
.await
.unwrap();
for (i, &off) in offsets.iter().enumerate() {
let single = append.read_at(off, item_size).await.unwrap().coalesce();
assert_eq!(
&batch_buf[i * item_size..(i + 1) * item_size],
single.as_ref(),
"mismatch at offset {off}",
);
}
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_scattered_cache_misses() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rmany").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(2));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let synced: Vec<u8> = (0u8..=255)
.cycle()
.take(PAGE_SIZE.get() as usize * 3)
.collect();
(append, _) = append.append(&synced).await.unwrap();
append = append.sync().await.unwrap();
let item_size = 10;
let tip_len = PAGE_SIZE.get() as usize / 2;
let tip: Vec<u8> = (100u8..=255).cycle().take(tip_len).collect();
(append, _) = append.append(&tip).await.unwrap();
let _ = append.read_at(0, item_size).await.unwrap();
let _ = append
.read_at(PAGE_SIZE.get() as u64 * 2, item_size)
.await
.unwrap();
let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
let tip_off = synced.len() as u64 + item_size as u64;
let offsets = [
0u64, PAGE_SIZE.get() as u64, PAGE_SIZE.get() as u64 * 2, straddle_off, tip_off, ];
let mut buf = vec![0u8; offsets.len() * item_size];
append
.read_many_into(&mut buf, &offsets, NZUsize!(item_size))
.await
.unwrap();
let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
for (i, &off) in offsets.iter().enumerate() {
assert_eq!(
&buf[i * item_size..(i + 1) * item_size],
&read[off as usize..off as usize + item_size],
);
}
});
}
#[test_traced("DEBUG")]
fn test_read_many_into_straddle_prefix_miss() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"rmany_smiss")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(1));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let synced: Vec<u8> = (0u8..=255)
.cycle()
.take(PAGE_SIZE.get() as usize * 3)
.collect();
(append, _) = append.append(&synced).await.unwrap();
append = append.sync().await.unwrap();
let item_size = 10;
let tip: Vec<u8> = (100u8..=255)
.cycle()
.take(PAGE_SIZE.get() as usize / 2)
.collect();
(append, _) = append.append(&tip).await.unwrap();
let _ = append.read_at(0, item_size).await.unwrap();
let straddle_off = synced.len() as u64 - (item_size as u64 / 2);
let tip_off = synced.len() as u64 + item_size as u64;
let offsets = [straddle_off, tip_off];
let mut buf = vec![0u8; offsets.len() * item_size];
let hits = append
.read_many_into(&mut buf, &offsets, NZUsize!(item_size))
.await
.unwrap();
assert_eq!(hits, 1);
let read: Vec<u8> = synced.iter().chain(tip.iter()).copied().collect();
for (i, &off) in offsets.iter().enumerate() {
assert_eq!(
&buf[i * item_size..(i + 1) * item_size],
&read[off as usize..off as usize + item_size],
);
}
});
}
#[test_traced("DEBUG")]
fn test_append_crc_empty() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
assert_eq!(blob_size, 0);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 0);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
assert_eq!(blob_size, 0);
let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 0);
});
}
#[test_traced("DEBUG")]
fn test_append_crc_basic() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
assert_eq!(blob_size, 0);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 0);
let data = vec![1, 2, 3, 4, 5];
(append, _) = append.append(&data).await.unwrap();
assert_eq!(append.size(), 5);
let more_data = vec![6, 7, 8, 9, 10];
(append, _) = append.append(&more_data).await.unwrap();
assert_eq!(append.size(), 10);
let read_buf = append.read_at(0, 5).await.unwrap().coalesce();
assert_eq!(read_buf, &data[..]);
let read_buf = append.read_at(5, 5).await.unwrap().coalesce();
assert_eq!(read_buf, &more_data[..]);
let read_buf = append.read_at(0, 10).await.unwrap().coalesce();
assert_eq!(read_buf, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
assert_eq!(blob_size, 115);
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 10);
let spanning_data: Vec<u8> = (11..=110).collect();
(append, _) = append.append(&spanning_data).await.unwrap();
assert_eq!(append.size(), 110);
let read_buf = append.read_at(10, 100).await.unwrap().coalesce();
assert_eq!(read_buf, &spanning_data[..]);
let read_buf = append.read_at(0, 110).await.unwrap().coalesce();
let expected: Vec<u8> = (1..=110).collect();
assert_eq!(read_buf, &expected[..]);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
assert_eq!(blob_size, 230);
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 110);
let boundary_data: Vec<u8> = (111..=206).collect();
assert_eq!(boundary_data.len(), 96);
(append, _) = append.append(&boundary_data).await.unwrap();
assert_eq!(append.size(), 206);
let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
let expected: Vec<u8> = (1..=206).collect();
assert_eq!(read_buf, &expected[..]);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context.open("test_partition", b"test_blob").await.unwrap();
assert_eq!(blob_size, 230);
let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 206);
let read_buf = append.read_at(0, 206).await.unwrap().coalesce();
assert_eq!(read_buf, &expected[..]);
});
}
#[test_traced("DEBUG")]
fn test_append_owned_bypass_from_empty_tip() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"owned_empty")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
let src = IoBuf::from(data.clone());
let src_start = src.as_ptr() as usize;
let src_range = src_start..src_start + src.len();
(append, _) = append.append_owned(src.clone()).await.unwrap();
assert_eq!(append.size(), 500);
let tip_ptr = append.buffer.parts().1.as_ptr() as usize;
assert!(!src_range.contains(&tip_ptr));
let mut probe = vec![0u8; PAGE_SIZE.get() as usize];
assert_eq!(
append.cache_ref.read_cached(append.id, &mut probe, 0),
PAGE_SIZE.get() as usize
);
assert_eq!(probe, &data[..PAGE_SIZE.get() as usize]);
let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
assert_eq!(read_buf, &data[..]);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context
.open("test_partition", b"owned_empty")
.await
.unwrap();
let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 500);
let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
assert_eq!(read_buf, &data[..]);
});
}
#[test_traced("DEBUG")]
fn test_append_owned_bypass_with_synced_partial_page() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"owned_partial")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let all: Vec<u8> = (0..500).map(|i| (i % 247) as u8).collect();
(append, _) = append.append(&all[..50]).await.unwrap();
append = append.sync().await.unwrap();
(append, _) = append
.append_owned(IoBuf::from(all[50..].to_vec()))
.await
.unwrap();
assert_eq!(append.size(), 500);
let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
assert_eq!(read_buf, &all[..]);
drop(append);
let (blob, blob_size) = context
.open("test_partition", b"owned_partial")
.await
.unwrap();
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 50);
let read_buf = append.read_at(0, 50).await.unwrap().coalesce();
assert_eq!(read_buf, &all[..50]);
(append, _) = append
.append_owned(IoBuf::from(all[50..].to_vec()))
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context
.open("test_partition", b"owned_partial")
.await
.unwrap();
let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 500);
let read_buf = append.read_at(0, 500).await.unwrap().coalesce();
assert_eq!(read_buf, &all[..]);
});
}
#[test_traced("DEBUG")]
fn test_append_owned_bypass_with_buffered_tip() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"owned_buffered")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let all: Vec<u8> = (0..430).map(|i| (i % 239) as u8).collect();
(append, _) = append.append(&all[..30]).await.unwrap();
(append, _) = append
.append_owned(IoBuf::from(all[30..].to_vec()))
.await
.unwrap();
assert_eq!(append.size(), 430);
let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
assert_eq!(read_buf, &all[..]);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context
.open("test_partition", b"owned_buffered")
.await
.unwrap();
let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 430);
let read_buf = append.read_at(0, 430).await.unwrap().coalesce();
assert_eq!(read_buf, &all[..]);
});
}
#[test_traced("DEBUG")]
fn test_append_owned_exact_page_multiple_and_small() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"owned_exact")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let bulk: Vec<u8> = (0..412).map(|i| (i % 233) as u8).collect();
(append, _) = append
.append_owned(IoBuf::from(bulk.clone()))
.await
.unwrap();
assert_eq!(append.size(), 412);
let small: Vec<u8> = (0..10).map(|i| (i % 229) as u8).collect();
(append, _) = append
.append_owned(IoBuf::from(small.clone()))
.await
.unwrap();
assert_eq!(append.size(), 422);
let read_buf = append.read_at(0, 422).await.unwrap().coalesce();
assert_eq!(&read_buf.as_ref()[..412], &bulk[..]);
assert_eq!(&read_buf.as_ref()[412..], &small[..]);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context
.open("test_partition", b"owned_exact")
.await
.unwrap();
let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 422);
});
}
#[test_traced("DEBUG")]
fn test_append_owned_physical_bytes_match_buffered() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let data: Vec<u8> = (0..500).map(|i| (i % 251) as u8).collect();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, size) = context
.open("test_partition", b"phys_direct")
.await
.unwrap();
let mut direct = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(direct, _) = direct
.append_owned(IoBuf::from(data.clone()))
.await
.unwrap();
direct = direct.sync().await.unwrap();
drop(direct);
let (blob, size) = context
.open("test_partition", b"phys_buffered")
.await
.unwrap();
let mut buffered = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
for chunk in data.chunks(10) {
(buffered, _) = buffered.append(chunk).await.unwrap();
}
buffered = buffered.sync().await.unwrap();
drop(buffered);
let (blob_a, size_a) = context
.open("test_partition", b"phys_direct")
.await
.unwrap();
let (blob_b, size_b) = context
.open("test_partition", b"phys_buffered")
.await
.unwrap();
assert_eq!(size_a, size_b);
let bytes_a = blob_a
.read_at(0, size_a as usize, ReadOptions::default())
.await
.unwrap()
.coalesce();
let bytes_b = blob_b
.read_at(0, size_b as usize, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(bytes_a.as_ref(), bytes_b.as_ref());
});
}
#[test_traced("DEBUG")]
fn test_append_borrowed_large_takes_direct_path() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"borrowed_large")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let all: Vec<u8> = (0..530).map(|i| (i % 241) as u8).collect();
(append, _) = append.append(&all[..30]).await.unwrap();
(append, _) = append.append(&all[30..]).await.unwrap();
assert_eq!(append.size(), 530);
assert_eq!(append.buffer.len(), 15);
let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
assert_eq!(read_buf, &all[..]);
append = append.sync().await.unwrap();
drop(append);
let (blob, blob_size) = context
.open("test_partition", b"borrowed_large")
.await
.unwrap();
let append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 530);
let read_buf = append.read_at(0, 530).await.unwrap().coalesce();
assert_eq!(read_buf, &all[..]);
});
}
#[test]
fn test_sync_preserves_partial_tip_allocation() {
const PAGE_SIZE: NonZeroU16 = NZU16!(4096);
const BUFFER_SIZE: usize = 8192;
deterministic::Runner::default().start(|context| async move {
let cache = CacheRef::new(PAGE_SIZE, NZUsize!(1));
let (blob, size) = context
.open("test_partition", b"partial_seal")
.await
.unwrap();
let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache).await.unwrap();
(writer, _) = writer.append(&[7; 16]).await.unwrap();
let ptr = writer.buffer.parts().1.as_ptr();
writer = writer.sync().await.unwrap();
assert_eq!(writer.buffer.parts().1.as_ptr(), ptr);
(writer, _) = writer.append(&[7; 16]).await.unwrap();
assert_eq!(writer.buffer.parts().1.as_ptr(), ptr);
let (sealed, sync) = writer.seal().await.unwrap();
assert_eq!(
sealed.read_at(0, 32).await.unwrap().coalesce().as_ref(),
&[7; 32]
);
sync.await.unwrap();
});
}
#[test]
fn test_recovery_sync_preserves_truncate_and_append() {
deterministic::Runner::default().start(|context| async move {
let page_size = NZU16!(64);
let cache = CacheRef::new(page_size, NZUsize!(4));
let (blob, size) = context
.open("test_partition", b"compact_recovery")
.await
.unwrap();
let mut recovery = Recovery::open(blob, size, 256, cache.clone())
.await
.unwrap();
let mut expected: Vec<u8> = (0..129).collect();
(recovery, _) = recovery.append(&expected).await.unwrap();
recovery = recovery.sync().await.unwrap();
assert_eq!(recovery.buffer.len(), 1);
assert_eq!(recovery.buffer.capacity, 256);
recovery = recovery.truncate(95).await.unwrap();
expected.truncate(95);
let suffix = [7; 80];
(recovery, _) = recovery.append(&suffix).await.unwrap();
expected.extend_from_slice(&suffix);
recovery = recovery.sync().await.unwrap();
assert_eq!(recovery.buffer.capacity, 256);
drop(recovery);
let (blob, size) = context
.open("test_partition", b"compact_recovery")
.await
.unwrap();
let writer = Writer::new(blob, size, 256, cache).await.unwrap();
assert_eq!(writer.size(), expected.len() as u64);
assert_eq!(
writer
.read_at(0, expected.len())
.await
.unwrap()
.coalesce()
.as_ref(),
expected
);
});
}
#[test_traced("DEBUG")]
fn test_sync_uses_range_sync_for_single_flush() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
let data = b"hello world";
(append, _) = append.append(data).await.unwrap();
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 1);
append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 1);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
.await
.unwrap();
let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
assert_eq!(read.as_ref(), data);
});
}
#[test_traced("DEBUG")]
fn test_start_sync_persists_and_marks_clean() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let (mut writer, handle) = writer.start_sync().await.unwrap();
handle.await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
let data = b"hello world";
(writer, _) = writer.append(data).await.unwrap();
writer = writer.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 1);
let (_, handle) = writer.start_sync().await.unwrap();
handle.await.unwrap();
let (_, _, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 1);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
.await
.unwrap();
let read = reopened.read_at(0, data.len()).await.unwrap().coalesce();
assert_eq!(read.as_ref(), data);
});
}
#[test_traced("DEBUG")]
fn test_start_sync_flush_failure_returns_err() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let faults = WriteFaults::default();
let context = WriteFaultContext {
inner: context,
faults: faults.clone(),
};
let (blob, size) = context
.open("test_partition", b"start_sync_flush_failure")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(writer, _) = writer.append(&[1u8; 24]).await.unwrap();
writer = writer.sync().await.unwrap();
(writer, _) = writer.append(&[2u8; 8]).await.unwrap();
faults.arm();
assert!(writer.start_sync().await.is_err());
});
}
#[test_traced("DEBUG")]
fn test_needs_sync_tracks_sync_work() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, size) = context.open("test_partition", b"needs_sync").await.unwrap();
let (blob, pending) = DelayedSyncBlob::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert!(writer.needs_sync());
let mut writer = writer.sync().await.unwrap();
assert!(!writer.needs_sync());
(writer, _) = writer.append(&[1u8; 20]).await.unwrap();
assert!(writer.needs_sync());
writer = writer.sync().await.unwrap();
assert!(!writer.needs_sync());
(writer, _) = writer.append(&[2u8; 4]).await.unwrap();
let (writer, handle) = writer.start_sync().await.unwrap();
assert!(writer.needs_sync());
next_pending_sync(&pending).release.send(Ok(())).unwrap();
handle.await.unwrap();
assert!(writer.needs_sync());
let mut writer = writer.wait_for_sync().await.unwrap();
assert!(!writer.needs_sync());
(writer, _) = writer.append(&[3u8; 8]).await.unwrap();
let (writer, handle) = writer.start_sync().await.unwrap();
next_pending_sync(&pending)
.release
.send(Err(Error::Closed))
.unwrap();
assert!(handle.await.is_err());
assert!(writer.needs_sync());
assert!(writer.sync().await.is_err());
});
}
#[test_traced("DEBUG")]
fn test_sync_waits_for_outstanding_start_sync() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
let (writer, handle) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
let mut sync = Box::pin(writer.sync());
assert!(
sync.as_mut().now_or_never().is_none(),
"sync must wait for the outstanding start_sync handle"
);
let (_, _, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
deferred.release.send(Ok(())).unwrap();
sync.await.unwrap();
handle.await.unwrap();
let (_, _, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
});
}
#[test_traced("DEBUG")]
fn test_sync_after_start_sync_and_small_write_waits_before_range_sync() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
let (writer, handle) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
let (writer, _) = writer.append(b"hello world").await.unwrap();
let mut sync = Box::pin(writer.sync());
assert!(
sync.as_mut().now_or_never().is_none(),
"sync must join the outstanding barrier before flushing the small write"
);
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
deferred.release.send(Ok(())).unwrap();
sync.await.unwrap();
handle.await.unwrap();
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 1);
});
}
#[test_traced("DEBUG")]
fn test_write_flush_waits_for_outstanding_start_sync() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
let (mut writer, handle) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
let data = vec![7; BUFFER_SIZE + PAGE_SIZE.get() as usize];
let append = context.child("append").spawn(move |_| async move {
(writer, _) = writer.append(&data).await.unwrap();
writer
});
deferred
.blocked
.await
.expect("append never waited on start_sync");
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
deferred.release.send(Ok(())).unwrap();
let writer = append.await.unwrap();
handle.await.unwrap();
writer.sync().await.unwrap();
let (_, writes, full_syncs, _) = inner.snapshot();
assert!(writes > 0);
assert!(full_syncs > 0);
});
}
#[test_traced("DEBUG")]
fn test_seal_waits_for_outstanding_start_sync_before_flushing() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
let (mut writer, prior) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
(writer, _) = writer.append(b"hello world").await.unwrap();
let seal = context
.child("seal")
.spawn(move |_| async move { writer.seal().await });
deferred
.blocked
.await
.expect("seal never waited on start_sync");
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
deferred.release.send(Ok(())).unwrap();
let (sealed, sync) = seal.await.unwrap().unwrap();
prior.await.unwrap();
let deferred = next_pending_sync(&pending);
deferred.release.send(Ok(())).unwrap();
sync.await.unwrap();
let read = sealed
.read_at(0, b"hello world".len())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), b"hello world");
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 2);
assert_eq!(range_syncs, 0);
});
}
#[test_traced("DEBUG")]
fn test_snapshot_waits_for_outstanding_start_sync_before_flushing() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
let data = [7u8; PAGE_SIZE.get() as usize + 47];
let (mut writer, prior) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
(writer, _) = writer.append(&data).await.unwrap();
let snapshot = context.child("snapshot").spawn(move |_| async move {
let (writer, snapshot) = writer.snapshot().await.unwrap();
let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
assert_eq!(read.as_ref(), data.as_slice());
writer
});
deferred
.blocked
.await
.expect("snapshot never waited on start_sync");
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
deferred.release.send(Ok(())).unwrap();
let _writer = snapshot.await.unwrap();
prior.await.unwrap();
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
});
}
#[test_traced("DEBUG")]
fn test_snapshot_copies_partial_page_without_writing() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = Arc::new(SyncTrackingBlob::new());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let (writer, _) = writer.append(b"hello").await.unwrap();
let (writer, snapshot) = writer.snapshot().await.unwrap();
let (_, writes, _, _) = blob.snapshot();
assert_eq!(writes, 0);
let (writer, _) = writer.append(b" world").await.unwrap();
let _writer = writer.sync().await.unwrap();
let (_, writes, _, _) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(snapshot.size(), 5);
let read = snapshot.read_at(0, 5).await.unwrap().coalesce();
assert_eq!(read.as_ref(), b"hello");
});
}
#[test_traced]
fn test_try_snapshot_requires_written_full_pages() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = Arc::new(SyncTrackingBlob::new());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let (writer, _) = writer.append(b"hello").await.unwrap();
let view = writer.try_snapshot().expect("partial page needs no write");
assert_eq!(view.size(), 5);
let read = view.read_at(0, 5).await.unwrap().coalesce();
assert_eq!(read.as_ref(), b"hello");
let (_, writes, _, _) = blob.snapshot();
assert_eq!(writes, 0);
let page = vec![b'x'; PAGE_SIZE.get() as usize];
let (writer, _) = writer.append(&page).await.unwrap();
assert!(writer.try_snapshot().is_none());
let (writer, snapshot) = writer.snapshot().await.unwrap();
let (_, writes, _, _) = blob.snapshot();
assert_eq!(writes, 1);
let view = writer.try_snapshot().expect("full pages are written");
assert_eq!(view.size(), snapshot.size());
let mut replay = view
.replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
.unwrap();
let total = 5 + page.len();
assert!(replay.ensure(total).await.unwrap());
let bytes = replay.copy_to_bytes(total);
assert_eq!(&bytes[..5], b"hello");
assert!(bytes[5..].iter().all(|byte| *byte == b'x'));
assert!(!replay.ensure(1).await.unwrap());
});
}
#[test_traced("DEBUG")]
fn test_replay_waits_for_outstanding_start_sync_before_flushing() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
let (mut writer, prior) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
(writer, _) = writer.append(b"hello world").await.unwrap();
let replay = context.child("replay").spawn(move |_| async move {
let (writer, mut replay) = writer
.replay(NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap();
assert!(replay.ensure(1).await.unwrap());
assert_eq!(replay.chunk()[0], b'h');
writer
});
deferred
.blocked
.await
.expect("replay never waited on start_sync");
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
deferred.release.send(Ok(())).unwrap();
let _writer = replay.await.unwrap();
prior.await.unwrap();
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
});
}
#[test_traced("DEBUG")]
fn test_append_waits_for_outstanding_start_sync_before_writing() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let writer = Writer::new(blob, 0, BUFFER_SIZE, cache_ref).await.unwrap();
let (mut writer, prior) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
let target_size = (BUFFER_SIZE + PAGE_SIZE.get() as usize) as u64;
let append = context.child("append").spawn(move |_| async move {
(writer, _) = writer.append(&vec![0; target_size as usize]).await.unwrap();
writer
});
deferred
.blocked
.await
.expect("append never waited on start_sync");
let (_, writes, full_syncs, range_syncs) = inner.snapshot();
assert_eq!(writes, 0);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
deferred.release.send(Ok(())).unwrap();
let writer = append.await.unwrap();
prior.await.unwrap();
assert_eq!(writer.size(), target_size);
writer.sync().await.unwrap();
let (_, writes, full_syncs, _) = inner.snapshot();
assert!(writes > 0);
assert!(full_syncs > 0);
});
}
#[test_traced("DEBUG")]
fn test_recovery_truncate_shrink_waits_for_outstanding_start_sync_before_resizing() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let inner = SyncTrackingBlob::new();
let inner = Arc::new(inner);
let (blob, pending) = DelayedSyncBlob::new(inner.clone());
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Recovery::open(blob, 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data = vec![3; PAGE_SIZE.get() as usize * 2];
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
(writer, _) = writer.append(b"x").await.unwrap();
let (mut writer, prior) = writer.start_sync().await.unwrap();
let deferred = next_pending_sync(&pending);
let physical_size = inner.size();
let resize = context.child("resize_shrink").spawn(move |_| async move {
writer = writer.truncate(PAGE_SIZE.get() as u64).await.unwrap();
writer
});
deferred
.blocked
.await
.expect("resize shrink never waited on start_sync");
assert_eq!(
inner.size(),
physical_size,
"resize must not shrink the blob before the pending sync finishes"
);
deferred.release.send(Ok(())).unwrap();
let writer = resize.await.unwrap();
prior.await.unwrap();
assert_eq!(writer.size(), PAGE_SIZE.get() as u64);
assert!(inner.size() < physical_size);
});
}
#[test_traced("DEBUG")]
fn test_sync_uses_full_sync_after_prior_plain_flush() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data = vec![7u8; BUFFER_SIZE + 1];
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 2);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 2);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
(append, _) = append.append(b"tip").await.unwrap();
append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 3);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 1);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
.await
.unwrap();
let mut expected = data;
expected.extend_from_slice(b"tip");
let read = reopened
.read_at(0, expected.len())
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), expected.as_slice());
});
}
#[test_traced("DEBUG")]
fn test_sync_uses_full_sync_after_replay_plain_flush() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(append, _) = append.append(b"replayed").await.unwrap();
let (append, mut replay) = append
.replay(NZUsize!(1024), ReadOptions::default())
.await
.unwrap();
assert!(replay.ensure(b"replayed".len()).await.unwrap());
assert_eq!(replay.remaining(), b"replayed".len());
assert_eq!(replay.chunk(), b"replayed");
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
});
}
#[test_traced("DEBUG")]
fn test_replay_uses_read_options_for_refills_and_seek() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, size) = context
.open("test_partition", b"replay_read_options")
.await
.unwrap();
let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
let read_options = blob.read_options();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let page_size = PAGE_SIZE.get() as usize;
let data = vec![3; page_size * 2];
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
read_options.lock().clear();
let physical_page_size = page_size + CHECKSUM_SIZE as usize;
let (_, mut replay) = writer
.replay(NZUsize!(physical_page_size), ReadOptions::DONT_CACHE)
.await
.unwrap();
assert!(replay.ensure(1).await.unwrap());
replay.advance(page_size);
assert!(replay.ensure(1).await.unwrap());
assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE; 2]);
replay.seek_to(0).unwrap();
assert!(replay.ensure(1).await.unwrap());
assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE; 4]);
});
}
#[test_traced("DEBUG")]
fn test_recreated_sync_preserves_replay_plain_flush_barrier() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(append, _) = append.append(b"replayed").await.unwrap();
let (append, mut replay) = append
.replay(NZUsize!(1024), ReadOptions::default())
.await
.unwrap();
assert!(replay.ensure(b"replayed".len()).await.unwrap());
assert_eq!(replay.remaining(), b"replayed".len());
assert_eq!(replay.chunk(), b"replayed");
drop(replay);
drop(append);
let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
assert!(durable.is_empty());
assert_eq!(writes, 1);
assert_eq!(full_syncs, 0);
assert_eq!(range_syncs, 0);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let reopened = Recovery::open(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(reopened.size(), b"replayed".len() as u64);
reopened.sync().await.unwrap();
let (durable, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(durable.len(), blob.size() as usize);
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 0);
});
}
#[test_traced("DEBUG")]
fn test_recreated_sync_skips_barrier_after_invalid_truncation() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
append = append.sync().await.unwrap();
(append, _) = append.append(b"valid").await.unwrap();
append = append.sync().await.unwrap();
drop(append);
blob.write_at(blob.size(), b"junk", WriteOptions::default())
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(reopened.size(), b"valid".len() as u64);
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 2);
assert_eq!(full_syncs, 2);
assert_eq!(range_syncs, 1);
reopened.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 2);
assert_eq!(full_syncs, 2);
assert_eq!(range_syncs, 1);
});
}
#[test_traced("DEBUG")]
fn test_sync_fuses_partial_page_rewrite() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
append = append.sync().await.unwrap();
(append, _) = append.append(b"abc").await.unwrap();
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 1);
let slot0_offset = PAGE_SIZE.get() as u64;
let slot1_offset = slot0_offset + CHECKSUM_SLOT_SIZE as u64;
let slot0_before: Vec<u8> = blob
.read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
(append, _) = append.append(b"de").await.unwrap();
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 2);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 2);
let slot0_after: Vec<u8> = blob
.read_at(slot0_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(
slot0_before, slot0_after,
"protected slot must be resubmitted byte-identically"
);
let slot1_between: Vec<u8> = blob
.read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
(append, _) = append.append(b"fg").await.unwrap();
append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 3);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 3);
let slot1_after: Vec<u8> = blob
.read_at(slot1_offset, CHECKSUM_SLOT_SIZE, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(
slot1_between, slot1_after,
"protected slot must be resubmitted byte-identically"
);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let reopened = Writer::new(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
.await
.unwrap();
let read = reopened.read_at(0, 7).await.unwrap().coalesce();
assert_eq!(read.as_ref(), b"abcdefg");
});
}
#[test_traced("DEBUG")]
fn test_read_up_to_zero_len_truncates_buffer() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"read_up_to_zero_len")
.await
.unwrap();
assert_eq!(blob_size, 0);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(append, _) = append.append(&[1, 2, 3, 4]).await.unwrap();
let stale = vec![9u8, 8, 7, 6];
let (buf, read) = append.read_up_to(0, 0, stale).await.unwrap();
assert_eq!(read, 0);
assert_eq!(buf.len(), 0, "read_up_to must truncate returned buffer");
assert_eq!(buf.freeze().as_ref(), b"");
});
}
fn read_crc_record_from_page(page_bytes: &[u8]) -> Checksum {
let crc_start = page_bytes.len() - CHECKSUM_SIZE as usize;
Checksum::read(&mut Copying(&page_bytes[crc_start..])).unwrap()
}
struct PartialWriteBlob<B: Blob> {
inner: B,
writes: Arc<AtomicUsize>,
read_options: Arc<Mutex<Vec<ReadOptions>>>,
failed_write_len: Arc<AtomicUsize>,
fail_on: usize,
partial_len: usize,
}
impl<B: Blob> PartialWriteBlob<B> {
fn new(inner: B, fail_on: usize, partial_len: usize) -> Self {
Self {
inner,
writes: Arc::new(AtomicUsize::new(0)),
read_options: Arc::new(Mutex::new(Vec::new())),
failed_write_len: Arc::new(AtomicUsize::new(0)),
fail_on,
partial_len,
}
}
fn failed_write_len(&self) -> Arc<AtomicUsize> {
self.failed_write_len.clone()
}
fn write_count(&self) -> Arc<AtomicUsize> {
self.writes.clone()
}
fn read_options(&self) -> Arc<Mutex<Vec<ReadOptions>>> {
self.read_options.clone()
}
}
impl<B: Blob> crate::Blob for PartialWriteBlob<B> {
async fn read_at(
&self,
offset: u64,
len: usize,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
self.read_options.lock().push(options);
self.inner.read_at(offset, len, options).await
}
async fn read_at_buf(
&self,
offset: u64,
len: usize,
bufs: impl Into<IoBufsMut> + Send,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
self.read_options.lock().push(options);
self.inner.read_at_buf(offset, len, bufs, options).await
}
async fn write_at(
&self,
offset: u64,
bufs: impl Into<IoBufs> + Send,
options: WriteOptions,
) -> Result<(), Error> {
let bufs = bufs.into();
let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
if write == self.fail_on {
let bytes = bufs.coalesce();
self.failed_write_len.store(bytes.len(), Ordering::SeqCst);
let partial_len = self.partial_len.min(bytes.len());
self.inner
.write_at(offset, bytes.slice(..partial_len), options)
.await?;
if !options.contains(WriteOptions::SYNC) {
self.inner.sync().await?;
}
return Err(Error::Io(
std::io::Error::other("injected partial write").into(),
));
}
self.inner.write_at(offset, bufs, options).await
}
async fn resize(&self, len: u64) -> Result<(), Error> {
self.inner.resize(len).await
}
async fn sync(&self) -> Result<(), Error> {
self.inner.sync().await
}
async fn start_sync(&self) -> Handle<()> {
self.inner.start_sync().await
}
}
struct TornExtensionBlob<B: Blob> {
inner: B,
writes: Arc<AtomicUsize>,
fail_on: usize,
durable_payload_len: usize,
}
impl<B: Blob> crate::Blob for TornExtensionBlob<B> {
async fn read_at(
&self,
offset: u64,
len: usize,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
self.inner.read_at(offset, len, options).await
}
async fn read_at_buf(
&self,
offset: u64,
len: usize,
bufs: impl Into<IoBufsMut> + Send,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
self.inner.read_at_buf(offset, len, bufs, options).await
}
async fn write_at(
&self,
offset: u64,
bufs: impl Into<IoBufs> + Send,
options: WriteOptions,
) -> Result<(), Error> {
let write = self.writes.fetch_add(1, Ordering::SeqCst) + 1;
if write != self.fail_on {
return self.inner.write_at(offset, bufs, options).await;
}
let bytes = bufs.into().coalesce();
let footer_start = bytes
.len()
.checked_sub(CHECKSUM_SIZE as usize)
.expect("physical page must contain a checksum footer");
assert!(self.durable_payload_len <= footer_start);
let footer_offset = offset
.checked_add(footer_start as u64)
.ok_or(Error::OffsetOverflow)?;
let write_options = options.without(WriteOptions::SYNC);
self.inner
.write_at(
offset,
bytes.slice(..self.durable_payload_len),
write_options,
)
.await?;
self.inner
.write_at(footer_offset, bytes.slice(footer_start..), write_options)
.await?;
self.inner.sync().await?;
Err(Error::Io(
std::io::Error::other("injected torn extension").into(),
))
}
async fn resize(&self, len: u64) -> Result<(), Error> {
self.inner.resize(len).await
}
async fn sync(&self) -> Result<(), Error> {
self.inner.sync().await
}
async fn start_sync(&self) -> Handle<()> {
self.inner.start_sync().await
}
}
#[test_traced("DEBUG")]
fn test_torn_extension_with_new_footer_recovers_previous_prefix() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"torn_extension_footer")
.await
.unwrap();
let blob = Arc::new(blob);
let write_count = Arc::new(AtomicUsize::new(0));
let faulty_blob = TornExtensionBlob {
inner: blob.clone(),
writes: write_count.clone(),
fail_on: 2,
durable_payload_len: 4,
};
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(faulty_blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(writer, _) = writer.append(b"abc").await.unwrap();
writer = writer.sync().await.unwrap();
assert_eq!(write_count.load(Ordering::SeqCst), 1);
(writer, _) = writer.append(b"def").await.unwrap();
assert!(writer.sync().await.is_err(), "extension write should fail");
assert_eq!(write_count.load(Ordering::SeqCst), 2);
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
assert_eq!(&page.as_ref()[..6], b"abcd\0\0");
let checksum = read_crc_record_from_page(page.as_ref());
assert_eq!(checksum.len1, 3);
assert_eq!(checksum.len2, 6);
drop(blob);
let (blob, blob_size) = context
.open("test_partition", b"torn_extension_footer")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let recovered = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(recovered.size(), 3);
assert_eq!(
recovered
.recoverable_prefix_len(0, NZUsize!(BUFFER_SIZE), ReadOptions::default())
.await
.unwrap(),
3
);
let data = recovered.read_at(0, 3).await.unwrap().coalesce();
assert_eq!(data.as_ref(), b"abc");
});
}
struct DelayedReadBlob<B: Blob> {
inner: B,
offset: u64,
len: usize,
reads: AtomicUsize,
started: Mutex<Option<oneshot::Sender<()>>>,
release: Mutex<Option<oneshot::Receiver<()>>>,
}
impl<B: Blob> DelayedReadBlob<B> {
const fn new(
inner: B,
offset: u64,
len: usize,
started: oneshot::Sender<()>,
release: oneshot::Receiver<()>,
) -> Self {
Self {
inner,
offset,
len,
reads: AtomicUsize::new(0),
started: Mutex::new(Some(started)),
release: Mutex::new(Some(release)),
}
}
}
impl<B: Blob> crate::Blob for DelayedReadBlob<B> {
async fn read_at(
&self,
offset: u64,
len: usize,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
if offset == self.offset
&& len == self.len
&& self.reads.fetch_add(1, Ordering::SeqCst) == 0
{
let bytes = self.inner.read_at(offset, len, options).await?;
let sender = self
.started
.lock()
.take()
.expect("delayed read start signal consumed more than once");
let _ = sender.send(());
let release = self
.release
.lock()
.take()
.expect("delayed read release receiver consumed more than once");
release.await.expect("release signal dropped");
return Ok(bytes);
}
self.inner.read_at(offset, len, options).await
}
async fn read_at_buf(
&self,
offset: u64,
len: usize,
bufs: impl Into<IoBufsMut> + Send,
options: ReadOptions,
) -> Result<IoBufsMut, Error> {
if offset == self.offset
&& len == self.len
&& self.reads.fetch_add(1, Ordering::SeqCst) == 0
{
let bytes = self.inner.read_at_buf(offset, len, bufs, options).await?;
let sender = self
.started
.lock()
.take()
.expect("delayed read start signal consumed more than once");
let _ = sender.send(());
let release = self
.release
.lock()
.take()
.expect("delayed read release receiver consumed more than once");
release.await.expect("release signal dropped");
return Ok(bytes);
}
self.inner.read_at_buf(offset, len, bufs, options).await
}
async fn write_at(
&self,
offset: u64,
bufs: impl Into<IoBufs> + Send,
options: WriteOptions,
) -> Result<(), Error> {
self.inner.write_at(offset, bufs, options).await
}
async fn resize(&self, len: u64) -> Result<(), Error> {
self.inner.resize(len).await
}
async fn sync(&self) -> Result<(), Error> {
self.inner.sync().await
}
async fn start_sync(&self) -> Handle<()> {
self.inner.start_sync().await
}
}
const DUMMY_MARKER: [u8; 6] = [0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF];
#[rstest::rstest]
#[case(103)]
#[case(4084)]
#[case(4096)]
fn test_to_physical_pages_preserves_chunk_owners(#[case] page_size: u16) {
deterministic::Runner::default().start(|context| async move {
let page_size = NonZeroU16::new(page_size).unwrap();
let cache = CacheRef::new(page_size, NZUsize!(4));
let (blob, size) = context
.open("test_partition", b"physical_chunks")
.await
.unwrap();
let page_size = usize::from(page_size.get());
let capacity = page_size * 32;
let writer = Writer::new(blob, size, capacity, cache).await.unwrap();
let data: Vec<_> = (0..page_size * 17 + 17).map(|i| (i % 251) as u8).collect();
let mut buffer = Buffer::from(0, &[], capacity, page_size);
for chunk in data.chunks(page_size) {
assert!(!buffer.append(chunk));
}
assert!(buffer.parts().0.chunk_count() >= 4);
let (prefix, tail) = buffer.parts();
let pointers: Vec<_> = prefix
.iter()
.map(AsRef::as_ref)
.chain(std::iter::once(tail))
.flat_map(|chunk| chunk.chunks_exact(page_size))
.map(|page| page.as_ptr())
.collect();
let full_pages = buffer.drain_full_pages();
let durable = ActiveChecksum::new(Slot::Second, 7, Crc32::checksum(&data[..7]));
let (physical, partial) =
writer.to_physical_pages(&full_pages, buffer.parts().1, true, None, Some(&durable));
assert_eq!(physical.chunk_count(), 17 * 2 + 1);
assert_eq!(partial.unwrap().len, 17);
for (page, ptr) in pointers.into_iter().enumerate() {
assert_eq!(physical.chunk_at(page * 2).unwrap().as_ptr(), ptr);
}
let physical = physical.coalesce();
let physical_size = page_size + CHECKSUM_SIZE as usize;
for (idx, page) in physical.as_ref().chunks_exact(physical_size).enumerate() {
let logical = &data[idx * page_size..data.len().min((idx + 1) * page_size)];
assert_eq!(&page[..logical.len()], logical);
assert!(page[logical.len()..page_size].iter().all(|byte| *byte == 0));
assert_eq!(
Checksum::validate_page(page).unwrap().len as usize,
logical.len()
);
let record = read_crc_record_from_page(page);
if idx == 0 {
assert_eq!(record.get_slot(Slot::Second), (durable.len, durable.crc));
} else {
assert_eq!(record.get_slot(Slot::Second), (0, 0));
}
}
});
}
#[test_traced("DEBUG")]
fn test_crc_slot1_protected() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let slot0_offset = PAGE_SIZE.get() as u64;
let slot1_offset = PAGE_SIZE.get() as u64 + 6;
let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(11..=30).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"slot1_prot").await.unwrap();
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert!(
crc.len2 > crc.len1,
"Slot 1 should be authoritative (len2={} > len1={})",
crc.len2,
crc.len1
);
let slot1_before: Vec<u8> = blob
.read_at(slot1_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let slot0_mangled: Vec<u8> = blob
.read_at(slot0_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(slot0_mangled, DUMMY_MARKER, "Mangle failed");
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(31..=50).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, _) = context.open("test_partition", b"slot1_prot").await.unwrap();
let slot0_after: Vec<u8> = blob
.read_at(slot0_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_ne!(
slot0_after, DUMMY_MARKER,
"Slot 0 should have been overwritten with new CRC"
);
let slot1_after: Vec<u8> = blob
.read_at(slot1_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(
slot1_before, slot1_after,
"Slot 1 was modified! Protected region violated."
);
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert_eq!(crc.len1, 50, "Slot 0 should have len=50");
});
}
#[test_traced("DEBUG")]
fn test_crc_slot0_protected() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let slot0_offset = PAGE_SIZE.get() as u64;
let slot1_offset = PAGE_SIZE.get() as u64 + 6;
let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(11..=30).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(31..=50).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"slot0_prot").await.unwrap();
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert!(
crc.len1 > crc.len2,
"Slot 0 should be authoritative (len1={} > len2={})",
crc.len1,
crc.len2
);
let slot0_before: Vec<u8> = blob
.read_at(slot0_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
blob.write_at(slot1_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let slot1_mangled: Vec<u8> = blob
.read_at(slot1_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(slot1_mangled, DUMMY_MARKER, "Mangle failed");
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(51..=70).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, _) = context.open("test_partition", b"slot0_prot").await.unwrap();
let slot1_after: Vec<u8> = blob
.read_at(slot1_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_ne!(
slot1_after, DUMMY_MARKER,
"Slot 1 should have been overwritten with new CRC"
);
let slot0_after: Vec<u8> = blob
.read_at(slot0_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(
slot0_before, slot0_after,
"Slot 0 was modified! Protected region violated."
);
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert_eq!(crc.len2, 70, "Slot 1 should have len=70");
});
}
#[test_traced("DEBUG")]
fn test_data_prefix_preserved_when_extending() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let (blob, _) = context
.open("test_partition", b"prefix_test")
.await
.unwrap();
let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let data1: Vec<u8> = (1..=20).collect();
(append, _) = append.append(&data1).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"prefix_test")
.await
.unwrap();
assert_eq!(size, physical_page_size as u64);
let prefix_before: Vec<u8> = blob
.read_at(0, 20, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
blob.write_at(25, DUMMY_MARKER.to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(21..=40).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, _) = context
.open("test_partition", b"prefix_test")
.await
.unwrap();
let prefix_after: Vec<u8> = blob
.read_at(0, 20, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(prefix_before, prefix_after, "Data prefix was modified!");
let overwritten: Vec<u8> = blob
.read_at(25, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(
overwritten,
vec![26, 27, 28, 29, 30, 31],
"New data should overwrite padding area"
);
});
}
#[test_traced("DEBUG")]
fn test_crc_slot_protection_across_page_boundary() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let slot0_offset = PAGE_SIZE.get() as u64;
let slot1_offset = PAGE_SIZE.get() as u64 + 6;
let (blob, _) = context.open("test_partition", b"boundary").await.unwrap();
let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&(1..=50).collect::<Vec<u8>>()).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(51..=80).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
let slot1_before: Vec<u8> = blob
.read_at(slot1_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(81..=120).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context.open("test_partition", b"boundary").await.unwrap();
assert_eq!(size, (physical_page_size * 2) as u64, "Should have 2 pages");
let slot0_after: Vec<u8> = blob
.read_at(slot0_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_ne!(
slot0_after, DUMMY_MARKER,
"Slot 0 should have full-page CRC"
);
let slot1_after: Vec<u8> = blob
.read_at(slot1_offset, 6, ReadOptions::default())
.await
.unwrap()
.coalesce()
.freeze()
.into();
assert_eq!(
slot1_before, slot1_after,
"Slot 1 was modified during page boundary crossing!"
);
let page0 = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc0 = read_crc_record_from_page(page0.as_ref());
assert_eq!(
crc0.len1,
PAGE_SIZE.get(),
"Slot 0 should have full page length"
);
let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 120);
let all_data: Vec<u8> = append.read_at(0, 120).await.unwrap().coalesce().into();
let expected: Vec<u8> = (1..=120).collect();
assert_eq!(all_data, expected);
});
}
#[test_traced("DEBUG")]
fn test_crc_fallback_on_corrupted_primary() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let crc2_offset = PAGE_SIZE.get() as u64 + 8;
let (blob, _) = context
.open("test_partition", b"crc_fallback")
.await
.unwrap();
let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let data1: Vec<u8> = (1..=10).collect();
(append, _) = append.append(&data1).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"crc_fallback")
.await
.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(11..=30).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"crc_fallback")
.await
.unwrap();
let blob = Arc::new(blob);
assert_eq!(size, physical_page_size as u64);
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert!(
crc.len2 > crc.len1,
"Slot 1 should be authoritative (len2={} > len1={})",
crc.len2,
crc.len1
);
assert_eq!(crc.len2, 30, "Slot 1 should have len=30");
assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 30);
let all_data: Vec<u8> = append.read_at(0, 30).await.unwrap().coalesce().into();
let expected: Vec<u8> = (1..=30).collect();
assert_eq!(all_data, expected);
drop(append);
blob.write_at(
crc2_offset,
vec![0xDE, 0xAD, 0xBE, 0xEF],
WriteOptions::default(),
)
.await
.unwrap();
blob.sync().await.unwrap();
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert_eq!(crc.len2, 30, "len2 should still be 30 after corruption");
assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be our corrupted value");
let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(
append.size(),
10,
"Should fall back to slot 0's 10 bytes after primary CRC corruption"
);
let fallback_data: Vec<u8> = append.read_at(0, 10).await.unwrap().coalesce().into();
assert_eq!(
fallback_data, data1,
"Fallback data should match original 10 bytes"
);
let result = append.read_at(0, 11).await;
assert!(result.is_err(), "Reading beyond fallback size should fail");
});
}
#[test_traced("DEBUG")]
fn test_non_last_page_rejects_partial_fallback() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let page0_crc2_offset = PAGE_SIZE.get() as u64 + 8;
let (blob, _) = context
.open("test_partition", b"non_last_page")
.await
.unwrap();
let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"non_last_page")
.await
.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(11..=PAGE_SIZE.get() as u8).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"non_last_page")
.await
.unwrap();
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert_eq!(crc.len1, 10, "Slot 0 should have len=10");
assert_eq!(
crc.len2,
PAGE_SIZE.get(),
"Slot 1 should have len=103 (full page)"
);
assert!(crc.len2 > crc.len1, "Slot 1 should be authoritative");
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append
.append(&(104..=113).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"non_last_page")
.await
.unwrap();
let blob = Arc::new(blob);
assert_eq!(
size,
(physical_page_size * 2) as u64,
"Should have 2 physical pages"
);
let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 113);
let all_data: Vec<u8> = append.read_at(0, 113).await.unwrap().coalesce().into();
let expected: Vec<u8> = (1..=113).collect();
assert_eq!(all_data, expected);
drop(append);
blob.write_at(
page0_crc2_offset,
vec![0xDE, 0xAD, 0xBE, 0xEF],
WriteOptions::default(),
)
.await
.unwrap();
blob.sync().await.unwrap();
let page = blob
.read_at(0, physical_page_size, ReadOptions::default())
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert_eq!(crc.len2, PAGE_SIZE.get(), "len2 should still be 103");
assert_eq!(crc.crc2, 0xDEADBEEF, "crc2 should be corrupted");
assert_eq!(crc.len1, 10, "Fallback slot 0 has partial length");
let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 113);
let result = append.read_at(0, 10).await;
assert!(
result.is_err(),
"Reading from corrupted non-last page via Append should fail, but got: {:?}",
result
);
drop(append);
let (blob, size) = context
.open("test_partition", b"non_last_page")
.await
.unwrap();
let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let (_, mut replay) = append
.replay(NZUsize!(1024), ReadOptions::default())
.await
.unwrap();
let result = replay.ensure(1).await;
assert!(
result.is_err(),
"Reading from corrupted non-last page via Replay should fail, but got: {:?}",
result
);
});
}
#[test]
fn test_replay_rejects_short_interior_page() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let page_size: u64 = PAGE_SIZE.widen();
let (blob, _) = context
.open("test_partition", b"short_bound")
.await
.unwrap();
let mut append = Writer::new(blob, 0, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&(1..=10).collect::<Vec<u8>>()).await.unwrap();
append = append.sync().await.unwrap();
(append, _) = append
.append(&(11..=113).collect::<Vec<u8>>())
.await
.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"short_bound")
.await
.unwrap();
let blob = Arc::new(blob);
let append = Writer::new(blob.clone(), size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let (_, mut replay) = append
.replay(NZUsize!(1024), ReadOptions::default())
.await
.unwrap();
assert!(replay.ensure(113).await.unwrap());
assert_eq!(replay.remaining(), 113);
assert_eq!(
replay.copy_to_bytes(113).as_ref(),
(1..=113).collect::<Vec<u8>>()
);
drop(replay);
let page = blob
.read_at(
0,
usize::from(PAGE_SIZE.get()) + CHECKSUM_SIZE as usize,
ReadOptions::default(),
)
.await
.unwrap()
.coalesce();
let crc = read_crc_record_from_page(page.as_ref());
assert_eq!((crc.len1, crc.len2), (10, PAGE_SIZE.get()));
blob.write_at(
page_size + 8,
vec![0xDE, 0xAD, 0xBE, 0xEF],
WriteOptions::default(),
)
.await
.unwrap();
blob.sync().await.unwrap();
let append = Writer::new(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 113);
let (_, mut replay) = append
.replay(NZUsize!(1024), ReadOptions::default())
.await
.unwrap();
assert_eq!(replay.blob_size(), 113);
assert!(
matches!(replay.ensure(50).await, Err(Error::InvalidChecksum)),
"an interior page covering fewer bytes than a page must fail validation"
);
replay.seek_to(40).unwrap();
assert!(matches!(
replay.ensure(1).await,
Err(Error::InvalidChecksum)
));
assert!(replay.chunk().is_empty());
assert_eq!(replay.remaining(), 0);
});
}
#[test]
fn test_recovery_truncate_partial_shrink_uses_uncached_read_hint() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let (blob, size) = context
.open("test_partition", b"resize_read_options")
.await
.unwrap();
let blob = PartialWriteBlob::new(blob, usize::MAX, 0);
let read_options = blob.read_options();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data = vec![7; PAGE_SIZE.get() as usize];
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
writer.truncate(50).await.unwrap();
assert_eq!(*read_options.lock(), vec![ReadOptions::DONT_CACHE]);
});
}
#[test]
fn test_recovery_truncate_shrink_validates_crc() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let (blob, size) = context
.open("test_partition", b"resize_crc_test")
.await
.unwrap();
let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
let data: Vec<u8> = (0..=249).collect();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
assert_eq!(append.size(), 250);
drop(append);
let (blob, size) = context
.open("test_partition", b"resize_crc_test")
.await
.unwrap();
assert_eq!(size as usize, physical_page_size * 3);
let page1_crc_offset = (physical_page_size * 2 - CHECKSUM_SIZE as usize) as u64;
blob.write_at(
page1_crc_offset,
vec![0xFF; CHECKSUM_SIZE as usize],
WriteOptions::default(),
)
.await
.unwrap();
blob.sync().await.unwrap();
let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 250);
let result = append.truncate(150).await.err();
assert!(
matches!(result, Some(crate::Error::InvalidChecksum)),
"Expected InvalidChecksum when shrinking to corrupted page, got: {:?}",
result
);
});
}
#[test]
fn test_recovery_preserves_cached_prefix_on_publication() {
let page = PAGE_SIZE.get() as u64;
for end in [None, Some(page * 2 + 10), Some(page + 10)] {
deterministic::Runner::default().start(|context| async move {
let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, size) = context.open("recovery-cache", b"blob").await.unwrap();
let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache)
.await
.unwrap();
let page = PAGE_SIZE.get() as usize;
(recovery, _) = recovery.append(&vec![0xA5; page * 2 + 20]).await.unwrap();
recovery = recovery.sync().await.unwrap();
recovery.read_at(0, page).await.unwrap();
if let Some(end) = end {
recovery = recovery.truncate(end).await.unwrap();
}
let writer: Writer<_> = recovery.into();
let mut cached = vec![0; page];
assert!(writer.try_read_sync_into(&mut cached, 0));
assert_eq!(cached, vec![0xA5; page]);
});
}
}
#[test]
fn test_recovery_truncate_direct_append_replaces_cached_pages() {
deterministic::Runner::default().start(|context| async move {
let cache = CacheRef::new(PAGE_SIZE, NZUsize!(2));
let (blob, size) = context.open("recovery-cache", b"direct").await.unwrap();
let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache.clone())
.await
.unwrap();
let page = PAGE_SIZE.get() as usize;
let mut model: Vec<u8> = (0..5 * page + 20).map(|i| (i % 251) as u8).collect();
(recovery, _) = recovery.append(&model).await.unwrap();
recovery = recovery.sync().await.unwrap();
cache.clear();
recovery.read_at(page as u64, page).await.unwrap();
recovery.read_at((2 * page) as u64, page).await.unwrap();
recovery = recovery.truncate((page + 10) as u64).await.unwrap();
model.truncate(page + 10);
let replacement = vec![0xCC; 3 * page + 7];
(recovery, _) = recovery
.append_owned(replacement.clone().into())
.await
.unwrap();
model.extend_from_slice(&replacement);
assert_eq!(
recovery
.read_at((2 * page) as u64, page)
.await
.unwrap()
.coalesce()
.as_ref(),
&model[2 * page..3 * page]
);
assert_eq!(
recovery
.read_at(0, model.len())
.await
.unwrap()
.coalesce()
.as_ref(),
model
);
recovery = recovery.sync().await.unwrap();
let writer: Writer<_> = recovery.into();
let (mut writer, snapshot) = writer.snapshot().await.unwrap();
(writer, _) = writer.append(&vec![0xDD; page - 17]).await.unwrap();
writer = writer.sync().await.unwrap();
assert_eq!(
snapshot
.read_at(0, model.len())
.await
.unwrap()
.coalesce()
.as_ref(),
model
);
model.extend_from_slice(&vec![0xDD; page - 17]);
assert_eq!(
writer
.read_at(0, model.len())
.await
.unwrap()
.coalesce()
.as_ref(),
model
);
});
}
#[test]
fn test_cancelled_recovery_read_cannot_repopulate_after_truncate() {
let cfg =
deterministic::Config::default().with_timeout(Some(std::time::Duration::from_secs(5)));
deterministic::Runner::new(cfg).start(|context| async move {
let page = PAGE_SIZE.get() as usize;
let physical = page + CHECKSUM_SIZE as usize;
let cache = CacheRef::new(PAGE_SIZE, NZUsize!(1));
let (inner, size) = context.open("recovery-cache", b"cancelled").await.unwrap();
let (started_tx, started_rx) = oneshot::channel();
let (release_tx, release_rx) = oneshot::channel();
let blob =
DelayedReadBlob::new(inner, physical as u64, physical, started_tx, release_rx);
let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache.clone())
.await
.unwrap();
(recovery, _) = recovery.append(&vec![0xAA; 3 * page + 20]).await.unwrap();
recovery = recovery.sync().await.unwrap();
cache.clear();
{
let mut read = Box::pin(recovery.read_at(page as u64, page));
commonware_macros::select! {
_ = started_rx => {},
_ = read.as_mut() => panic!("read completed before release"),
}
}
recovery = recovery.truncate((page + 10) as u64).await.unwrap();
(recovery, _) = recovery.append(&vec![0xBB; page - 10]).await.unwrap();
recovery = recovery.sync().await.unwrap();
recovery.read_at(0, page).await.unwrap();
let _ = release_tx.send(());
let actual = recovery
.read_at(page as u64, page)
.await
.unwrap()
.coalesce();
assert_eq!(&actual.as_ref()[..10], &[0xAA; 10]);
assert_eq!(&actual.as_ref()[10..], vec![0xBB; page - 10]);
});
}
#[test]
fn test_recovery_truncate_replaces_cached_tail() {
deterministic::Runner::default().start(|context| async move {
let cache = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, size) = context.open("recovery-cache", b"truncate").await.unwrap();
let mut recovery = Recovery::open(blob, size, BUFFER_SIZE, cache)
.await
.unwrap();
let page = PAGE_SIZE.get() as usize;
(recovery, _) = recovery.append(&vec![0xAA; page * 2 + 16]).await.unwrap();
recovery = recovery.sync().await.unwrap();
recovery.read_at(0, page * 2).await.unwrap();
let mut cached = vec![0; page * 2];
assert!(recovery.try_read_sync_into(&mut cached, 0));
assert_eq!(cached, vec![0xAA; page * 2]);
recovery = recovery.truncate((page + 16) as u64).await.unwrap();
recovery.read_at(0, page).await.unwrap();
recovery = recovery.truncate((page + 8) as u64).await.unwrap();
assert!(recovery.try_read_sync_into(&mut cached[..page], 0));
(recovery, _) = recovery.append(&vec![0xBB; page - 8]).await.unwrap();
recovery = recovery.sync().await.unwrap();
let mut rewritten = vec![0; page];
assert!(recovery.try_read_sync_into(&mut rewritten, page as u64));
assert_eq!(&rewritten[..8], &[0xAA; 8]);
assert_eq!(&rewritten[8..], vec![0xBB; page - 8]);
});
}
#[test]
fn test_snapshot_fetch_populates_shared_cache_across_append() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let page_size = PAGE_SIZE.get() as usize;
let physical_page_size = page_size + CHECKSUM_SIZE as usize;
let (inner, blob_size) = context
.open("test_partition", b"snapshot_resize_cache")
.await
.unwrap();
let (started_tx, started_rx) = oneshot::channel();
let (release_tx, release_rx) = oneshot::channel();
let blob = DelayedReadBlob::new(
inner,
physical_page_size as u64,
physical_page_size,
started_tx,
release_rx,
);
let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let old_page0 = vec![0x11u8; page_size];
let old_page1 = vec![0x22u8; page_size];
(writer, _) = writer.append(&old_page0).await.unwrap();
(writer, _) = writer.append(&old_page1).await.unwrap();
writer = writer.sync().await.unwrap();
writer.cache_ref.clear();
let (mut writer, snapshot) = writer.snapshot().await.unwrap();
let snapshot_task = context
.child("snapshot")
.spawn(move |_| async move { snapshot.read_at(page_size as u64, page_size).await });
started_rx.await.expect("snapshot read never started");
let new_page1 = vec![0x33u8; page_size];
(writer, _) = writer.append(&new_page1).await.unwrap();
writer = writer.sync().await.unwrap();
let _ = release_tx.send(());
let stale = snapshot_task
.await
.expect("snapshot task failed")
.expect("snapshot read failed")
.coalesce();
assert_eq!(stale.as_ref(), old_page1.as_slice());
let mut probe = vec![0u8; page_size];
assert!(writer.try_read_sync_into(&mut probe, page_size as u64));
assert_eq!(probe, old_page1);
});
}
#[test]
fn test_append_preserves_snapshot_partial_page() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, blob_size) = context
.open("test_partition", b"snapshot_blocks_shrink")
.await
.unwrap();
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let page_size = PAGE_SIZE.get() as usize;
let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2 + 7).collect();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
let tail_ptr = append.buffer.parts().1.as_ptr();
let (mut append, snapshot) = append.snapshot().await.unwrap();
let snapshot_clone = snapshot.clone();
let snapshot_size = snapshot.size();
let read = snapshot.read_at(0, data.len()).await.unwrap().coalesce();
assert_eq!(read.as_ref(), data.as_slice());
(append, _) = append.append(&[0; 3]).await.unwrap();
assert_eq!(append.size(), snapshot_size + 3);
assert_eq!(append.buffer.parts().1.as_ptr(), tail_ptr);
let original = snapshot_clone
.read_at(0, data.len())
.await
.unwrap()
.coalesce();
assert_eq!(original.as_ref(), data.as_slice());
assert_eq!(snapshot_clone.size(), snapshot_size);
});
}
#[test]
fn test_snapshot_read_many_into_matches_read_at() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, blob_size) = context
.open("test_partition", b"snapshot_read_many")
.await
.unwrap();
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let item_size = 4;
let page_size = PAGE_SIZE.get() as usize;
let total = page_size * 3 + 13;
let data: Vec<u8> = (0u8..=255).cycle().take(total).collect();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
let (_, snapshot) = append.snapshot().await.unwrap();
let offsets = [
0,
(page_size - item_size) as u64,
(page_size + 5) as u64,
(page_size * 3 + 2) as u64,
];
let mut batch = vec![0u8; offsets.len() * item_size];
snapshot
.read_many_into(&mut batch, &offsets, NZUsize!(item_size))
.await
.unwrap();
for (item, offset) in batch.chunks_exact(item_size).zip(offsets) {
let single = snapshot
.read_at(offset, item_size)
.await
.unwrap()
.coalesce();
assert_eq!(item, single.as_ref());
assert_eq!(item, &data[offset as usize..offset as usize + item_size]);
}
});
}
#[test]
fn test_snapshot_try_read_sync_prefers_snapshot_tail() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, blob_size) = context
.open("test_partition", b"snapshot_try_read_tail")
.await
.unwrap();
let mut append = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let page_size = PAGE_SIZE.get() as usize;
(append, _) = append.append(&vec![0xAA; page_size]).await.unwrap();
append = append.sync().await.unwrap();
let tail = b"oldtail";
(append, _) = append.append(tail).await.unwrap();
let (append, snapshot) = append.snapshot().await.unwrap();
let poison = vec![0xBB; page_size];
assert_eq!(
append.cache_ref.cache(append.id, &poison, page_size as u64),
0
);
let mut read = vec![0; tail.len()];
assert!(snapshot.try_read_sync_into(&mut read, page_size as u64));
assert_eq!(read.as_slice(), tail);
});
}
#[test]
fn test_recovery_truncate_same_size_preserves_contents() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, blob_size) = context
.open("test_partition", b"resize_same_size")
.await
.unwrap();
let mut append = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
(append, _) = append.append(b"hello world").await.unwrap();
assert_eq!(append.size(), 11);
append = append.truncate(11).await.unwrap();
assert_eq!(append.size(), 11);
let read = append.read_at(0, 11).await.unwrap().coalesce();
assert_eq!(read.as_ref(), b"hello world");
});
}
#[test]
fn test_recovery_truncate_same_page_shrink_reopens_at_shorter_size() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let data: Vec<u8> = (0..50).collect();
let (blob, size) = context
.open("test_partition", b"same_page_shrink")
.await
.unwrap();
let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
append = append.truncate(45).await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"same_page_shrink")
.await
.unwrap();
let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 45);
let read = append.read_at(0, 45).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data[..45]);
});
}
#[test_traced("DEBUG")]
fn test_shrink_then_torn_rewrite_does_not_resurrect() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context
.open("test_partition", b"shrink_torn")
.await
.unwrap();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Recovery::open(blob.clone(), blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (1u8..=80).collect();
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
writer = writer.truncate(50).await.unwrap();
drop(writer);
let page_size = u64::from(PAGE_SIZE.get());
blob.write_at(
page_size,
80u16.to_be_bytes().to_vec(),
WriteOptions::default(),
)
.await
.unwrap();
blob.sync().await.unwrap();
drop(blob);
let (blob, blob_size) = context
.open("test_partition", b"shrink_torn")
.await
.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let recovered = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(recovered.size(), 50);
let read = recovered.read_at(0, 50).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data[..50]);
});
}
#[test]
fn test_recovery_truncate_same_page_shrink_survives_interrupted_crc_stage() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let data: Vec<u8> = (0..50).collect();
let (blob, size) = context
.open("test_partition", b"same_page_shrink_interrupted")
.await
.unwrap();
let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&data[..40]).await.unwrap();
append = append.sync().await.unwrap();
(append, _) = append.append(&data[40..]).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"same_page_shrink_interrupted")
.await
.unwrap();
let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
let write_count = faulty_blob.write_count();
let failed_write_len = faulty_blob.failed_write_len();
let append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert!(
append.truncate(45).await.is_err(),
"phase-1 partial write should fail"
);
assert_eq!(write_count.load(Ordering::SeqCst), 1);
assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
let (blob, size) = context
.open("test_partition", b"same_page_shrink_interrupted")
.await
.unwrap();
let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 50);
let read = append.read_at(0, 50).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data);
});
}
#[test]
fn test_recovery_truncate_same_page_shrink_survives_interrupted_len_stage() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
const LARGE_BUFFER_SIZE: usize = 1_200;
let cache_ref = CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
let (blob, size) = context
.open("test_partition", b"same_page_shrink_len_stage")
.await
.unwrap();
let mut append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&data[..255]).await.unwrap();
append = append.sync().await.unwrap();
(append, _) = append.append(&data[255..]).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"same_page_shrink_len_stage")
.await
.unwrap();
let faulty_blob = PartialWriteBlob::new(blob, 2, 1);
let write_count = faulty_blob.write_count();
let failed_write_len = faulty_blob.failed_write_len();
let append = Recovery::open(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert!(
append.truncate(257).await.is_err(),
"length-stage partial write should fail"
);
assert_eq!(write_count.load(Ordering::SeqCst), 2);
assert_eq!(failed_write_len.load(Ordering::SeqCst), 2);
let (blob, size) = context
.open("test_partition", b"same_page_shrink_len_stage")
.await
.unwrap();
let append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 300);
let read = append.read_at(0, 300).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data);
});
}
#[test]
fn test_recovery_truncate_same_page_shrink_preserves_validated_fallback_slot() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let data: Vec<u8> = (0..52).collect();
let (blob, size) = context
.open("test_partition", b"same_page_shrink_fallback_slot")
.await
.unwrap();
let blob = Arc::new(blob);
let faulty_blob = PartialWriteBlob::new(blob.clone(), 4, 3);
let write_count = faulty_blob.write_count();
let failed_write_len = faulty_blob.failed_write_len();
let mut append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&data[..48]).await.unwrap();
append = append.sync().await.unwrap();
assert_eq!(write_count.load(Ordering::SeqCst), 1);
(append, _) = append.append(&data[48..50]).await.unwrap();
append = append.sync().await.unwrap();
assert_eq!(write_count.load(Ordering::SeqCst), 2);
(append, _) = append.append(&data[50..]).await.unwrap();
append = append.sync().await.unwrap();
assert_eq!(write_count.load(Ordering::SeqCst), 3);
let slot0_offset = PAGE_SIZE.get() as u64;
blob.write_at(slot0_offset, DUMMY_MARKER.to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
assert!(
append.truncate(45).await.is_err(),
"phase-1 partial write should fail"
);
assert_eq!(write_count.load(Ordering::SeqCst), 4);
assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
drop(blob);
let (blob, size) = context
.open("test_partition", b"same_page_shrink_fallback_slot")
.await
.unwrap();
let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 50);
let read = append.read_at(0, 50).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data[..50]);
});
}
#[test]
fn test_recovery_truncate_full_page_to_partial_reopens_at_shorter_size() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let page_size = PAGE_SIZE.get() as u64;
let target = page_size + 45;
let data: Vec<u8> = (0..page_size * 2).map(|i| (i % 251) as u8).collect();
let (blob, size) = context
.open("test_partition", b"full_page_to_partial")
.await
.unwrap();
let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
append = append.truncate(target).await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"full_page_to_partial")
.await
.unwrap();
let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), target);
let read = append.read_at(0, target as usize).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data[..target as usize]);
});
}
#[test]
fn test_recovery_truncate_full_page_to_partial_survives_interrupted_crc_stage() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let page_size = PAGE_SIZE.get() as u64;
let target = page_size + 45;
let data: Vec<u8> = (0..page_size * 3).map(|i| (i % 251) as u8).collect();
let (blob, size) = context
.open("test_partition", b"full_page_to_partial_interrupted")
.await
.unwrap();
let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"full_page_to_partial_interrupted")
.await
.unwrap();
let faulty_blob = PartialWriteBlob::new(blob, 1, 3);
let write_count = faulty_blob.write_count();
let failed_write_len = faulty_blob.failed_write_len();
let append = Recovery::open(faulty_blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert!(
append.truncate(target).await.is_err(),
"phase-1 partial write should fail"
);
assert_eq!(write_count.load(Ordering::SeqCst), 1);
assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
let (blob, size) = context
.open("test_partition", b"full_page_to_partial_interrupted")
.await
.unwrap();
let append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), page_size * 2);
let read = append
.read_at(0, (page_size * 2) as usize)
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), &data[..(page_size * 2) as usize]);
});
}
#[test]
fn test_recovery_truncate_same_page_shrink_survives_interrupted_length_invalidation() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
const LARGE_PAGE_SIZE: NonZeroU16 = NZU16!(600);
const LARGE_BUFFER_SIZE: usize = 1_200;
let cache_ref = CacheRef::new(LARGE_PAGE_SIZE, NZUsize!(LARGE_BUFFER_SIZE));
let data: Vec<u8> = (0..300).map(|i| (i % 251) as u8).collect();
let (blob, size) = context
.open(
"test_partition",
b"same_page_shrink_interrupted_len_invalidation",
)
.await
.unwrap();
let mut append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&data[..255]).await.unwrap();
append = append.sync().await.unwrap();
(append, _) = append.append(&data[255..]).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open(
"test_partition",
b"same_page_shrink_interrupted_len_invalidation",
)
.await
.unwrap();
let faulty_blob = PartialWriteBlob::new(blob, 3, 1);
let write_count = faulty_blob.write_count();
let failed_write_len = faulty_blob.failed_write_len();
let append = Recovery::open(faulty_blob, size, LARGE_BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert!(
append.truncate(40).await.is_err(),
"old-slot length invalidation should fail"
);
assert_eq!(write_count.load(Ordering::SeqCst), 3);
assert_eq!(failed_write_len.load(Ordering::SeqCst), CHECKSUM_SLOT_SIZE);
let (blob, size) = context
.open(
"test_partition",
b"same_page_shrink_interrupted_len_invalidation",
)
.await
.unwrap();
let append = Recovery::open(blob, size, LARGE_BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(append.size(), 40);
let read = append.read_at(0, 40).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data[..40]);
});
}
#[test_traced("DEBUG")]
fn test_recovery_truncate_partial_shrink_without_physical_resize_uses_range_sync() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
append = append.sync().await.unwrap();
let data = vec![5u8; PAGE_SIZE.get() as usize];
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
append = append.truncate(50).await.unwrap();
append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 4);
assert_eq!(full_syncs, 1);
assert_eq!(range_syncs, 4);
});
}
#[test_traced("DEBUG")]
fn test_recovery_truncate_partial_shrink_with_physical_resize_clears_full_sync_requirement() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
append = append.sync().await.unwrap();
let data = vec![9u8; PAGE_SIZE.get() as usize * 2];
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
append = append.truncate(50).await.unwrap();
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 4);
assert_eq!(full_syncs, 2);
assert_eq!(range_syncs, 3);
(append, _) = append.append(b"x").await.unwrap();
append = append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 5);
assert_eq!(full_syncs, 2);
assert_eq!(range_syncs, 4);
let mut expected = data[..50].to_vec();
expected.push(b'x');
let read = append.read_at(0, expected.len()).await.unwrap().coalesce();
assert_eq!(read.as_ref(), expected.as_slice());
});
}
#[test_traced("DEBUG")]
fn test_recovery_truncate_page_boundary_shrink_uses_full_sync() {
let executor = deterministic::Runner::default();
executor.start(|_: deterministic::Context| async move {
let blob = SyncTrackingBlob::new();
let blob = Arc::new(blob);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut append = Recovery::open(blob.clone(), 0, BUFFER_SIZE, cache_ref)
.await
.unwrap();
append = append.sync().await.unwrap();
let page_size = PAGE_SIZE.get() as usize;
let data = vec![11u8; page_size * 2];
(append, _) = append.append(&data).await.unwrap();
append = append.sync().await.unwrap();
append = append.truncate(PAGE_SIZE.get() as u64).await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 2);
assert_eq!(range_syncs, 1);
append.sync().await.unwrap();
let (_, writes, full_syncs, range_syncs) = blob.snapshot();
assert_eq!(writes, 1);
assert_eq!(full_syncs, 2);
assert_eq!(range_syncs, 1);
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let reopened = Recovery::open(blob.clone(), blob.size(), BUFFER_SIZE, cache_ref)
.await
.unwrap();
assert_eq!(reopened.size(), PAGE_SIZE.get() as u64);
let read = reopened.read_at(0, page_size).await.unwrap().coalesce();
assert_eq!(read.as_ref(), &data[..page_size]);
});
}
#[test]
fn test_recovery_partial_tail_append_and_truncate() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
const PAGE_SIZE: NonZeroU16 = NZU16!(64);
const BUFFER_SIZE: usize = 256;
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(4));
let (blob, size) = context
.open("test_partition", b"partial_tail_test")
.await
.unwrap();
let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&[1, 2, 3, 4, 5]).await.unwrap();
append = append.sync().await.unwrap();
assert_eq!(append.size(), 5);
drop(append);
let (blob, size) = context
.open("test_partition", b"partial_tail_test")
.await
.unwrap();
let mut append = Recovery::open(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
assert_eq!(append.size(), 5);
(append, _) = append.append(&[6, 7, 8]).await.unwrap();
append = append.truncate(6).await.unwrap();
append = append.sync().await.unwrap();
let data: Vec<u8> = append.read_at(0, 6).await.unwrap().coalesce().into();
assert_eq!(data, vec![1, 2, 3, 4, 5, 6]);
});
}
#[test]
fn test_corrupted_crc_len_too_large() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let physical_page_size = PAGE_SIZE.get() as usize + CHECKSUM_SIZE as usize;
let (blob, size) = context
.open("test_partition", b"crc_len_test")
.await
.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&[0x42; 50]).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"crc_len_test")
.await
.unwrap();
assert_eq!(size as usize, physical_page_size);
let crc_offset = PAGE_SIZE.get() as u64;
let bad_crc_record: [u8; 12] = [
0xFF, 0xFF, 0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ];
blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
match result {
Ok(append) => {
let recovered_size = append.size();
assert_eq!(
recovered_size, 0,
"Corrupted page should be truncated, size should be 0"
);
}
Err(e) => {
assert!(
matches!(e, crate::Error::InvalidChecksum),
"Expected InvalidChecksum error, got: {:?}",
e
);
}
}
});
}
#[test]
fn test_corrupted_crc_both_slots_len_too_large() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let (blob, size) = context
.open("test_partition", b"crc_both_bad")
.await
.unwrap();
let mut append = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone())
.await
.unwrap();
(append, _) = append.append(&[0x42; 50]).await.unwrap();
append = append.sync().await.unwrap();
drop(append);
let (blob, size) = context
.open("test_partition", b"crc_both_bad")
.await
.unwrap();
let crc_offset = PAGE_SIZE.get() as u64;
let bad_crc_record: [u8; 12] = [
0x01, 0x00, 0xDE, 0xAD, 0xBE, 0xEF, 0x02, 0x00, 0xCA, 0xFE, 0xBA, 0xBE, ];
blob.write_at(crc_offset, bad_crc_record.to_vec(), WriteOptions::default())
.await
.unwrap();
blob.sync().await.unwrap();
let result = Writer::new(blob, size, BUFFER_SIZE, cache_ref.clone()).await;
match result {
Ok(append) => {
assert_eq!(append.size(), 0);
}
Err(e) => {
assert!(
matches!(e, crate::Error::InvalidChecksum),
"Expected InvalidChecksum, got: {:?}",
e
);
}
}
});
}
#[test_traced("DEBUG")]
fn test_reader_sees_buffered_bytes() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rdr_buf").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Writer::new(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let data: Vec<u8> = (0u8..50).collect();
(writer, _) = writer.append(&data).await.unwrap();
assert_eq!(writer.size(), 50);
let read = writer.read_at(0, 50).await.unwrap().coalesce();
assert_eq!(read.as_ref(), data.as_slice());
let mut buf = vec![0u8; 10];
assert!(writer.try_read_sync_into(&mut buf, 20));
assert_eq!(buf, data[20..30]);
});
}
#[test_traced("DEBUG")]
fn test_recovery_read_past_truncated_end_errors_cleanly() {
let executor = deterministic::Runner::default();
executor.start(|context: deterministic::Context| async move {
let (blob, blob_size) = context.open("test_partition", b"rdr_rsz").await.unwrap();
let cache_ref = CacheRef::new(PAGE_SIZE, NZUsize!(BUFFER_SIZE));
let mut writer = Recovery::open(blob, blob_size, BUFFER_SIZE, cache_ref)
.await
.unwrap();
let page_size = PAGE_SIZE.get() as usize;
let data: Vec<u8> = (0u8..=255).cycle().take(page_size * 2).collect();
(writer, _) = writer.append(&data).await.unwrap();
writer = writer.sync().await.unwrap();
assert_eq!(writer.size(), (page_size * 2) as u64);
let new_size = (page_size / 2) as u64;
writer = writer.truncate(new_size).await.unwrap();
let err = writer
.read_at(new_size, page_size)
.await
.expect_err("read past truncated end must fail");
assert!(matches!(err, Error::BlobInsufficientLength));
let read = writer
.read_at(0, new_size as usize)
.await
.unwrap()
.coalesce();
assert_eq!(read.as_ref(), &data[..new_size as usize]);
});
}
}