#![forbid(unsafe_code)]
use std::collections::{HashMap, HashSet};
use crate::core::extent::ChunkId;
use crate::core::representation::Representation;
use crate::format::version::RecordTag;
use crate::store::StoreError;
use crate::store::index::{BTreeError, ObjectProvider};
use crate::store::inode::{Inode, InodeData};
use crate::store::root::Root;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CrashPoint {
AfterRecordAppend,
AfterSegmentFdatasync,
AfterSegmentDirFsync,
AfterRootWrite,
AfterSuperblockWrite,
AfterSuperblockFsync,
BeforeOldSegmentDelete,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CrashHooks {
pub armed: Option<CrashPoint>,
}
impl CrashHooks {
pub fn none() -> Self {
Self { armed: None }
}
pub fn crash_at(point: CrashPoint) -> Self {
Self { armed: Some(point) }
}
pub fn hit(&self, point: CrashPoint) -> Result<(), StoreError> {
if self.armed == Some(point) {
Err(StoreError::CrashSimulated(format!("{point:?}")))
} else {
Ok(())
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct PendingRecord {
pub(crate) tag: RecordTag,
pub(crate) payload: Vec<u8>,
pub(crate) materialized_len: Option<u64>,
}
pub struct Tx<'a> {
pending: HashMap<ChunkId, Vec<u8>>,
records: Vec<PendingRecord>,
root: Root,
pub(crate) store: &'a crate::store::Store,
_commit_guard: std::sync::MutexGuard<'a, ()>,
}
impl<'a> Tx<'a> {
fn stage(
&mut self,
id: ChunkId,
bytes: Vec<u8>,
tag: RecordTag,
materialized_len: Option<u64>,
) {
if self.pending.contains_key(&id) || self.store.object_index().contains(&id) {
return;
}
self.pending.insert(id, bytes.clone());
self.records.push(PendingRecord {
tag,
payload: bytes,
materialized_len,
});
}
pub(crate) fn begin(
store: &'a crate::store::Store,
guard: std::sync::MutexGuard<'a, ()>,
) -> Self {
Self {
pending: HashMap::new(),
records: Vec::new(),
root: store.current_root(),
store,
_commit_guard: guard,
}
}
pub fn root(&self) -> &Root {
&self.root
}
pub fn root_mut(&mut self) -> &mut Root {
&mut self.root
}
pub fn resolve_descriptor(&self, cid: &ChunkId) -> Result<Option<Representation>, StoreError> {
let bytes = self.fetch_pending_or_store(cid)?;
match bytes {
Some(b) => {
let rep = crate::format::descriptor::decode(
&b,
self.store.limits().max_descriptor_bytes,
self.store.limits().max_inline_bytes,
self.store.limits().max_palette,
self.store.limits().max_period,
self.store.limits().max_chunk_size,
)?;
Ok(Some(rep))
}
None => Ok(None),
}
}
pub(crate) fn fetch_pending_or_store(
&self,
id: &ChunkId,
) -> Result<Option<Vec<u8>>, StoreError> {
if let Some(b) = self.pending.get(id) {
return Ok(Some(b.clone()));
}
self.store.fetch_object(id)
}
pub fn commit(self, hooks: &CrashHooks) -> Result<(), StoreError> {
let store = self.commit_deferred(hooks)?;
store.durability_barrier(hooks)?;
Ok(())
}
pub fn commit_deferred(
mut self,
hooks: &CrashHooks,
) -> Result<&'a crate::store::Store, StoreError> {
self.root.generation = self.store.generation() + 1;
self.root.segment_seq = self.store.current_segment_seq();
let root_bytes = self.root.encode();
let root_id = ChunkId::of(&root_bytes);
self.records.push(PendingRecord {
tag: RecordTag::Root,
payload: root_bytes,
materialized_len: None,
});
self.prune_unreachable_records(&root_id)?;
self.store.ensure_commit_space(&self.records)?;
hooks.hit(CrashPoint::AfterRootWrite)?;
self.store.append_records(&mut self.records)?;
self.store.flush_segment()?;
self.store.write_superblock(root_id, &self.root)?;
self.store.publish_commit(&self.root, root_id)?;
Ok(self.store)
}
fn prune_unreachable_records(&mut self, root_id: &ChunkId) -> Result<(), StoreError> {
let mut reachable: HashSet<ChunkId> = HashSet::new();
reachable.insert(*root_id);
let mut work: Vec<(ChunkId, TreeKind)> = Vec::new();
work.push((self.root.inode_index_root, TreeKind::InodeIndex));
work.push((self.root.chunk_index_root, TreeKind::ChunkIndex));
if !self.root.snapshot_tree_root.is_zero() {
work.push((self.root.snapshot_tree_root, TreeKind::Snapshot));
}
if !self.root.model_index_root.is_zero() {
work.push((self.root.model_index_root, TreeKind::ChunkIndex));
}
let limits = *self.store.limits();
while let Some((id, kind)) = work.pop() {
if id.is_zero() || !reachable.insert(id) {
continue;
}
if kind == TreeKind::Root {
if let Some(bytes) = self.fetch_pending_or_store(&id)? {
if let Ok(root) = Root::decode(&bytes) {
work.push((root.inode_index_root, TreeKind::InodeIndex));
work.push((root.chunk_index_root, TreeKind::ChunkIndex));
if !root.snapshot_tree_root.is_zero() {
work.push((root.snapshot_tree_root, TreeKind::Snapshot));
}
if !root.model_index_root.is_zero() {
work.push((root.model_index_root, TreeKind::ChunkIndex));
}
}
}
continue;
}
let Some(payload) = self.fetch_pending_or_store(&id)? else {
continue;
};
let node = crate::store::index::Node::decode(
&payload,
crate::store::BTREE_ORDER,
limits.max_fanout,
)
.map_err(|e| StoreError::Index(e.to_string()))?;
match node {
crate::store::index::Node::Internal {
first_child,
entries,
} => {
work.push((first_child, kind));
for e in entries {
let child =
ChunkId::new(e.value.as_slice().try_into().expect("32-byte child id"));
work.push((child, kind));
}
}
crate::store::index::Node::Leaf { entries } => {
for e in entries {
match kind {
TreeKind::InodeIndex => {
let inode_id =
ChunkId::new(e.value.as_slice().try_into().map_err(|_| {
StoreError::Invariant("inode value not 32 bytes".into())
})?);
if reachable.insert(inode_id) {
if let Some(bytes) = self.fetch_pending_or_store(&inode_id)? {
if let Ok(inode) = Inode::decode(&bytes) {
if !inode.xattr_root.is_zero() {
work.push((inode.xattr_root, TreeKind::Xattr));
}
match inode.data {
InodeData::Directory { dir_root }
if !dir_root.is_zero() =>
{
work.push((dir_root, TreeKind::Directory));
}
InodeData::File { extent_root }
if !extent_root.is_zero() =>
{
work.push((extent_root, TreeKind::Extent));
}
_ => {}
}
}
}
}
}
TreeKind::Extent | TreeKind::ChunkIndex => {
for oid in descriptor_object_ids(&e.value, &limits) {
reachable.insert(oid);
}
}
TreeKind::Directory | TreeKind::Xattr => {}
TreeKind::Snapshot => {
if let Ok(entry) =
crate::store::snapshot::SnapshotEntry::decode(&e.value)
{
work.push((entry.root_id, TreeKind::Root));
}
}
TreeKind::Root => {}
}
}
}
}
}
let mut kept: Vec<PendingRecord> = Vec::with_capacity(self.records.len());
for r in self.records.drain(..) {
let id = ChunkId::of(&r.payload);
if reachable.contains(&id) {
kept.push(r);
}
}
self.records = kept;
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TreeKind {
InodeIndex,
ChunkIndex,
Extent,
Directory,
Snapshot,
Xattr,
Root,
}
fn descriptor_object_ids(bytes: &[u8], limits: &crate::core::limits::Limits) -> Vec<ChunkId> {
let Ok(desc) = crate::format::descriptor::decode(
bytes,
limits.max_descriptor_bytes,
limits.max_inline_bytes,
limits.max_palette,
limits.max_period,
limits.max_chunk_size,
) else {
return Vec::new();
};
descriptor_objects(&desc, limits)
}
pub(crate) fn descriptor_objects(
desc: &Representation,
limits: &crate::core::limits::Limits,
) -> Vec<ChunkId> {
let _ = limits;
let mut out = Vec::new();
let residual_objs = |r: &crate::core::representation::Residual, out: &mut Vec<ChunkId>| {
use crate::core::representation::Residual;
match r {
Residual::RansCoded { enc_obj, model, .. }
| Residual::BaseSequence { enc_obj, model, .. } => {
out.push(*enc_obj);
out.push(*model);
}
_ => {}
}
};
match &desc {
Representation::Raw { obj, .. } => out.push(*obj),
Representation::Rans { model, enc_obj, .. }
| Representation::SequenceRans { model, enc_obj, .. }
| Representation::SparseBlock64 { model, enc_obj, .. }
| Representation::SequenceDict { model, enc_obj, .. }
| Representation::SequenceSharedDict { model, enc_obj, .. } => {
out.push(*model);
out.push(*enc_obj);
}
Representation::BaseResidual { base, residual, .. } => {
out.push(*base);
residual_objs(residual, &mut out);
}
Representation::EntropyRef { residual, .. } => {
residual_objs(residual, &mut out);
}
_ => {}
}
out
}
impl<'a> ObjectProvider for Tx<'a> {
fn get(&self, id: &ChunkId) -> Result<Option<Vec<u8>>, BTreeError> {
self.fetch_pending_or_store(id)
.map_err(|e| BTreeError::Provider(e.to_string()))
}
fn put(&mut self, id: ChunkId, bytes: Vec<u8>) {
self.stage(id, bytes, RecordTag::BtreeNode, None);
}
}
pub fn put_object(
tx: &mut Tx<'_>,
tag: RecordTag,
payload: Vec<u8>,
materialized_len: Option<u64>,
) -> ChunkId {
let id = ChunkId::of(&payload);
tx.stage(id, payload, tag, materialized_len);
id
}
pub fn put_inode(tx: &mut Tx<'_>, inode: &crate::store::inode::Inode) -> ChunkId {
put_object(tx, RecordTag::Inode, inode.encode(), None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crash_points_are_distinct() {
let points = [
CrashPoint::AfterRecordAppend,
CrashPoint::AfterSegmentFdatasync,
CrashPoint::AfterSegmentDirFsync,
CrashPoint::AfterRootWrite,
CrashPoint::AfterSuperblockWrite,
CrashPoint::AfterSuperblockFsync,
CrashPoint::BeforeOldSegmentDelete,
];
let mut seen = std::collections::HashSet::new();
for p in points {
assert!(seen.insert(p));
}
assert_eq!(points.len(), 7);
}
}