#![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())?;
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.store
.perf()
.time_request("prune", || self.prune_unreachable_records(&root_id))?;
self.store.ensure_commit_space(&self.records)?;
hooks.hit(CrashPoint::AfterRootWrite)?;
self.store.perf().time_request("append_flush", || {
self.store.append_records(&mut self.records)?;
self.store.flush_segment()
})?;
self.store.perf().time_request("superblock", || {
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 !work.is_empty() {
let level: Vec<(ChunkId, TreeKind)> = std::mem::take(&mut work);
let mut entries: Vec<(ChunkId, TreeKind, Vec<u8>)> = Vec::with_capacity(level.len());
let mut to_fetch: Vec<ChunkId> = Vec::new();
let mut fetch_idx: Vec<usize> = Vec::new();
for (id, kind) in level {
if id.is_zero() || !reachable.insert(id) {
continue;
}
match self.pending.get(&id) {
Some(bytes) => entries.push((id, kind, bytes.clone())),
None => {
fetch_idx.push(entries.len());
entries.push((id, kind, Vec::new()));
to_fetch.push(id);
}
}
}
let fetched = self.store.fetch_objects_many(&to_fetch);
for (i, r) in fetched.into_iter().enumerate() {
match r {
Ok(Some(bytes)) => {
let ei = fetch_idx[i];
entries[ei].2 = bytes;
}
Ok(None) => {}
Err(e) => return Err(e),
}
}
let mut inode_ids: Vec<ChunkId> = Vec::new();
let mut inode_slots: Vec<(usize, ChunkId)> = Vec::new();
let mut inode_pending: Vec<(usize, ChunkId, Vec<u8>)> = Vec::new();
for (i, (_id, kind, bytes)) in entries.iter_mut().enumerate() {
if *kind == TreeKind::Root {
if bytes.is_empty() {
continue;
}
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;
}
if bytes.is_empty() {
continue; }
let node = crate::store::index::Node::decode(
bytes,
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: node_entries,
} => {
work.push((first_child, *kind));
for e in node_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: leaf_entries,
} => {
for e in leaf_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) {
match self.pending.get(&inode_id) {
Some(b) => inode_pending.push((i, inode_id, b.clone())),
None => {
inode_slots.push((i, inode_id));
inode_ids.push(inode_id);
}
}
}
}
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 inode_fetched = self.store.fetch_objects_many(&inode_ids);
let mut inode_results: Vec<(usize, Vec<u8>)> = inode_pending
.into_iter()
.map(|(_ei, _id, bytes)| (_ei, bytes))
.collect();
for (i, r) in inode_fetched.into_iter().enumerate() {
match r {
Ok(Some(bytes)) => {
let (ei, _id) = inode_slots[i];
inode_results.push((ei, bytes));
}
Ok(None) => {}
Err(e) => return Err(e),
}
}
for (_ei, bytes) in inode_results {
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));
}
_ => {}
}
}
}
}
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) 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, .. }
| Representation::SequenceDeep { 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);
}
}