use super::*;
use std::ops::Deref;
pub(in crate::canonical) struct Accumulator {
segment: SegmentAccumulator,
pending_push: Option<record_push::Progress>,
pending_properties: Option<node_properties::Progress>,
memory: CheckpointAllocationOwner,
work: CheckpointWorkContext,
admitted: bool,
pending_flush: Option<(u64, hawdb_integrity::IntegrityDigest)>,
flush_progress: checkpoint_flush::FlushProgress,
}
impl Deref for Accumulator {
type Target = SegmentAccumulator;
fn deref(&self) -> &Self::Target {
&self.segment
}
}
impl Accumulator {
pub(crate) fn bind_checkpoint_attempt(
&mut self,
work: &crate::background::CheckpointWorkContext,
) {
self.work = work.clone();
}
pub(in crate::canonical) fn new(
kind: CanonicalSegmentKind,
generation: ManifestGeneration,
segment_id: u64,
config: CanonicalSegmentConfig,
work: CheckpointWorkContext,
admitted: bool,
) -> Self {
Self {
segment: SegmentAccumulator::new(kind, generation, segment_id, config),
pending_push: None,
pending_properties: None,
memory: CheckpointAllocationOwner::default(),
work,
admitted,
pending_flush: None,
flush_progress: checkpoint_flush::FlushProgress::default(),
}
}
#[cfg(test)]
fn with_memory<T>(
&mut self,
operation: impl FnOnce(
&mut SegmentAccumulator,
&CheckpointDecodeContext,
) -> Result<T, CanonicalSegmentError>,
) -> Result<T, CanonicalSegmentError> {
let work = self.work.clone();
work.classify(|work| {
let context = CheckpointDecodeContext {
work: work.clone(),
memory: RefCell::new(std::mem::take(&mut self.memory)),
};
let result = operation(&mut self.segment, &context);
self.memory = context.memory.into_inner();
result
})
.map_err(|error| match error {
CheckpointOperationError::Work(error) => CanonicalSegmentError::Work(error),
CheckpointOperationError::Operation(error) => error,
})
}
pub(in crate::canonical) fn push(
&mut self,
record_id: u64,
payload: &[u8],
endpoints: Option<(u64, u64)>,
) -> Result<(), CanonicalSegmentError> {
if self.pending_flush.is_some() || self.pending_properties.is_some() {
return Err(CanonicalSegmentError::Corrupt(
"canonical input cannot change during retained property or flush recovery".into(),
));
}
if !self.admitted {
let unit = self.work.start_unit()?;
self.segment.push(record_id, payload, endpoints)?;
unit.finish();
return Ok(());
}
let work = self.work.clone();
self.push_retained_with_work_context(record_id, payload, endpoints, &work)
}
pub(in crate::canonical) fn push_retained_with_work_context(
&mut self,
record_id: u64,
payload: &[u8],
endpoints: Option<(u64, u64)>,
work: &CheckpointWorkContext,
) -> Result<(), CanonicalSegmentError> {
if self.pending_flush.is_some() || self.pending_properties.is_some() {
return Err(CanonicalSegmentError::Corrupt(
"canonical input cannot change during retained property or flush recovery".into(),
));
}
record_push::push(self, record_id, payload, endpoints, work)
}
pub(in crate::canonical) fn add_node_properties(
&mut self,
node: &NodeRecord,
work: Option<&CheckpointWorkContext>,
) -> Result<(), CanonicalSegmentError> {
if self.pending_flush.is_some() || self.pending_push.is_some() {
return Err(CanonicalSegmentError::Corrupt(
"canonical input cannot change during retained record or flush recovery".into(),
));
}
if !self.admitted {
return self.segment.add_node_properties(node, work);
}
let owned_work = self.work.clone();
node_properties::add(self, node, work.unwrap_or(&owned_work))
}
pub(in crate::canonical) fn flush_with_work_context(
mut self,
file: &mut File,
artifact_digest: &mut IntegrityHasher,
offset: u64,
work: Option<&CheckpointWorkContext>,
) -> Result<super::descriptor::Descriptor, CanonicalSegmentError> {
let owned_work = self.work.clone();
if self.admitted {
self.flush_retained_with_work_context(
file,
artifact_digest,
offset,
work.unwrap_or(&owned_work),
)
} else {
self.segment
.flush_with_work_context(file, artifact_digest, offset, work)
.map(|descriptor| {
super::descriptor::Descriptor::new(
descriptor,
CheckpointAllocationOwner::default(),
)
})
}
}
pub(in crate::canonical) fn flush_retained_with_work_context(
&mut self,
file: &mut File,
artifact_digest: &mut IntegrityHasher,
offset: u64,
work: &CheckpointWorkContext,
) -> Result<super::descriptor::Descriptor, CanonicalSegmentError> {
use std::io::{Seek, SeekFrom};
if self.pending_push.is_some() || self.pending_properties.is_some() {
return Err(CanonicalSegmentError::Corrupt(
"canonical flush cannot publish an unfinished record".into(),
));
}
let covered_digest = artifact_digest.clone().finish();
if let Some((covered_offset, digest)) = self.pending_flush
&& (covered_offset != offset || digest != covered_digest)
{
return Err(CanonicalSegmentError::Corrupt(
"retained canonical flush source boundary changed".into(),
));
}
if self.pending_flush.is_some() && self.flush_progress.dirty_tail {
let boundary = offset
.checked_add(self.flush_progress.written_bytes)
.ok_or_else(|| {
CanonicalSegmentError::Corrupt(
"private flush recovery boundary overflow".into(),
)
})?;
let unit = work.start_unit()?;
{
let _wave = work.io_wave()?;
if file.metadata()?.len() < boundary {
return Err(CanonicalSegmentError::Corrupt(
"retained canonical flush lost its covered prefix".into(),
));
}
file.set_len(boundary)?;
file.seek(SeekFrom::Start(boundary))?;
self.flush_progress.dirty_tail = false;
}
unit.finish();
work.checkpoint()?;
}
self.pending_flush = Some((offset, covered_digest));
let result = checkpoint_flush::flush_admitted(
&self.segment,
file,
artifact_digest,
offset,
work,
&mut self.flush_progress,
);
if result.is_ok() {
self.pending_flush = None;
self.flush_progress = checkpoint_flush::FlushProgress::default();
}
result
}
}
mod node_properties;
mod record_push;
#[cfg(test)]
mod tests;