use super::manager::{
AppendFactory, Config as ManagerConfig, Manager, section_from_name, stored_names,
};
use crate::journal::Error;
use commonware_codec::{CodecFixed, CodecFixedShared, DecodeExt as _, ReadExt as _};
use commonware_runtime::{
Blob, Error as RError, Handle, Metrics, ReadOptions, Storage,
buffer::paged::{CacheRef, Replay as BlobReplay, Writer},
};
use commonware_utils::NZUsize;
use std::{
collections::{BTreeMap, BTreeSet, VecDeque},
marker::PhantomData,
num::{NonZeroU16, NonZeroUsize},
};
use tracing::{trace, warn};
struct SectionReplay<B: Blob> {
section: u64,
reader: BlobReplay<B>,
position: u64,
}
#[derive(Clone)]
pub struct Config {
pub partition: String,
pub page_cache: CacheRef,
pub write_buffer: NonZeroUsize,
}
struct Inner<E: Storage + Metrics, A: CodecFixed> {
manager: Manager<E, AppendFactory>,
unrecovered: BTreeSet<u64>,
floors: BTreeMap<u64, u64>,
_array: PhantomData<A>,
}
enum PreflightMode {
Floors(BTreeMap<u64, u64>),
Restore {
section: u64,
floors: BTreeMap<u64, u64>,
},
}
pub(crate) struct RecoveryPreflight<E: Storage + Metrics, A: CodecFixed> {
context: E,
cfg: Config,
boundaries: BTreeMap<u64, Option<A>>,
mode: PreflightMode,
}
impl<E: Storage + Metrics, A: CodecFixedShared> RecoveryPreflight<E, A> {
pub(crate) const fn boundaries(&self) -> &BTreeMap<u64, Option<A>> {
&self.boundaries
}
pub(crate) async fn finish(self) -> Result<Journal<E, A>, Error> {
Ok(Journal(Box::new(
Inner::init(self.context, self.cfg, Some(self.mode)).await?,
)))
}
}
impl<E: Storage + Metrics, A: CodecFixed> Inner<E, A> {
fn writer(&mut self, section: u64) -> &mut Writer<E::Blob> {
self.manager
.get_mut(section)
.expect("replayed section is present")
}
}
impl<E: Storage + Metrics, A: CodecFixedShared> Inner<E, A> {
const CHUNK_SIZE: usize = A::SIZE;
const CHUNK_SIZE_U64: u64 = Self::CHUNK_SIZE as u64;
async fn stored(context: &E, cfg: &Config) -> Result<BTreeMap<u64, Vec<u8>>, Error> {
let mut stored = BTreeMap::new();
for name in stored_names(context, &cfg.partition).await? {
let section = section_from_name(&name)?;
stored.insert(section, name);
}
Ok(stored)
}
async fn preflight_floors(
context: E,
cfg: Config,
minimum_items: &BTreeMap<u64, u64>,
) -> Result<RecoveryPreflight<E, A>, Error> {
let stored = Self::stored(&context, &cfg).await?;
let page_size = cfg.page_cache.page_size();
let mut floor_sizes = BTreeMap::new();
let mut boundaries = BTreeMap::new();
for (§ion, &items) in minimum_items {
let Some(name) = stored.get(§ion) else {
return Err(Error::Corruption(format!(
"section {section} has a validation floor but no blob"
)));
};
let required = items.checked_mul(Self::CHUNK_SIZE_U64).ok_or_else(|| {
Error::Corruption(format!(
"section {section} validation floor {items} overflows its byte size"
))
})?;
if required == 0 {
boundaries.insert(section, None);
continue;
}
let (blob, _) = context.open(&cfg.partition, name).await?;
let entry = Self::boundary(&blob, page_size, section, required).await?;
floor_sizes.insert(section, required);
boundaries.insert(section, Some(entry));
}
Ok(RecoveryPreflight {
context,
cfg,
boundaries,
mode: PreflightMode::Floors(floor_sizes),
})
}
async fn preflight_restore(
context: E,
cfg: Config,
section: u64,
size: u64,
) -> Result<RecoveryPreflight<E, A>, Error> {
if !size.is_multiple_of(Self::CHUNK_SIZE_U64) {
return Err(Error::Corruption(format!(
"section {section} checkpoint size {size} is not item-aligned"
)));
}
let stored = Self::stored(&context, &cfg).await?;
if size > 0 && !stored.contains_key(§ion) {
return Err(Error::Corruption(format!(
"section {section} has a checkpoint but no blob"
)));
}
let page_size = cfg.page_cache.page_size();
let mut boundaries = BTreeMap::new();
let mut floors = BTreeMap::new();
for (&candidate, name) in stored.range(..=section) {
let (blob, physical_size) = context.open(&cfg.partition, name).await?;
let entry = if candidate == section {
if size == 0 {
None
} else {
let entry = Self::boundary(&blob, page_size, section, size).await?;
floors.insert(section, size);
Some(entry)
}
} else if physical_size == 0 {
None
} else {
let (logical_size, entry) = Writer::<E::Blob>::read_tail(
&blob,
physical_size,
page_size,
Self::CHUNK_SIZE,
ReadOptions::default(),
)
.await
.map_err(|err| Self::boundary_error(candidate, physical_size, err))?;
if !logical_size.is_multiple_of(Self::CHUNK_SIZE_U64) {
return Err(Error::Corruption(format!(
"section {candidate} is not a complete checkpoint-covered index"
)));
}
floors.insert(candidate, logical_size);
Some(A::decode(entry.coalesce()).map_err(Error::Codec)?)
};
boundaries.insert(candidate, entry);
}
boundaries.entry(section).or_insert(None);
Ok(RecoveryPreflight {
context,
cfg,
boundaries,
mode: PreflightMode::Restore { section, floors },
})
}
async fn boundary(
blob: &E::Blob,
page_size: NonZeroU16,
section: u64,
size: u64,
) -> Result<A, Error> {
let entry = Writer::<E::Blob>::read_range(
blob,
page_size,
size - Self::CHUNK_SIZE_U64,
Self::CHUNK_SIZE,
ReadOptions::default(),
)
.await
.map_err(|err| Self::boundary_error(section, size, err))?;
A::decode(entry.coalesce()).map_err(Error::Codec)
}
fn boundary_error(section: u64, searched: u64, err: RError) -> Error {
match err {
RError::InvalidChecksum | RError::BlobInsufficientLength => Error::Corruption(format!(
"section {section} does not retain a valid durable boundary within {searched} bytes"
)),
err => err.into(),
}
}
async fn init(context: E, cfg: Config, mode: Option<PreflightMode>) -> Result<Self, Error> {
let (floors, restore) = match mode {
None => (BTreeMap::new(), None),
Some(PreflightMode::Floors(floors)) => (floors, None),
Some(PreflightMode::Restore { section, floors }) => {
let size = floors.get(§ion).copied().unwrap_or(0);
(floors, Some((section, size)))
}
};
let manager_cfg = ManagerConfig {
partition: cfg.partition,
factory: AppendFactory {
write_buffer: cfg.write_buffer,
page_cache_ref: cfg.page_cache,
},
};
let mut manager = Manager::init(context, manager_cfg).await?;
if let Some((section, size)) = restore {
manager.rewind(section, size).await?;
manager.sync(section).await?;
return Ok(Self {
manager,
unrecovered: BTreeSet::new(),
floors,
_array: PhantomData,
});
}
let mut unrecovered = BTreeSet::new();
for section in manager.sections() {
let size = manager.size(section)?;
let floor = floors.get(§ion).copied().unwrap_or(0);
if size < floor {
return Err(Error::Corruption(format!(
"section {section} retains {size} of its {floor}-byte validation floor"
)));
}
if size > floor {
unrecovered.insert(section);
}
}
Ok(Self {
manager,
unrecovered,
floors,
_array: PhantomData,
})
}
async fn append(&mut self, section: u64, item: &A) -> Result<u64, Error> {
assert!(
!self.unrecovered.contains(§ion),
"section {section} must be replayed before append"
);
let blob = self.manager.get_or_create(section).await?;
let buf = item.encode_mut();
let offset = blob.append(&buf).await?;
if !offset.is_multiple_of(Self::CHUNK_SIZE_U64) {
return Err(Error::InvalidBlobSize(section, offset));
}
let position = offset / Self::CHUNK_SIZE_U64;
trace!(section, position, "appended item");
Ok(position)
}
async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
let blob = self
.manager
.get(section)?
.ok_or(Error::SectionOutOfRange(section))?;
let offset = position
.checked_mul(Self::CHUNK_SIZE_U64)
.ok_or(Error::ItemOutOfRange(position))?;
let buf = blob
.read_at(offset, Self::CHUNK_SIZE)
.await
.map_err(|err| match err {
commonware_runtime::Error::BlobInsufficientLength
| commonware_runtime::Error::OffsetOverflow => Error::ItemOutOfRange(position),
err => Error::Runtime(err),
})?;
A::decode(buf.coalesce()).map_err(Error::Codec)
}
async fn get_many(
&self,
section: u64,
positions: &[u64],
buf: &mut [u8],
) -> Result<(Vec<A>, usize), Error> {
assert!(
positions.is_sorted_by(|a, b| a < b),
"positions must be strictly increasing"
);
if positions.is_empty() {
return Ok((Vec::new(), 0));
}
assert!(
buf.len() >= positions.len() * Self::CHUNK_SIZE,
"get_many requires buf.len() >= positions.len() * CHUNK_SIZE"
);
let buf = &mut buf[..positions.len() * Self::CHUNK_SIZE];
let blob = self
.manager
.get(section)?
.ok_or(Error::SectionOutOfRange(section))?;
let offsets: Vec<u64> = positions
.iter()
.map(|&p| {
p.checked_mul(Self::CHUNK_SIZE_U64)
.ok_or(Error::ItemOutOfRange(p))
})
.collect::<Result<_, _>>()?;
let hits = blob
.read_many_into(buf, &offsets, NZUsize!(Self::CHUNK_SIZE))
.await?;
let mut items = Vec::with_capacity(positions.len());
for i in 0..positions.len() {
let slice = &buf[i * Self::CHUNK_SIZE..(i + 1) * Self::CHUNK_SIZE];
items.push(A::decode(slice).map_err(Error::Codec)?);
}
Ok((items, hits))
}
fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
let blob = self.manager.get(section).ok()??;
let offset = position.checked_mul(Self::CHUNK_SIZE_U64)?;
let remaining = blob.size().checked_sub(offset)?;
if remaining < Self::CHUNK_SIZE_U64 {
return None;
}
let mut buf = vec![0u8; Self::CHUNK_SIZE];
if !blob.try_read_sync_into(&mut buf, offset) {
return None;
}
A::decode(&buf[..]).ok()
}
async fn last(&self, section: u64) -> Result<Option<A>, Error> {
let blob = self
.manager
.get(section)?
.ok_or(Error::SectionOutOfRange(section))?;
let size = blob.size();
if size < Self::CHUNK_SIZE_U64 {
return Ok(None);
}
let last_position = (size / Self::CHUNK_SIZE_U64) - 1;
let offset = last_position * Self::CHUNK_SIZE_U64;
let buf = blob.read_at(offset, Self::CHUNK_SIZE).await?;
A::decode(buf.coalesce()).map_err(Error::Codec).map(Some)
}
async fn sync(&mut self, sections: impl crate::Sections) -> Result<(), Error> {
self.manager.sync(sections).await
}
async fn start_sync(&mut self, sections: impl crate::Sections) -> Result<Handle<()>, Error> {
self.manager.start_sync(sections).await
}
async fn sync_all(&mut self) -> Result<(), Error> {
self.manager.sync_all().await
}
async fn prune(&mut self, min: u64) -> Result<bool, Error> {
let pruned = self.manager.prune(min).await?;
if pruned {
self.unrecovered.retain(|section| *section >= min);
self.floors.retain(|section, _| *section >= min);
}
Ok(pruned)
}
const fn pruned(&self, section: u64) -> bool {
self.manager.pruned(section)
}
fn oldest_section(&self) -> Option<u64> {
self.manager.oldest_section()
}
fn newest_section(&self) -> Option<u64> {
self.manager.newest_section()
}
fn sections(&self) -> impl Iterator<Item = u64> + '_ {
self.manager.sections()
}
fn section_len(&self, section: u64) -> Result<u64, Error> {
let size = self.manager.size(section)?;
Ok(size / Self::CHUNK_SIZE_U64)
}
fn size(&self, section: u64) -> Result<u64, Error> {
self.manager.size(section)
}
async fn rewind(&mut self, section: u64, offset: u64) -> Result<(), Error> {
self.manager.rewind(section, offset).await?;
self.unrecovered.retain(|candidate| *candidate <= section);
self.floors.retain(|candidate, _| *candidate <= section);
if offset == 0 {
self.unrecovered.remove(§ion);
}
if let Some(floor) = self.floors.get_mut(§ion) {
*floor = (*floor).min(offset);
}
Ok(())
}
async fn rewind_section(&mut self, section: u64, size: u64) -> Result<(), Error> {
self.manager.rewind_section(section, size).await?;
if size == 0 {
self.unrecovered.remove(§ion);
}
if let Some(floor) = self.floors.get_mut(§ion) {
*floor = (*floor).min(size);
}
Ok(())
}
async fn destroy(self) -> Result<(), Error> {
self.manager.destroy().await
}
async fn clear(&mut self) -> Result<(), Error> {
self.manager.clear().await?;
self.unrecovered.clear();
self.floors.clear();
Ok(())
}
}
pub struct Journal<E: Storage + Metrics, A: CodecFixed>(Box<Inner<E, A>>);
impl<E: Storage + Metrics, A: CodecFixedShared> std::fmt::Debug for Journal<E, A> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Journal")
.field("oldest_section", &self.oldest_section())
.field("newest_section", &self.newest_section())
.finish_non_exhaustive()
}
}
impl<E: Storage + Metrics, A: CodecFixedShared> Journal<E, A> {
pub const CHUNK_SIZE: usize = Inner::<E, A>::CHUNK_SIZE;
pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
Ok(Self(Box::new(Inner::init(context, cfg, None).await?)))
}
pub(crate) async fn preflight_floors(
context: E,
cfg: Config,
minimum_items: &BTreeMap<u64, u64>,
) -> Result<RecoveryPreflight<E, A>, Error> {
Inner::preflight_floors(context, cfg, minimum_items).await
}
pub(crate) async fn preflight_restore(
context: E,
cfg: Config,
section: u64,
size: u64,
) -> Result<RecoveryPreflight<E, A>, Error> {
Inner::preflight_restore(context, cfg, section, size).await
}
pub async fn append(mut self, section: u64, item: &A) -> Result<(Self, u64), Error> {
let position = self.0.append(section, item).await?;
Ok((self, position))
}
pub async fn get(&self, section: u64, position: u64) -> Result<A, Error> {
self.0.get(section, position).await
}
pub async fn get_many(
&self,
section: u64,
positions: &[u64],
buf: &mut [u8],
) -> Result<(Vec<A>, usize), Error> {
self.0.get_many(section, positions, buf).await
}
pub fn try_get_sync(&self, section: u64, position: u64) -> Option<A> {
self.0.try_get_sync(section, position)
}
pub async fn last(&self, section: u64) -> Result<Option<A>, Error> {
self.0.last(section).await
}
pub async fn replay(
mut self,
start_section: u64,
start_position: u64,
buffer: NonZeroUsize,
read_options: ReadOptions,
) -> Result<Replay<E, A>, Error> {
let mut sections = VecDeque::new();
for (§ion, blob) in self.0.manager.sections_from(start_section) {
let blob_size = blob.size();
let mut reader = blob.replay(buffer, read_options).await?;
let position = if section == start_section {
let start = start_position
.checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
.ok_or(Error::ItemOutOfRange(start_position))?;
if start > blob_size {
return Err(Error::ItemOutOfRange(start_position));
}
reader.seek_to(start)?;
start_position
} else {
0
};
sections.push_back(SectionReplay {
section,
reader,
position,
});
}
let finished = sections.is_empty();
Ok(Replay {
journal: self,
sections,
recovered_from: if start_position == 0 {
Some(start_section)
} else {
start_section.checked_add(1)
},
buffer,
read_options,
finished,
errored: false,
repairing: false,
})
}
pub async fn sync(mut self, sections: impl crate::Sections) -> Result<Self, Error> {
self.0.sync(sections).await?;
Ok(self)
}
pub async fn start_sync(
mut self,
sections: impl crate::Sections,
) -> Result<(Self, Handle<()>), Error> {
let handle = self.0.start_sync(sections).await?;
Ok((self, handle))
}
pub async fn sync_all(mut self) -> Result<Self, Error> {
self.0.sync_all().await?;
Ok(self)
}
pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
let pruned = self.0.prune(min).await?;
Ok((self, pruned))
}
pub fn pruned(&self, section: u64) -> bool {
self.0.pruned(section)
}
pub fn oldest_section(&self) -> Option<u64> {
self.0.oldest_section()
}
pub fn newest_section(&self) -> Option<u64> {
self.0.newest_section()
}
pub fn sections(&self) -> impl Iterator<Item = u64> + '_ {
self.0.sections()
}
pub fn section_len(&self, section: u64) -> Result<u64, Error> {
self.0.section_len(section)
}
pub fn size(&self, section: u64) -> Result<u64, Error> {
self.0.size(section)
}
pub async fn rewind(mut self, section: u64, size: u64) -> Result<Self, Error> {
self.0.rewind(section, size).await?;
Ok(self)
}
pub async fn rewind_section(mut self, section: u64, size: u64) -> Result<Self, Error> {
self.0.rewind_section(section, size).await?;
Ok(self)
}
pub async fn destroy(self) -> Result<(), Error> {
self.0.destroy().await
}
pub async fn clear(mut self) -> Result<Self, Error> {
self.0.clear().await?;
Ok(self)
}
}
pub struct Replay<E: Storage + Metrics, A: CodecFixed> {
journal: Journal<E, A>,
sections: VecDeque<SectionReplay<E::Blob>>,
recovered_from: Option<u64>,
buffer: NonZeroUsize,
read_options: ReadOptions,
finished: bool,
errored: bool,
repairing: bool,
}
impl<E: Storage + Metrics, A: CodecFixedShared> Replay<E, A> {
async fn plan_repair(&mut self, source: RError) -> Result<(u64, u64), Error> {
if !matches!(source, RError::InvalidChecksum) {
return Err(source.into());
}
let current = self.sections.front().expect("replayed section is present");
let section = current.section;
let position = current.position;
let size = current.reader.blob_size();
let valid_size = position
.checked_mul(Inner::<E, A>::CHUNK_SIZE_U64)
.ok_or(Error::OffsetOverflow)?;
let recoverable = self
.journal
.0
.writer(section)
.recoverable_prefix_len(valid_size, self.buffer, self.read_options)
.await?;
if recoverable >= size {
return Err(source.into());
}
let target = recoverable - recoverable % Inner::<E, A>::CHUNK_SIZE_U64;
if target < valid_size {
return Err(Error::ItemOutOfRange(position));
}
self.ensure_above_floor(section, target)?;
Ok((valid_size, target))
}
async fn repair(&mut self, source: RError) -> Result<(), Error> {
let (valid_size, target) = match self.plan_repair(source).await {
Ok(plan) => plan,
Err(err) => {
self.sections.pop_front();
return Err(err);
}
};
let current = self.sections.front().expect("replayed section is present");
let (section, position) = (current.section, current.position);
warn!(
section,
invalid_size = current.reader.blob_size(),
new_size = target,
"torn page detected: truncating"
);
self.repairing = true;
let current = self
.sections
.pop_front()
.expect("repaired section is present");
drop(current.reader);
repair_blob(&mut self.journal, section, target).await?;
let mut reader = self
.journal
.0
.writer(section)
.replay(self.buffer, self.read_options)
.await?;
reader.seek_to(valid_size)?;
self.sections.push_front(SectionReplay {
section,
reader,
position,
});
self.repairing = false;
Ok(())
}
fn ensure_above_floor(&self, section: u64, target: u64) -> Result<(), Error> {
let floor = self.journal.0.floors.get(§ion).copied().unwrap_or(0);
if target < floor {
return Err(Error::Corruption(format!(
"section {section} recovery target {target} is below its {floor}-byte validation floor"
)));
}
Ok(())
}
pub async fn next(&mut self) -> Option<Result<(u64, u64, A), Error>> {
if self.repairing {
self.repairing = false;
self.sections.clear();
if !self.errored {
return self.fail(Error::ReplayInterrupted);
}
}
while let Some(current) = self.sections.front_mut() {
match current.reader.ensure(Inner::<E, A>::CHUNK_SIZE).await {
Ok(true) => {}
Ok(false) => {
let valid_size =
match current.position.checked_mul(Inner::<E, A>::CHUNK_SIZE_U64) {
Some(size) => size,
None => return self.fail(Error::OffsetOverflow),
};
let blob_size = current.reader.blob_size();
if valid_size < blob_size {
let section = current.section;
if let Err(err) = self.ensure_above_floor(section, valid_size) {
self.sections.pop_front();
return self.fail(err);
}
warn!(
section,
invalid_size = blob_size,
new_size = valid_size,
"incomplete item detected: truncating"
);
self.repairing = true;
if let Err(err) = repair_blob(&mut self.journal, section, valid_size).await
{
self.sections.pop_front();
return self.fail(err);
}
self.repairing = false;
}
self.sections.pop_front();
continue;
}
Err(err) => {
if let Err(err) = self.repair(err).await {
return self.fail(err);
}
continue;
}
}
match A::read(&mut current.reader) {
Ok(item) => {
let yielded = (current.section, current.position, item);
current.position += 1;
return Some(Ok(yielded));
}
Err(err) => {
self.sections.pop_front();
return self.fail(Error::Codec(err));
}
}
}
self.finished = true;
None
}
const fn fail(&mut self, err: Error) -> Option<Result<(u64, u64, A), Error>> {
self.errored = true;
Some(Err(err))
}
pub fn finish(mut self) -> Result<Journal<E, A>, Error> {
if self.errored || !self.finished {
return Err(Error::ReplayFailed);
}
if let Some(start) = self.recovered_from {
self.journal
.0
.unrecovered
.retain(|section| *section < start);
}
Ok(self.journal)
}
}
async fn repair_blob<E: Storage + Metrics, A: CodecFixed>(
journal: &mut Journal<E, A>,
section: u64,
size: u64,
) -> Result<(), Error> {
let blob = journal.0.writer(section);
blob.resize(size).await?;
blob.sync().await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use commonware_codec::FixedSize;
use commonware_cryptography::{Hasher as _, Sha256, sha256::Digest};
use commonware_macros::test_traced;
use commonware_runtime::{
BufferPooler, Error as RError, Runner, Spawner as _, Supervisor as _,
buffer::paged::{CacheRef, Writer, corrupt_page},
deterministic,
mocks::{
DelayedSyncContext, PendingSyncs, RecordingContext, fail_pending_syncs,
release_pending_syncs,
},
};
use commonware_utils::{NZU16, NZUsize};
use core::num::NonZeroU16;
use std::{
ops::RangeInclusive,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
const PAGE_SIZE: NonZeroU16 = NZU16!(44);
const PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(3);
fn test_digest(value: u64) -> Digest {
Sha256::hash(&[&value.to_be_bytes()])
}
fn test_cfg(pooler: &impl BufferPooler) -> Config {
Config {
partition: "test-partition".into(),
page_cache: CacheRef::from_pooler(pooler, PAGE_SIZE, PAGE_CACHE_SIZE),
write_buffer: NZUsize!(2048),
}
}
fn aligned_cfg(pooler: &impl BufferPooler) -> Config {
Config {
partition: "segmented-fixed-aligned".into(),
page_cache: CacheRef::from_pooler(pooler, NZU16!(16), NZUsize!(4)),
write_buffer: NZUsize!(128),
}
}
fn lazy_recovery_cfg(pooler: &impl BufferPooler, partition: &str) -> Config {
Config {
partition: partition.into(),
page_cache: CacheRef::from_pooler(pooler, NZU16!(16), NZUsize!(4)),
write_buffer: NZUsize!(1),
}
}
async fn replay_all<E, A>(journal: Journal<E, A>) -> Journal<E, A>
where
E: Storage + Metrics,
A: CodecFixedShared,
{
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to start recovery replay");
while let Some(item) = replay.next().await {
item.expect("failed to recover journal");
}
replay.finish().expect("failed to finish recovery replay")
}
async fn seed<E: Storage + Metrics>(context: &E, cfg: &Config, sections: RangeInclusive<u64>) {
let mut journal = Journal::init(context.child("seed"), cfg.clone())
.await
.expect("failed to init");
for section in sections {
for value in 0..16u64 {
(journal, _) = journal
.append(section, &value)
.await
.expect("failed to append");
}
}
journal.sync_all().await.expect("failed to sync");
}
#[test_traced]
fn test_segmented_fixed_append_and_get() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
let pos0;
(journal, pos0) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
assert_eq!(pos0, 0);
let pos1;
(journal, pos1) = journal
.append(1, &test_digest(1))
.await
.expect("failed to append");
assert_eq!(pos1, 1);
let pos2;
(journal, pos2) = journal
.append(2, &test_digest(2))
.await
.expect("failed to append");
assert_eq!(pos2, 0);
let item0 = journal.get(1, 0).await.expect("failed to get");
assert_eq!(item0, test_digest(0));
let item1 = journal.get(1, 1).await.expect("failed to get");
assert_eq!(item1, test_digest(1));
let item2 = journal.get(2, 0).await.expect("failed to get");
assert_eq!(item2, test_digest(2));
let err = journal.get(1, 2).await;
assert!(matches!(err, Err(Error::ItemOutOfRange(2))));
let err = journal.get(3, 0).await;
assert!(matches!(err, Err(Error::SectionOutOfRange(3))));
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_replay_empty_finishes_immediately() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
.await
.expect("failed to init");
let replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let journal = replay.finish().expect("failed to finish replay");
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_replay_propagates_read_options() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let (context, recordings) = RecordingContext::new(context);
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg)
.await
.expect("failed to init");
for section in 1..=2 {
(journal, _) = journal
.append(section, &test_digest(section))
.await
.expect("failed to append");
}
let mut replay = journal
.replay(1, 0, NZUsize!(56), ReadOptions::DONT_CACHE)
.await
.expect("failed to replay");
recordings.clear();
let (section, position, item) = replay
.next()
.await
.expect("missing first replay item")
.expect("failed to read first replay item");
assert_eq!((section, position, item), (1, 0, test_digest(1)));
let reads = recordings.snapshot().reads;
assert!(!reads.is_empty());
assert!(
reads
.iter()
.all(|options| *options == ReadOptions::DONT_CACHE)
);
recordings.clear();
let (section, position, item) = replay
.next()
.await
.expect("missing second replay item")
.expect("failed to read second replay item");
assert_eq!((section, position, item), (2, 0, test_digest(2)));
let reads = recordings.snapshot().reads;
assert!(!reads.is_empty());
assert!(
reads
.iter()
.all(|options| *options == ReadOptions::DONT_CACHE)
);
assert!(replay.next().await.is_none());
let journal = replay.finish().expect("failed to finish replay");
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_clean_recovery_uses_replay_buffer() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let (context, recordings) = RecordingContext::new(context);
let cfg = lazy_recovery_cfg(&context, "segmented-fixed-lazy-recovery");
seed(&context, &cfg, 1..=1).await;
recordings.clear();
let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
.await
.expect("failed to reopen");
assert_eq!(recordings.snapshot().reads.len(), 1);
let mut replay = journal
.replay(0, 0, NZUsize!(112), ReadOptions::default())
.await
.expect("failed to replay");
while let Some(item) = replay.next().await {
item.expect("failed to read replay item");
}
assert_eq!(recordings.snapshot().reads.len(), 3);
replay
.finish()
.expect("failed to finish replay")
.destroy()
.await
.expect("failed to destroy");
});
}
#[test_traced]
#[should_panic(expected = "must be replayed before append")]
fn test_segmented_fixed_append_requires_replay_after_reopen() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("seed"), cfg.clone())
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
journal = journal.sync_all().await.expect("failed to sync");
drop(journal);
let journal = Journal::init(context.child("reopen"), cfg)
.await
.expect("failed to reopen");
journal.append(1, &test_digest(1)).await.unwrap();
});
}
#[test_traced]
#[should_panic(expected = "must be replayed before append")]
fn test_segmented_fixed_gates_older_section_after_reopen() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
seed(&context, &cfg, 1..=3).await;
let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
.await
.expect("failed to reopen");
journal.append(2, &2).await.unwrap();
});
}
#[test_traced]
fn test_segmented_fixed_floor_preflight_reads_boundary_only() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let (context, recordings) = RecordingContext::new(context);
let cfg = lazy_recovery_cfg(&context, "segmented-fixed-floor-boundary");
seed(&context, &cfg, 1..=1).await;
recordings.clear();
let floors = BTreeMap::from([(1, 16)]);
let preflight =
Journal::<_, u64>::preflight_floors(context.child("preflight"), cfg, &floors)
.await
.expect("failed to preflight");
assert_eq!(recordings.snapshot().reads.len(), 1);
preflight
.finish()
.await
.expect("failed to finish preflight")
.destroy()
.await
.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_restore_reads_boundaries_only() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let (context, recordings) = RecordingContext::new(context);
let cfg = lazy_recovery_cfg(&context, "segmented-fixed-restore-boundaries");
seed(&context, &cfg, 0..=1).await;
recordings.clear();
let preflight = Journal::<_, u64>::preflight_restore(
context.child("preflight"),
cfg,
1,
16 * u64::SIZE as u64,
)
.await
.expect("failed to preflight");
assert_eq!(recordings.snapshot().reads.len(), 2);
preflight
.finish()
.await
.expect("failed to finish preflight")
.destroy()
.await
.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_restore_retains_empty_checkpoint_section() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = lazy_recovery_cfg(&context, "segmented-fixed-empty-restore");
let mut journal = Journal::init(context.child("seed"), cfg.clone())
.await
.expect("failed to init");
for section in 0..=2 {
(journal, _) = journal
.append(section, §ion)
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
journal = journal
.rewind_section(1, 0)
.await
.expect("failed to empty checkpoint section");
journal = journal.sync(1).await.expect("failed to sync empty section");
drop(journal);
let journal = Journal::<_, u64>::preflight_restore(context.child("restore"), cfg, 1, 0)
.await
.expect("failed to preflight")
.finish()
.await
.expect("failed to finish preflight");
assert_eq!(journal.sections().collect::<Vec<_>>(), vec![0, 1]);
assert_eq!(journal.size(1).expect("missing checkpoint section"), 0);
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_restore_floors_block_below_checkpoint_repair() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = lazy_recovery_cfg(&context, "segmented-fixed-restore-floors");
seed(&context, &cfg, 0..=1).await;
corrupt_page(&context, &cfg.partition, &1u64.to_be_bytes(), 2, 16).await;
let size = 16 * u64::SIZE as u64;
let journal = Journal::<_, u64>::preflight_restore(
context.child("restore"),
cfg.clone(),
1,
size,
)
.await
.expect("failed to preflight")
.finish()
.await
.expect("failed to finish preflight");
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to start replay");
let mut outcome = None;
while let Some(item) = replay.next().await {
if let Err(err) = item {
outcome = Some(err);
break;
}
}
assert!(matches!(
outcome,
Some(Error::Corruption(ref message))
if message.contains("below its 128-byte validation floor")
));
drop(replay);
let (_, blob_size) = context
.open(&cfg.partition, &1u64.to_be_bytes())
.await
.expect("failed to open");
assert_eq!(blob_size, 8 * 28);
});
}
#[test_traced]
fn test_segmented_fixed_floor_preflight_reads_terminal_entry_only() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let (context, recordings) = RecordingContext::new(context);
let cfg = aligned_cfg(&context);
let mut journal = Journal::init(context.child("seed"), cfg.clone())
.await
.expect("failed to init");
for value in 0..2 {
(journal, _) = journal
.append(1, &test_digest(value))
.await
.expect("failed to append");
}
journal = journal.sync(1).await.expect("failed to sync");
drop(journal);
recordings.clear();
let floors = BTreeMap::from([(1, 2)]);
let journal =
Journal::<_, Digest>::preflight_floors(context.child("reopen"), cfg, &floors)
.await
.expect("failed to preflight")
.finish()
.await
.expect("failed to reopen");
assert_eq!(recordings.snapshot().reads.len(), 3);
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_replay_finish_before_drain_fails() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
journal = journal.sync_all().await.expect("failed to sync");
let replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
});
}
#[test_traced]
fn test_segmented_fixed_replay() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
for i in 0u64..10 {
(journal, _) = journal
.append(1, &test_digest(i))
.await
.expect("failed to append");
}
for i in 10u64..20 {
(journal, _) = journal
.append(2, &test_digest(i))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
drop(journal);
let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
.await
.expect("failed to re-init");
let items = {
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
match result {
Ok((section, pos, item)) => items.push((section, pos, item)),
Err(err) => panic!("replay error: {err}"),
}
}
journal = replay.finish().expect("failed to finish replay");
items
};
assert_eq!(items.len(), 20);
for (i, item) in items.iter().enumerate().take(10) {
assert_eq!(item.0, 1);
assert_eq!(item.1, i as u64);
assert_eq!(item.2, test_digest(i as u64));
}
for (i, item) in items.iter().enumerate().skip(10).take(10) {
assert_eq!(item.0, 2);
assert_eq!(item.1, (i - 10) as u64);
assert_eq!(item.2, test_digest(i as u64));
}
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_replay_with_start_offset() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
for i in 0u64..10 {
(journal, _) = journal
.append(1, &test_digest(i))
.await
.expect("failed to append");
}
for i in 10u64..15 {
(journal, _) = journal
.append(2, &test_digest(i))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
drop(journal);
let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
.await
.expect("failed to re-init");
{
let mut replay = journal
.replay(1, 5, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, pos, item) = result.expect("replay error");
items.push((section, pos, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(
items.len(),
10,
"Should have 5 items from section 1 + 5 from section 2"
);
for (i, (section, pos, item)) in items.iter().enumerate().take(5) {
assert_eq!(*section, 1);
assert_eq!(*pos, (i + 5) as u64);
assert_eq!(*item, test_digest((i + 5) as u64));
}
for (i, (section, pos, item)) in items.iter().enumerate().skip(5) {
assert_eq!(*section, 2);
assert_eq!(*pos, (i - 5) as u64);
assert_eq!(*item, test_digest((i + 5) as u64));
}
}
{
let mut replay = journal
.replay(1, 9, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, pos, item) = result.expect("replay error");
items.push((section, pos, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(
items.len(),
6,
"Should have 1 item from section 1 + 5 from section 2"
);
assert_eq!(items[0], (1, 9, test_digest(9)));
for (i, (section, pos, item)) in items.iter().enumerate().skip(1) {
assert_eq!(*section, 2);
assert_eq!(*pos, (i - 1) as u64);
assert_eq!(*item, test_digest((i + 9) as u64));
}
}
{
let mut replay = journal
.replay(2, 3, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, pos, item) = result.expect("replay error");
items.push((section, pos, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(items.len(), 2, "Should have 2 items from section 2");
assert_eq!(items[0], (2, 3, test_digest(13)));
assert_eq!(items[1], (2, 4, test_digest(14)));
}
let result = journal
.replay(1, 100, NZUsize!(1024), ReadOptions::default())
.await;
assert!(matches!(result, Err(Error::ItemOutOfRange(100))));
let journal = Journal::<_, Digest>::init(context.child("third"), cfg.clone())
.await
.expect("failed to re-init");
let result = journal
.replay(1, u64::MAX, NZUsize!(1024), ReadOptions::default())
.await;
assert!(matches!(result, Err(Error::ItemOutOfRange(u64::MAX))));
let journal = Journal::<_, Digest>::init(context.child("fourth"), cfg.clone())
.await
.expect("failed to re-init");
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_prune() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
for section in 1u64..=5 {
(journal, _) = journal
.append(section, &test_digest(section))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
(journal, _) = journal.prune(3).await.expect("failed to prune");
let err = journal.get(1, 0).await;
assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
let err = journal.get(2, 0).await;
assert!(matches!(err, Err(Error::AlreadyPrunedToSection(3))));
let item = journal.get(3, 0).await.expect("should exist");
assert_eq!(item, test_digest(3));
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_pruned_after_full_prune() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
for section in 1u64..=3 {
(journal, _) = journal
.append(section, &test_digest(section))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
(journal, _) = journal.prune(10).await.expect("failed to prune");
assert_eq!(journal.oldest_section(), None);
assert!(journal.pruned(3));
assert!(journal.pruned(9));
assert!(!journal.pruned(10));
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_rewind() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
for section in 1u64..=3 {
(journal, _) = journal
.append(section, &test_digest(section))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
for section in 1u64..=3 {
let size = journal.size(section).expect("failed to get size");
assert!(size > 0, "section {section} should have data");
}
let size = journal.size(1).expect("failed to get size");
journal = journal.rewind(1, size).await.expect("failed to rewind");
let size = journal.size(1).expect("failed to get size");
assert!(size > 0, "section 1 should still have data");
for section in 2u64..=3 {
let size = journal.size(section).expect("failed to get size");
assert_eq!(size, 0, "section {section} should be removed");
}
let item = journal.get(1, 0).await.expect("failed to get");
assert_eq!(item, test_digest(1));
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_rewind_max_section() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
(journal, _) = journal
.append(u64::MAX, &test_digest(0))
.await
.expect("failed to append");
journal = journal.sync_all().await.expect("failed to sync");
let size = journal.size(u64::MAX).expect("failed to get size");
journal = journal
.rewind(u64::MAX, size)
.await
.expect("failed to rewind");
assert_eq!(journal.size(u64::MAX).expect("failed to get size"), size);
assert_eq!(journal.get(u64::MAX, 0).await.unwrap(), test_digest(0));
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_rewind_many_sections() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
for section in 1u64..=10 {
(journal, _) = journal
.append(section, &test_digest(section))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
let size = journal.size(5).expect("failed to get size");
journal = journal.rewind(5, size).await.expect("failed to rewind");
for section in 1u64..=5 {
let size = journal.size(section).expect("failed to get size");
assert!(size > 0, "section {section} should still have data");
}
for section in 6u64..=10 {
let size = journal.size(section).expect("failed to get size");
assert_eq!(size, 0, "section {section} should be removed");
}
{
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, _, item) = result.expect("failed to read");
items.push((section, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(items.len(), 5);
for (i, (section, item)) in items.iter().enumerate() {
assert_eq!(*section, (i + 1) as u64);
assert_eq!(*item, test_digest((i + 1) as u64));
}
}
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_rewind_persistence() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
for section in 1u64..=5 {
(journal, _) = journal
.append(section, &test_digest(section))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
let size = journal.size(2).expect("failed to get size");
journal = journal.rewind(2, size).await.expect("failed to rewind");
journal = journal.sync_all().await.expect("failed to sync");
drop(journal);
let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
.await
.expect("failed to re-init");
for section in 1u64..=2 {
let size = journal.size(section).expect("failed to get size");
assert!(size > 0, "section {section} should have data after restart");
}
for section in 3u64..=5 {
let size = journal.size(section).expect("failed to get size");
assert_eq!(size, 0, "section {section} should be gone after restart");
}
let item1 = journal.get(1, 0).await.expect("failed to get");
assert_eq!(item1, test_digest(1));
let item2 = journal.get(2, 0).await.expect("failed to get");
assert_eq!(item2, test_digest(2));
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_corruption_recovery() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
for i in 0u64..5 {
(journal, _) = journal
.append(1, &test_digest(i))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
drop(journal);
let (blob, size) = context
.open(&cfg.partition, &1u64.to_be_bytes())
.await
.expect("failed to open blob");
blob.resize(size - 1).await.expect("failed to truncate");
blob.sync().await.expect("failed to sync");
let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
.await
.expect("failed to re-init");
let count = {
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut count = 0;
while let Some(result) = replay.next().await {
result.expect("should be ok");
count += 1;
}
journal = replay.finish().expect("failed to finish replay");
count
};
assert_eq!(count, 4);
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_persistence() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
for i in 0u64..5 {
(journal, _) = journal
.append(1, &test_digest(i))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
drop(journal);
let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
.await
.expect("failed to re-init");
for i in 0u64..5 {
let item = journal.get(1, i).await.expect("failed to get");
assert_eq!(item, test_digest(i));
}
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_sync() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
for section in 1u64..=3 {
(journal, _) = journal
.append(section, &test_digest(section))
.await
.expect("failed to append");
}
journal
.sync(&[1, 3, 99])
.await
.expect("failed to sync sections");
let journal = Journal::<_, Digest>::init(context.child("second"), cfg)
.await
.expect("failed to re-init");
assert_eq!(
journal.get(1, 0).await.expect("section 1 durable"),
test_digest(1)
);
assert_eq!(
journal.get(3, 0).await.expect("section 3 durable"),
test_digest(3)
);
assert!(matches!(
journal.get(2, 0).await,
Err(Error::ItemOutOfRange(0)) | Err(Error::SectionOutOfRange(2))
));
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_section_len() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
assert_eq!(journal.section_len(1).unwrap(), 0);
for i in 0u64..5 {
(journal, _) = journal
.append(1, &test_digest(i))
.await
.expect("failed to append");
}
assert_eq!(journal.section_len(1).unwrap(), 5);
assert_eq!(journal.section_len(2).unwrap(), 0);
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_non_contiguous_sections() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(100))
.await
.expect("failed to append");
(journal, _) = journal
.append(5, &test_digest(500))
.await
.expect("failed to append");
(journal, _) = journal
.append(10, &test_digest(1000))
.await
.expect("failed to append");
journal = journal.sync_all().await.expect("failed to sync");
assert_eq!(journal.get(1, 0).await.unwrap(), test_digest(100));
assert_eq!(journal.get(5, 0).await.unwrap(), test_digest(500));
assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(1000));
for missing_section in [0u64, 2, 3, 4, 6, 7, 8, 9, 11] {
let result = journal.get(missing_section, 0).await;
assert!(
matches!(result, Err(Error::SectionOutOfRange(_))),
"Expected SectionOutOfRange for section {}, got {:?}",
missing_section,
result
);
}
drop(journal);
let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
.await
.expect("failed to re-init");
{
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, _, item) = result.expect("replay error");
items.push((section, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(items.len(), 3, "Should have 3 items");
assert_eq!(items[0], (1, test_digest(100)));
assert_eq!(items[1], (5, test_digest(500)));
assert_eq!(items[2], (10, test_digest(1000)));
}
{
let mut replay = journal
.replay(5, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay from section 5");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, _, item) = result.expect("replay error");
items.push((section, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(items.len(), 2, "Should have 2 items from section 5 onwards");
assert_eq!(items[0], (5, test_digest(500)));
assert_eq!(items[1], (10, test_digest(1000)));
}
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_empty_section_in_middle() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(100))
.await
.expect("failed to append");
(journal, _) = journal
.append(2, &test_digest(200))
.await
.expect("failed to append");
journal = journal.sync(2).await.expect("failed to sync");
journal = journal
.rewind_section(2, 0)
.await
.expect("failed to rewind");
(journal, _) = journal
.append(3, &test_digest(300))
.await
.expect("failed to append");
journal = journal.sync_all().await.expect("failed to sync");
assert_eq!(journal.section_len(1).unwrap(), 1);
assert_eq!(journal.section_len(2).unwrap(), 0);
assert_eq!(journal.section_len(3).unwrap(), 1);
drop(journal);
let mut journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
.await
.expect("failed to re-init");
{
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, _, item) = result.expect("replay error");
items.push((section, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(
items.len(),
2,
"Should have 2 items (skipping empty section)"
);
assert_eq!(items[0], (1, test_digest(100)));
assert_eq!(items[1], (3, test_digest(300)));
}
{
let mut replay = journal
.replay(2, 0, NZUsize!(1024), ReadOptions::default())
.await
.expect("failed to replay from section 2");
let mut items = Vec::new();
while let Some(result) = replay.next().await {
let (section, _, item) = result.expect("replay error");
items.push((section, item));
}
journal = replay.finish().expect("failed to finish replay");
assert_eq!(items.len(), 1, "Should have 1 item from section 3");
assert_eq!(items[0], (3, test_digest(300)));
}
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_validates_pages_before_trailing_bytes() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
const LOGICAL_PAGE_SIZE: u64 = 5;
const SECTION: u64 = 0;
let cfg = Config {
partition: "segmented-fixed-validate-before-tail-trim".into(),
page_cache: CacheRef::from_pooler(
&context,
NZU16!(LOGICAL_PAGE_SIZE as u16),
NZUsize!(4),
),
write_buffer: NZUsize!(128),
};
let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
.await
.unwrap();
for value in [11u64, 22, 33, 44] {
(journal, _) = journal.append(SECTION, &value).await.unwrap();
}
journal = journal.sync_all().await.unwrap();
drop(journal);
let (blob, size) = context
.open(&cfg.partition, &SECTION.to_be_bytes())
.await
.unwrap();
let mut writer = Writer::new(blob, size, 128, cfg.page_cache.clone())
.await
.unwrap();
writer.resize(30).await.unwrap();
writer.sync().await.unwrap();
drop(writer);
corrupt_page(
&context,
&cfg.partition,
&SECTION.to_be_bytes(),
4,
LOGICAL_PAGE_SIZE,
)
.await;
let journal = Journal::<_, u64>::init(context.child("recover"), cfg)
.await
.unwrap();
let journal = replay_all(journal).await;
assert_eq!(journal.section_len(SECTION).unwrap(), 2);
assert_eq!(journal.get(SECTION, 0).await.unwrap(), 11);
assert_eq!(journal.get(SECTION, 1).await.unwrap(), 22);
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_segmented_fixed_repairs_torn_interior_page() {
const SECTION: u64 = 0;
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = aligned_cfg(&context);
let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
.await
.unwrap();
for value in 0..6 {
(journal, _) = journal.append(SECTION, &value).await.unwrap();
}
journal = journal.sync_all().await.unwrap();
drop(journal);
corrupt_page(&context, &cfg.partition, &SECTION.to_be_bytes(), 1, 16).await;
let mut journal = Journal::<_, u64>::init(context.child("recover"), cfg.clone())
.await
.unwrap();
journal = replay_all(journal).await;
assert_eq!(journal.section_len(SECTION).unwrap(), 2);
assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
assert!(matches!(
journal.get(SECTION, 2).await,
Err(Error::ItemOutOfRange(2))
));
let position;
(journal, position) = journal.append(SECTION, &99).await.unwrap();
assert_eq!(position, 2);
journal.sync_all().await.unwrap();
let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
.await
.unwrap();
assert_eq!(journal.section_len(SECTION).unwrap(), 3);
assert_eq!(journal.get(SECTION, 0).await.unwrap(), 0);
assert_eq!(journal.get(SECTION, 1).await.unwrap(), 1);
assert_eq!(journal.get(SECTION, 2).await.unwrap(), 99);
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_segmented_fixed_truncation_recovery_across_page_boundary() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("first"), cfg.clone())
.await
.expect("failed to init");
for i in 0u64..3 {
(journal, _) = journal
.append(1, &test_digest(i))
.await
.expect("failed to append");
}
journal = journal.sync_all().await.expect("failed to sync");
for i in 0u64..3 {
let item = journal.get(1, i).await.expect("failed to get");
assert_eq!(item, test_digest(i));
}
drop(journal);
let (blob, size) = context
.open(&cfg.partition, &1u64.to_be_bytes())
.await
.expect("failed to open blob");
blob.resize(size - 1).await.expect("failed to truncate");
blob.sync().await.expect("failed to sync");
drop(blob);
let journal = Journal::<_, Digest>::init(context.child("second"), cfg.clone())
.await
.expect("failed to re-init");
let journal = replay_all(journal).await;
assert_eq!(journal.section_len(1).unwrap(), 2);
assert_eq!(journal.size(1).unwrap(), 64);
let item0 = journal.get(1, 0).await.expect("failed to get item 0");
assert_eq!(item0, test_digest(0));
let item1 = journal.get(1, 1).await.expect("failed to get item 1");
assert_eq!(item1, test_digest(1));
let err = journal.get(1, 2).await;
assert!(
matches!(err, Err(Error::ItemOutOfRange(2))),
"expected ItemOutOfRange(2), got {:?}",
err
);
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_journal_clear() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = Config {
partition: "clear-test".into(),
page_cache: CacheRef::from_pooler(&context, PAGE_SIZE, PAGE_CACHE_SIZE),
write_buffer: NZUsize!(1024),
};
let mut journal: Journal<_, Digest> =
Journal::init(context.child("journal"), cfg.clone())
.await
.expect("Failed to initialize journal");
for section in 0..5u64 {
for i in 0..10u64 {
(journal, _) = journal
.append(section, &test_digest(section * 1000 + i))
.await
.expect("Failed to append");
}
journal = journal.sync(section).await.expect("Failed to sync");
}
assert_eq!(journal.get(0, 0).await.unwrap(), test_digest(0));
assert_eq!(journal.get(4, 0).await.unwrap(), test_digest(4000));
journal = journal.clear().await.expect("Failed to clear");
for section in 0..5u64 {
assert!(matches!(
journal.get(section, 0).await,
Err(Error::SectionOutOfRange(s)) if s == section
));
}
for i in 0..5u64 {
(journal, _) = journal
.append(10, &test_digest(i * 100))
.await
.expect("Failed to append after clear");
}
journal = journal.sync(10).await.expect("Failed to sync after clear");
assert_eq!(journal.get(10, 0).await.unwrap(), test_digest(0));
assert!(matches!(
journal.get(0, 0).await,
Err(Error::SectionOutOfRange(0))
));
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_last_missing_section_returns_error() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
assert!(matches!(
journal.last(0).await,
Err(Error::SectionOutOfRange(0))
));
assert!(matches!(
journal.last(99).await,
Err(Error::SectionOutOfRange(99))
));
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_last_after_rewind_to_zero() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
(journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
(journal, _) = journal.append(0, &test_digest(1)).await.unwrap();
journal = journal.sync(0).await.unwrap();
assert!(journal.last(0).await.unwrap().is_some());
journal = journal.rewind(0, 0).await.unwrap();
assert_eq!(journal.last(0).await.unwrap(), None);
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_last_pruned_section_returns_error() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::<_, Digest>::init(context.child("storage"), cfg.clone())
.await
.expect("failed to init");
(journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
(journal, _) = journal.append(1, &test_digest(1)).await.unwrap();
journal = journal.sync_all().await.unwrap();
(journal, _) = journal.prune(1).await.unwrap();
assert!(matches!(
journal.last(0).await,
Err(Error::AlreadyPrunedToSection(1))
));
assert!(journal.last(1).await.unwrap().is_some());
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_get_many_empty() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
(journal, _) = journal.append(0, &test_digest(0)).await.unwrap();
assert_eq!(journal.section_len(0).unwrap(), 1);
let mut buf = [];
let (items, hits) = journal.get_many(0, &[], &mut buf).await.unwrap();
assert!(items.is_empty());
assert_eq!(hits, 0);
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_get_many_single_section() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
for i in 0..5 {
(journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
}
assert_eq!(journal.section_len(0).unwrap(), 5);
let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
let mut buf = vec![0u8; 6 * chunk];
let (items, _) = journal
.get_many(0, &[0, 1, 2, 3, 4], &mut buf)
.await
.unwrap();
for (i, item) in items.iter().enumerate() {
assert_eq!(*item, test_digest(i as u64));
}
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_get_many_subset() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
for i in 0..10 {
(journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
}
assert_eq!(journal.section_len(0).unwrap(), 10);
let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
let positions = [1, 4, 7, 9];
let mut buf = vec![0u8; positions.len() * chunk];
let (items, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
for (i, &pos) in positions.iter().enumerate() {
assert_eq!(items[i], test_digest(pos));
}
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_get_many_bad_section() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let journal = Journal::<_, Digest>::init(context.child("storage"), cfg)
.await
.unwrap();
let mut buf = vec![0u8; 64];
let err = journal.get_many(99, &[0], &mut buf).await.unwrap_err();
assert!(matches!(err, Error::SectionOutOfRange(99)));
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_get_many_matches_get() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg).await.unwrap();
for i in 0..8 {
(journal, _) = journal.append(0, &test_digest(i)).await.unwrap();
}
assert_eq!(journal.section_len(0).unwrap(), 8);
journal = journal.sync_all().await.unwrap();
let chunk = Journal::<deterministic::Context, Digest>::CHUNK_SIZE;
let positions: Vec<u64> = (0..8).collect();
let mut buf = vec![0u8; positions.len() * chunk];
let (batch, _) = journal.get_many(0, &positions, &mut buf).await.unwrap();
for pos in &positions {
let single = journal.get(0, *pos).await.unwrap();
assert_eq!(batch[*pos as usize], single);
}
journal.destroy().await.unwrap();
});
}
#[test_traced]
fn test_segmented_fixed_prune_waits_for_in_flight_start_sync() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let pending = PendingSyncs::default();
let context = DelayedSyncContext {
inner: context,
pending: pending.clone(),
};
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg)
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
let handle;
(journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
assert!(!pending.lock().is_empty());
let started = Arc::new(AtomicUsize::new(0));
let completed = Arc::new(AtomicUsize::new(0));
let started_clone = started.clone();
let completed_clone = completed.clone();
let waiter = context.inner.child("prune").spawn(|_| async move {
started_clone.fetch_add(1, Ordering::Relaxed);
let (journal, pruned) = journal.prune(2).await.expect("failed to prune");
assert!(pruned);
completed_clone.fetch_add(1, Ordering::Relaxed);
journal
});
while started.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
commonware_runtime::reschedule().await;
assert_eq!(
completed.load(Ordering::Relaxed),
0,
"prune must wait for in-flight syncs on pruned sections"
);
release_pending_syncs(&pending);
handle
.await
.expect("sync handle should complete despite pruning");
while completed.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
let journal = waiter.await.expect("prune task failed");
assert_eq!(journal.oldest_section(), None);
});
}
#[test_traced]
fn test_segmented_fixed_destroy_waits_for_in_flight_start_sync() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let pending = PendingSyncs::default();
let context = DelayedSyncContext {
inner: context,
pending: pending.clone(),
};
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg)
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
let handle;
(journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
assert!(!pending.lock().is_empty());
let started = Arc::new(AtomicUsize::new(0));
let completed = Arc::new(AtomicUsize::new(0));
let started_clone = started.clone();
let completed_clone = completed.clone();
let waiter = context.inner.child("destroy").spawn(|_| async move {
started_clone.fetch_add(1, Ordering::Relaxed);
journal.destroy().await.expect("failed to destroy");
completed_clone.fetch_add(1, Ordering::Relaxed);
});
while started.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
commonware_runtime::reschedule().await;
assert_eq!(
completed.load(Ordering::Relaxed),
0,
"destroy must wait for in-flight syncs"
);
release_pending_syncs(&pending);
handle
.await
.expect("sync handle should complete despite destruction");
while completed.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
waiter.await.expect("destroy task failed");
});
}
#[test_traced]
fn test_segmented_fixed_clear_waits_for_in_flight_start_sync() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let pending = PendingSyncs::default();
let context = DelayedSyncContext {
inner: context,
pending: pending.clone(),
};
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg)
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
let handle;
(journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
assert!(!pending.lock().is_empty());
let started = Arc::new(AtomicUsize::new(0));
let completed = Arc::new(AtomicUsize::new(0));
let started_clone = started.clone();
let completed_clone = completed.clone();
let waiter = context.inner.child("clear").spawn(|_| async move {
started_clone.fetch_add(1, Ordering::Relaxed);
journal = journal.clear().await.expect("failed to clear");
completed_clone.fetch_add(1, Ordering::Relaxed);
journal
});
while started.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
commonware_runtime::reschedule().await;
assert_eq!(
completed.load(Ordering::Relaxed),
0,
"clear must wait for in-flight syncs"
);
release_pending_syncs(&pending);
handle
.await
.expect("sync handle should complete despite clearing");
while completed.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
let mut journal = waiter.await.expect("clear task failed");
assert_eq!(journal.oldest_section(), None);
let position;
(journal, position) = journal
.append(1, &test_digest(1))
.await
.expect("failed to append after clear");
assert_eq!(position, 0);
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_rewind_waits_for_in_flight_start_sync() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let pending = PendingSyncs::default();
let context = DelayedSyncContext {
inner: context,
pending: pending.clone(),
};
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg)
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
(journal, _) = journal
.append(2, &test_digest(1))
.await
.expect("failed to append");
let handle;
(journal, handle) = journal.start_sync(2).await.expect("failed to start sync");
assert!(!pending.lock().is_empty());
let size = journal.size(1).expect("failed to get size");
let started = Arc::new(AtomicUsize::new(0));
let completed = Arc::new(AtomicUsize::new(0));
let started_clone = started.clone();
let completed_clone = completed.clone();
let waiter = context.inner.child("rewind").spawn(move |_| async move {
started_clone.fetch_add(1, Ordering::Relaxed);
journal = journal.rewind(1, size).await.expect("failed to rewind");
completed_clone.fetch_add(1, Ordering::Relaxed);
journal
});
while started.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
commonware_runtime::reschedule().await;
assert_eq!(
completed.load(Ordering::Relaxed),
0,
"rewind must wait for in-flight syncs on removed sections"
);
release_pending_syncs(&pending);
handle
.await
.expect("sync handle should complete despite rewind");
while completed.load(Ordering::Relaxed) == 0 {
commonware_runtime::reschedule().await;
}
let journal = waiter.await.expect("rewind task failed");
assert_eq!(journal.size(2).expect("failed to get size"), 0);
journal.destroy().await.expect("failed to destroy");
});
}
#[test_traced]
fn test_segmented_fixed_prune_surfaces_failed_in_flight_start_sync() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let pending = PendingSyncs::default();
let context = DelayedSyncContext {
inner: context,
pending: pending.clone(),
};
let cfg = test_cfg(&context);
let mut journal = Journal::init(context.child("storage"), cfg)
.await
.expect("failed to init");
(journal, _) = journal
.append(1, &test_digest(0))
.await
.expect("failed to append");
let handle;
(journal, handle) = journal.start_sync(1).await.expect("failed to start sync");
fail_pending_syncs(&pending);
let err = journal
.prune(2)
.await
.expect_err("prune must surface a failed in-flight sync");
assert!(matches!(err, Error::Runtime(RError::Io(_))));
let err = handle.await.expect_err("sync handle should fail");
assert!(matches!(err, RError::Io(_)));
});
}
}