use super::manager::{
AppendFactory, Config as ManagerConfig, Manager, section_from_name, stored_names,
};
use crate::journal::Error;
use commonware_codec::{CodecFixed, CodecFixedShared, Copying, DecodeExt as _, ReadExt as _};
use commonware_runtime::{
Blob, Error as RError, Handle, Metrics, ReadOptions, Storage,
buffer::paged::{CacheRef, Recovery as PagedRecovery, Replay as BlobReplay, Writer},
};
use commonware_utils::{Cached, NZUsize};
use std::{
collections::{BTreeMap, BTreeSet, VecDeque},
marker::PhantomData,
num::{NonZeroU16, NonZeroUsize},
};
use tracing::{trace, warn};
commonware_utils::thread_local_cache!(static READ_SCRATCH: Vec<u8>);
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 {
floors: BTreeMap<u64, u64>,
ceiling: 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, self.mode).await?,
)))
}
}
impl<E: Storage + Metrics, A: CodecFixed> Inner<E, A> {
fn writer(&self, section: u64) -> Result<&PagedRecovery<E::Blob>, Error> {
Ok(self
.manager
.get(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>,
ceiling: u64,
) -> Result<RecoveryPreflight<E, A>, Error> {
assert!(
minimum_items.keys().all(|section| *section <= ceiling),
"validation floors must not exceed the recovery ceiling"
);
let stored = Self::stored(&context, &cfg).await?;
let page_size = cfg.page_cache.page_size();
let mut floors = 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?;
floors.insert(section, required);
boundaries.insert(section, Some(entry));
}
Ok(RecoveryPreflight {
context,
cfg,
boundaries,
mode: PreflightMode::Floors { floors, ceiling },
})
}
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: PreflightMode) -> Result<Self, Error> {
let (floors, restore, ceiling) = match mode {
PreflightMode::Floors { floors, ceiling } => (floors, None, ceiling),
PreflightMode::Restore { section, floors } => {
let size = floors.get(§ion).copied().unwrap_or(0);
(floors, Some((section, size)), section)
}
};
let manager_cfg = ManagerConfig {
partition: cfg.partition,
factory: AppendFactory {
write_buffer: cfg.write_buffer,
page_cache_ref: cfg.page_cache,
},
};
let manager = Manager::init_bounded(context, manager_cfg, ceiling).await?;
if let Some((section, size)) = restore {
let manager = manager
.truncate_pending(section, size)
.await?
.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 offset = match blob.try_append_value(item) {
Some(offset) => offset,
None => {
let blob = self.manager.take(section).await?;
let (blob, offset) = blob.append_owned(item.encode_mut().into()).await?;
self.manager.put(section, blob);
offset
}
};
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 mut offsets = positions.to_vec();
for offset in &mut offsets {
*offset = offset
.checked_mul(Self::CHUNK_SIZE_U64)
.ok_or(Error::ItemOutOfRange(*offset))?;
}
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(Copying(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 scratch =
Cached::take(&READ_SCRATCH, || Ok::<_, ()>(Vec::new()), |_| Ok(())).unwrap();
if scratch.len() < Self::CHUNK_SIZE {
scratch.resize(Self::CHUNK_SIZE, 0);
}
let buf = &mut scratch[..Self::CHUNK_SIZE];
if !blob.try_read_sync_into(buf, offset) {
return None;
}
A::decode(Copying(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: Box<Self>, sections: impl crate::Sections) -> Result<Box<Self>, Error> {
self.manager = self.manager.sync(sections).await?;
Ok(self)
}
async fn start_sync(
mut self: Box<Self>,
sections: impl crate::Sections,
) -> Result<(Box<Self>, Handle<()>), Error> {
let (manager, handle) = self.manager.start_sync(sections).await?;
self.manager = manager;
Ok((self, handle))
}
async fn sync_all(mut self: Box<Self>) -> Result<Box<Self>, Error> {
self.manager = self.manager.sync_all().await?;
Ok(self)
}
async fn prune(mut self: Box<Self>, min: u64) -> Result<(Box<Self>, bool), Error> {
let (manager, pruned) = self.manager.prune(min).await?;
self.manager = manager;
if pruned {
self.unrecovered.retain(|section| *section >= min);
self.floors.retain(|section, _| *section >= min);
}
Ok((self, 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 truncate_pending_section(
mut self: Box<Self>,
section: u64,
size: u64,
) -> Result<Box<Self>, Error> {
self.manager = self.manager.truncate_pending_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(self)
}
async fn destroy(self) -> Result<(), Error> {
self.manager.destroy().await
}
async fn clear(mut self: Box<Self>) -> Result<Box<Self>, Error> {
self.manager = self.manager.clear().await?;
self.unrecovered.clear();
self.floors.clear();
Ok(self)
}
}
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_at_most(
context: E,
cfg: Config,
section: u64,
end: u64,
) -> Result<Self, Error> {
let replay_buffer = cfg.write_buffer;
let mut end = end - end % Self::CHUNK_SIZE as u64;
super::manager::truncate_paged_tail(
&context,
&cfg.partition,
cfg.page_cache.page_size(),
section,
end,
)
.await?;
let mut journal = Self::init(context, cfg).await?;
if end > 0
&& journal
.0
.manager
.get(section)?
.is_some_and(|blob| end < blob.size())
{
end = journal
.recoverable_prefix_len_at_most(section, end, replay_buffer)
.await?;
}
journal.0.manager = journal
.0
.manager
.truncate_pending(section, end - end % Self::CHUNK_SIZE as u64)
.await?;
let mut replay = journal
.replay(0, 0, replay_buffer, ReadOptions::default())
.await?;
while let Some(item) = replay.next().await {
item?;
}
replay.finish()
}
pub async fn init(context: E, cfg: Config) -> Result<Self, Error> {
Self::init_bounded(context, cfg, u64::MAX).await
}
pub(crate) async fn init_bounded(context: E, cfg: Config, ceiling: u64) -> Result<Self, Error> {
let mode = PreflightMode::Floors {
floors: BTreeMap::new(),
ceiling,
};
Ok(Self(Box::new(Inner::init(context, cfg, mode).await?)))
}
pub(crate) async fn preflight_floors(
context: E,
cfg: Config,
minimum_items: &BTreeMap<u64, u64>,
ceiling: u64,
) -> Result<RecoveryPreflight<E, A>, Error> {
Inner::preflight_floors(context, cfg, minimum_items, ceiling).await
}
pub(crate) async fn recoverable_prefix_len_at_most(
&self,
section: u64,
end: u64,
buffer: NonZeroUsize,
) -> Result<u64, Error> {
let Some(blob) = self.0.manager.get(section)? else {
return Ok(end);
};
let recoverable = blob
.recoverable_prefix_len_at_most(0, end, buffer, ReadOptions::default())
.await?;
Ok(recoverable - recoverable % Self::CHUNK_SIZE as u64)
}
pub(crate) async fn truncate_pending_tail(
mut self,
section: u64,
end: u64,
) -> Result<Self, Error> {
self.0.manager = self.0.manager.truncate_pending(section, end).await?;
self.0.unrecovered.retain(|candidate| *candidate <= section);
self.0.floors.retain(|candidate, _| *candidate <= section);
Ok(self)
}
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();
let replayed: Vec<_> = self.0.manager.sections_from(start_section).collect();
for section in replayed {
let blob = self.0.manager.take(section).await?;
let blob_size = blob.size();
let (blob, mut reader) = blob.replay(buffer, read_options).await?;
self.0.manager.put(section, blob);
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 = self.0.sync(sections).await?;
Ok(self)
}
pub async fn start_sync(
mut self,
sections: impl crate::Sections,
) -> Result<(Self, Handle<()>), Error> {
let (inner, handle) = self.0.start_sync(sections).await?;
self.0 = inner;
Ok((self, handle))
}
pub async fn sync_all(mut self) -> Result<Self, Error> {
self.0 = self.0.sync_all().await?;
Ok(self)
}
pub async fn prune(mut self, min: u64) -> Result<(Self, bool), Error> {
let (inner, pruned) = self.0.prune(min).await?;
self.0 = inner;
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(crate) async fn truncate_pending_section(
mut self,
section: u64,
size: u64,
) -> Result<Self, Error> {
self.0 = self.0.truncate_pending_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 = 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;
if !self.journal.0.unrecovered.contains(§ion) {
return Err(source.into());
}
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);
let writer = self.journal.0.manager.take(section).await?;
let writer = writer.truncate(target).await?;
let (writer, mut reader) = writer.replay(self.buffer, self.read_options).await?;
self.journal.0.manager.put(section, writer);
reader.seek_to(valid_size)?;
self.sections.push_front(SectionReplay {
section,
reader,
position,
});
self.repairing = false;
Ok(())
}
async fn repair_tail(&mut self, section: u64, size: u64) -> Result<(), Error> {
self.repairing = true;
let writer = self.journal.0.manager.take(section).await?;
let writer = writer.truncate(size).await?;
self.journal.0.manager.put(section, writer);
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 !self.journal.0.unrecovered.contains(§ion) {
let position = current.position;
self.sections.pop_front();
return self.fail(Error::ItemOutOfRange(position));
}
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"
);
if let Err(err) = self.repair_tail(section, valid_size).await {
self.sections.pop_front();
return self.fail(err);
}
}
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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::codec::View;
use commonware_codec::FixedSize;
use commonware_cryptography::{Hasher as _, Sha256, sha256::Digest};
use commonware_macros::test_traced;
use commonware_runtime::{
Error as RError, Runner, Spawner as _, Supervisor as _,
buffer::paged::{CacheRef, 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},
},
};
impl<E: Storage + Metrics, A: CodecFixedShared> Journal<E, A> {
pub(in super::super) async fn test_reopen_at_most(
self,
section: u64,
end: u64,
) -> Result<Self, Error> {
let (context, partition, factory) = self.0.manager.test_configuration();
let cfg = Config {
partition,
write_buffer: factory.write_buffer,
page_cache: factory.page_cache_ref,
};
_ = self.sync_all().await?;
Self::init_at_most(context, cfg, section, end).await
}
async fn test_reopen_section(self, section: u64, end: u64) -> Result<Self, Error> {
let (context, partition, factory) = self.0.manager.test_configuration();
let replay_buffer = factory.write_buffer;
let cfg = Config {
partition,
write_buffer: replay_buffer,
page_cache: factory.page_cache_ref,
};
_ = self.sync_all().await?;
let mut journal = Self::init(context, cfg).await?;
journal.0.manager = journal
.0
.manager
.truncate_pending_section(section, end)
.await?;
let mut replay = journal
.replay(0, 0, replay_buffer, ReadOptions::default())
.await?;
while let Some(item) = replay.next().await {
item?;
}
replay.finish()
}
pub(in super::super) fn test_configuration(&self) -> (E, String, AppendFactory) {
self.0.manager.test_configuration()
}
}
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() -> Config {
Config {
partition: "test-partition".into(),
page_cache: CacheRef::new(PAGE_SIZE, PAGE_CACHE_SIZE),
write_buffer: NZUsize!(2048),
}
}
fn aligned_cfg() -> Config {
Config {
partition: "segmented-fixed-aligned".into(),
page_cache: CacheRef::new(NZU16!(16), NZUsize!(4)),
write_buffer: NZUsize!(128),
}
}
#[test_traced]
fn test_fixed_cached_get_preserves_owned_byte_fields() {
deterministic::Runner::default().start(|context| async move {
let cfg = test_cfg();
let mut journal = Journal::init(context, cfg).await.unwrap();
(journal, _) = journal.append(1, &View::new(7)).await.unwrap();
let decoded = journal.try_get_sync(1, 0).unwrap();
journal.destroy().await.unwrap();
assert_eq!(decoded.bytes.as_ref(), &7u64.to_be_bytes());
});
}
fn lazy_recovery_cfg(partition: &str) -> Config {
Config {
partition: partition.into(),
page_cache: CacheRef::new(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]
fn test_segmented_capped_init_bounds_torn_suffix_reads() {
deterministic::Runner::default().start(|context| async move {
for source_section in [0, 1] {
for cap in [0, 8] {
let mut counts = Vec::new();
for pages in [16, 4096] {
let partition = format!("torn-cap-{source_section}-{cap}-{pages}");
let page: Vec<u8> = (0..9u64).flat_map(u64::to_be_bytes).collect();
assert_eq!(page.len(), 72);
super::super::manager::tests::seed_torn_suffix(
&context,
&partition,
source_section,
&page,
pages,
)
.await;
let cfg = Config {
partition,
page_cache: CacheRef::new(NZU16!(72), NZUsize!(4)),
write_buffer: NZUsize!(144),
};
let (recorded, recordings) = RecordingContext::new(context.child("cap"));
let mut journal =
Journal::<_, u64>::init_at_most(recorded, cfg.clone(), 0, cap)
.await
.unwrap();
let retained = if source_section == 0 { cap } else { 0 };
assert_eq!(journal.size(0).unwrap(), retained);
counts.push(recordings.snapshot().reads.len());
(journal, _) = journal.append(0, &999).await.unwrap();
drop(journal.sync_all().await.unwrap());
let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
.await
.unwrap();
let mut replay = journal
.replay(0, 0, NZUsize!(144), ReadOptions::default())
.await
.unwrap();
let mut actual = Vec::new();
while let Some(item) = replay.next().await {
actual.push(item.unwrap().2);
}
replay.finish().unwrap();
let expected = if retained == 0 {
vec![999]
} else {
vec![0, 999]
};
assert_eq!(actual, expected);
}
assert_eq!(
counts[0], counts[1],
"discarded suffix reads grew: {counts:?}"
);
}
}
});
}
#[test_traced]
fn test_bounded_initialization_bounds_scan() {
deterministic::Runner::default().start(|context| async move {
let mut counts = Vec::new();
for count in [128, 65536] {
let cfg = Config {
partition: format!("fixed-cap-scan-{count}"),
page_cache: CacheRef::new(NZU16!(64), NZUsize!(4)),
write_buffer: NZUsize!(65536),
};
let mut journal = Journal::<_, u64>::init(
context.child(if count == 128 {
"seed_small"
} else {
"seed_large"
}),
cfg.clone(),
)
.await
.unwrap();
for value in 0..count {
(journal, _) = journal.append(0, &value).await.unwrap();
}
_ = journal.sync_all().await.unwrap();
let (recorded, recordings) =
RecordingContext::new(context.child(if count == 128 {
"cap_small"
} else {
"cap_large"
}));
let journal = Journal::<_, u64>::init_at_most(recorded, cfg, 0, 8)
.await
.unwrap();
assert_eq!(journal.section_len(0).unwrap(), 1);
counts.push(recordings.snapshot().reads.len());
}
assert_eq!(
counts[0], counts[1],
"same one-item cap: small/large suffix read counts {counts:?}"
);
});
}
#[rstest::rstest]
#[case(30)]
#[case(31)]
#[case(32)]
fn test_bounded_initialization_recovers_torn_page(#[case] tail: u64) {
deterministic::Runner::default().start(|context| async move {
for cap in [0, 8, 16, 20, 24, 28, 29, 30, 31, 32, u64::MAX] {
let cfg = Config {
partition: format!("capped-torn-{cap}"),
page_cache: CacheRef::new(NZU16!(5), NZUsize!(4)),
write_buffer: NZUsize!(256),
};
let mut journal = Journal::<_, u64>::init(context.child("first"), cfg.clone())
.await
.unwrap();
for value in 0..4 {
(journal, _) = journal.append(0, &value).await.unwrap();
}
(journal, _) = journal.append(1, &100).await.unwrap();
_ = journal.sync_all().await.unwrap();
let (blob, size) = context
.open(&cfg.partition, &0u64.to_be_bytes())
.await
.unwrap();
let partial = commonware_runtime::buffer::paged::Recovery::open(
blob,
size,
cfg.write_buffer.get(),
cfg.page_cache.clone(),
)
.await
.unwrap();
partial.truncate(tail).await.unwrap();
corrupt_page(&context, &cfg.partition, &0u64.to_be_bytes(), 4, 5).await;
let journal =
Journal::<_, u64>::init_at_most(context.child("cap"), cfg.clone(), 0, cap)
.await
.unwrap();
let expected_bytes = cap.min(16) / 8 * 8;
assert_eq!(journal.size(0).unwrap(), expected_bytes, "cap {cap}");
assert_eq!(journal.newest_section(), Some(0));
drop(journal);
let journal = Journal::<_, u64>::init(context.child("reopen"), cfg)
.await
.unwrap();
let mut replay = journal
.replay(0, 0, NZUsize!(256), ReadOptions::default())
.await
.unwrap();
let mut count = 0;
while let Some(item) = replay.next().await {
let item = item.unwrap();
assert_eq!(item.0, 0);
assert_eq!(item.2, count);
count += 1;
}
assert_eq!(count, expected_bytes / 8);
let mut journal = replay.finish().unwrap();
let position;
(journal, position) = journal.append(0, &99).await.unwrap();
assert_eq!(position, expected_bytes / 8);
journal.destroy().await.unwrap();
}
});
}
#[test_traced]
fn test_segmented_fixed_append_and_get() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg();
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");
});
}
#[rstest::rstest]
#[case(100)]
#[case(8193)]
#[case(32769)]
fn test_segmented_fixed_synced_sections_partial_pages(#[case] items: u64) {
const PAGE_SIZE: usize = 4096;
const WRITE_BUFFER: usize = 131072;
const SECTIONS: u64 = 8;
const CACHE_PAGES: usize = 4;
deterministic::Runner::default().start(move |context| async move {
let cfg = Config {
partition: "segmented-fixed-buffers".into(),
page_cache: CacheRef::new(NZU16!(PAGE_SIZE as u16), NZUsize!(CACHE_PAGES)),
write_buffer: NZUsize!(WRITE_BUFFER),
};
let mut journal = Journal::<_, u64>::init(context.child("journal"), cfg)
.await
.unwrap();
for section in 0..SECTIONS {
for value in 0..items {
(journal, _) = journal.append(section, &value).await.unwrap();
}
journal = journal.sync_all().await.unwrap();
}
for section in 0..SECTIONS {
assert_eq!(journal.get(section, 0).await.unwrap(), 0);
assert_eq!(journal.get(section, items - 1).await.unwrap(), items - 1);
(journal, _) = journal.append(section, &items).await.unwrap();
}
journal = journal.sync_all().await.unwrap();
for section in 0..SECTIONS {
assert_eq!(journal.get(section, items).await.unwrap(), items);
}
});
}
#[test_traced]
fn test_segmented_fixed_append_buffer_boundaries() {
for page_size in [NZU16!(8), NZU16!(16), PAGE_SIZE] {
deterministic::Runner::default().start(|context| async move {
let cfg = Config {
partition: "buffer-boundaries".into(),
page_cache: CacheRef::new(page_size, PAGE_CACHE_SIZE),
write_buffer: NZUsize!(1),
};
let mut journal = Journal::init(context.child("initial"), cfg.clone())
.await
.unwrap();
for section in [1, 3] {
for position in 0..10 {
let item = test_digest(section * 100 + position);
let appended;
(journal, appended) = journal.append(section, &item).await.unwrap();
assert_eq!(appended, position);
assert_eq!(journal.section_len(section).unwrap(), position + 1);
assert_eq!(journal.get(section, position).await.unwrap(), item);
}
}
journal = journal.sync_all().await.unwrap();
drop(journal);
let journal = Journal::<_, Digest>::init(context, cfg).await.unwrap();
let mut replay = journal
.replay(0, 0, NZUsize!(128), ReadOptions::default())
.await
.unwrap();
for section in [1, 3] {
for position in 0..10 {
assert_eq!(
replay.next().await.unwrap().unwrap(),
(section, position, test_digest(section * 100 + position))
);
}
}
assert!(replay.next().await.is_none());
let mut journal = replay.finish().unwrap();
let position;
(journal, position) = journal.append(3, &test_digest(310)).await.unwrap();
assert_eq!(position, 10);
assert_eq!(journal.get(3, 10).await.unwrap(), test_digest(310));
journal.destroy().await.unwrap();
});
}
}
#[test_traced]
fn test_segmented_fixed_replay_empty_finishes_immediately() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg();
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 = aligned_cfg();
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_replay_leaves_page_cache_cold() {
deterministic::Runner::default().start(|context| async move {
let (context, recordings) = RecordingContext::new(context);
let cfg = aligned_cfg();
let page_cache = cfg.page_cache.clone();
let mut journal = Journal::<_, [u8; 16]>::init(context.child("storage"), cfg)
.await
.unwrap();
for value in 0..2 {
(journal, _) = journal.append(1, &[value; 16]).await.unwrap();
}
journal = journal.sync(1).await.unwrap();
page_cache.clear();
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::DONT_CACHE)
.await
.unwrap();
for value in 0..2 {
assert_eq!(
replay.next().await.unwrap().unwrap(),
(1, u64::from(value), [value; 16])
);
}
assert!(replay.next().await.is_none());
let journal = replay.finish().unwrap();
for position in 0..2 {
assert!(journal.try_get_sync(1, position).is_none());
}
recordings.clear();
assert_eq!(journal.get(1, 0).await.unwrap(), [0; 16]);
assert!(!recordings.snapshot().reads.is_empty());
assert_eq!(journal.try_get_sync(1, 0), Some([0; 16]));
recordings.clear();
assert_eq!(journal.get(1, 0).await.unwrap(), [0; 16]);
assert!(recordings.snapshot().reads.is_empty());
journal.destroy().await.unwrap();
});
}
#[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("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!(448), 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();
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();
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("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,
u64::MAX,
)
.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("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("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
.test_reopen_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("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::Runtime(RError::InvalidChecksum))
));
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();
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,
u64::MAX,
)
.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();
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();
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();
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();
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();
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_truncate() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg();
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
.test_reopen_at_most(1, size)
.await
.expect("failed to truncate");
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_truncate_max_section() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg();
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
.test_reopen_at_most(u64::MAX, size)
.await
.expect("failed to truncate");
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_truncate_many_sections() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg();
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
.test_reopen_at_most(5, size)
.await
.expect("failed to truncate");
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_truncate_persistence() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg();
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
.test_reopen_at_most(2, size)
.await
.expect("failed to truncate");
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();
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");
drop(blob);
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();
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();
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();
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();
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();
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
.test_reopen_section(2, 0)
.await
.expect("failed to truncate");
(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::new(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 writer = commonware_runtime::buffer::paged::Recovery::open(
blob,
size,
128,
cfg.page_cache.clone(),
)
.await
.unwrap();
let writer = writer.truncate(30).await.unwrap();
writer.sync().await.unwrap();
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();
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();
});
}
#[rstest::rstest]
#[case::tail(false)]
#[case::torn(true)]
fn test_segmented_fixed_replay_dropped_during_repair_fails_replay(#[case] torn: bool) {
const SECTION: u64 = 0;
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = aligned_cfg();
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);
let (drained, retained) = if torn {
corrupt_page(&context, &cfg.partition, &SECTION.to_be_bytes(), 1, 16).await;
(0, 2)
} else {
let (blob, size) = context
.open(&cfg.partition, &SECTION.to_be_bytes())
.await
.unwrap();
let writer = commonware_runtime::buffer::paged::Recovery::open(
blob,
size,
cfg.write_buffer.get(),
cfg.page_cache.clone(),
)
.await
.unwrap();
writer.truncate(44).await.unwrap();
(5, 5)
};
let pending = PendingSyncs::default();
let gated = DelayedSyncContext {
inner: context.child("second"),
pending: pending.clone(),
};
let journal = Journal::<_, u64>::init(gated, cfg.clone()).await.unwrap();
let mut replay = journal
.replay(0, 0, NZUsize!(1024), ReadOptions::default())
.await
.unwrap();
for value in 0..drained {
assert_eq!(
replay.next().await.unwrap().unwrap(),
(SECTION, value, value)
);
}
pending.arm();
{
let next = replay.next();
futures::pin_mut!(next);
assert!(futures::poll!(next.as_mut()).is_pending());
}
release_pending_syncs(&pending);
assert!(matches!(
replay.next().await,
Some(Err(Error::ReplayInterrupted))
));
assert!(replay.next().await.is_none());
assert!(matches!(replay.finish(), Err(Error::ReplayFailed)));
let journal = Journal::<_, u64>::init(context.child("third"), cfg)
.await
.unwrap();
let journal = replay_all(journal).await;
assert_eq!(journal.section_len(SECTION).unwrap(), retained);
for value in 0..retained {
assert_eq!(journal.get(SECTION, value).await.unwrap(), value);
}
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();
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::new(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();
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_truncate_to_zero() {
let executor = deterministic::Runner::default();
executor.start(|context| async move {
let cfg = test_cfg();
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.test_reopen_at_most(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();
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();
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();
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();
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();
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();
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();
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();
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();
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_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();
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(_)));
});
}
}