haematite 0.6.1

Content-addressed, branchable, actor-native storage engine
Documentation
use std::error::Error;
use std::fmt;
use std::sync::Arc;

use super::commit::{
    BranchCommitError, CommitDurability, CommitRequest, commit_branch, commit_branch_with_source,
};
use super::durable_record::{
    CreateExclusive, DurableRecordStore, EntryFence, entry_fence_compat, entry_fence_with_source,
};
use super::handle::DEFAULT_SHARD_ID;
use super::lifecycle::create_branch;
use super::registry::BranchRegistry;
use crate::store::{MemoryStore, NodeStore};
use crate::tree::mutate::batch_mutate_owned_with_source;
use crate::tree::{Cursor, Hash, LeafNode, Node, TreeError, batch_mutate_owned};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InjectedFailure {
    NodeGet,
    NodePut,
    NodePublicationFence,
    RecordEntryFence,
}

impl fmt::Display for InjectedFailure {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NodeGet => formatter.write_str("injected node get failure"),
            Self::NodePut => formatter.write_str("injected node put failure"),
            Self::NodePublicationFence => {
                formatter.write_str("injected node publication fence failure")
            }
            Self::RecordEntryFence => formatter.write_str("injected record entry fence failure"),
        }
    }
}

impl Error for InjectedFailure {}

#[derive(Debug, Clone, Copy)]
enum FailureMode {
    Get,
    Put,
    PublicationFence,
}

#[derive(Debug)]
struct FailingNodeStore {
    inner: MemoryStore,
    mode: FailureMode,
}

impl FailingNodeStore {
    const fn new(inner: MemoryStore, mode: FailureMode) -> Self {
        Self { inner, mode }
    }
}

impl NodeStore for FailingNodeStore {
    type Error = InjectedFailure;

    fn get(&self, hash: &Hash) -> Result<Option<Arc<Node>>, Self::Error> {
        if matches!(self.mode, FailureMode::Get) {
            Err(InjectedFailure::NodeGet)
        } else {
            Ok(self.inner.get(hash))
        }
    }

    fn put(&mut self, node: &Node) -> Result<Hash, Self::Error> {
        if matches!(self.mode, FailureMode::Put) {
            Err(InjectedFailure::NodePut)
        } else {
            Ok(self.inner.put(node))
        }
    }

    fn sync_dirty_dirs(&self) -> Result<(), Self::Error> {
        if matches!(self.mode, FailureMode::PublicationFence) {
            Err(InjectedFailure::NodePublicationFence)
        } else {
            Ok(())
        }
    }
}

fn source_downcast<'a, T: Error + 'static>(error: &'a (dyn Error + 'static)) -> Option<&'a T> {
    let mut source = error.source();
    while let Some(error) = source {
        if let Some(typed) = error.downcast_ref::<T>() {
            return Some(typed);
        }
        source = error.source();
    }
    None
}

fn empty_root(store: &mut MemoryStore) -> Result<Hash, crate::tree::NodeError> {
    let leaf = LeafNode::new(Vec::new())?;
    Ok(store.put(&Node::Leaf(leaf)))
}

#[test]
fn portable_core_preserves_node_get_error_source() -> Result<(), Box<dyn Error>> {
    let root = Hash::from_bytes([7; 32]);
    let store = FailingNodeStore::new(MemoryStore::new(), FailureMode::Get);

    let Err(core_error) = Cursor::new(&store, root).get_with_source(b"key") else {
        return Err("typed core accepted an injected node get failure".into());
    };
    assert_eq!(
        source_downcast::<InjectedFailure>(&core_error),
        Some(&InjectedFailure::NodeGet)
    );

    assert_eq!(
        Cursor::new(&store, root).get(b"key"),
        Err(TreeError::MissingNode { hash: root })
    );
    Ok(())
}

#[test]
fn portable_core_preserves_node_put_error_source() -> Result<(), Box<dyn Error>> {
    let mut memory = MemoryStore::new();
    let root = empty_root(&mut memory)?;
    let mut store = FailingNodeStore::new(memory, FailureMode::Put);
    let mutation = vec![(b"key".to_vec(), Some(b"value".to_vec()))];

    let Err(core_error) = batch_mutate_owned_with_source(&mut store, root, mutation.clone()) else {
        return Err("typed core accepted an injected node put failure".into());
    };
    assert_eq!(
        source_downcast::<InjectedFailure>(&core_error),
        Some(&InjectedFailure::NodePut)
    );

    assert_eq!(
        batch_mutate_owned(&mut store, root, mutation),
        Err(TreeError::InvalidNode)
    );
    Ok(())
}

#[test]
fn portable_core_preserves_node_publication_fence_error_source() -> Result<(), Box<dyn Error>> {
    let mut memory = MemoryStore::new();
    let root = empty_root(&mut memory)?;
    let directory = tempfile::tempdir()?;
    let registry = BranchRegistry::new();
    let mut refs = super::refstore::BranchRefStore::open(directory.path())?;
    let branch = create_branch(
        "typed-fence",
        [(DEFAULT_SHARD_ID, root)],
        &mut refs,
        &registry,
        10,
    )?;
    branch.put(DEFAULT_SHARD_ID, b"key", b"value")?;
    let mut store = FailingNodeStore::new(memory, FailureMode::PublicationFence);

    let core_result = commit_branch_with_source(
        &branch,
        &mut store,
        &registry,
        CommitRequest {
            durability: CommitDurability::Durable { refs: &mut refs },
            extra_parents: &[],
            timestamp: 20,
        },
    );
    let Err(core_error) = core_result else {
        return Err("typed core accepted an injected node publication failure".into());
    };
    assert_eq!(
        source_downcast::<InjectedFailure>(&core_error),
        Some(&InjectedFailure::NodePublicationFence)
    );

    let public_result = commit_branch(
        &branch,
        &mut store,
        &registry,
        CommitRequest {
            durability: CommitDurability::Durable { refs: &mut refs },
            extra_parents: &[],
            timestamp: 20,
        },
    );
    let Err(public_error) = public_result else {
        return Err("native wrapper accepted an injected node publication failure".into());
    };
    match &public_error {
        BranchCommitError::Barrier(message) => {
            assert_eq!(message, "injected node publication fence failure");
        }
        other => return Err(format!("expected native Barrier(String), got {other:?}").into()),
    }
    assert!(Error::source(&public_error).is_none());
    Ok(())
}

#[derive(Debug)]
struct FailingRecordStore;

impl DurableRecordStore for FailingRecordStore {
    type Error = InjectedFailure;

    fn list_read_at_open(&mut self) -> Result<Vec<super::BranchRefRecord>, Self::Error> {
        Ok(Vec::new())
    }

    fn create_exclusive(
        &mut self,
        _record: &super::BranchRefRecord,
    ) -> Result<CreateExclusive, Self::Error> {
        Ok(CreateExclusive::Installed)
    }

    fn cas_replace_install(
        &mut self,
        _name: &str,
        _expected_created: super::Timestamp,
        _expected_seq: u64,
        replacement: &super::BranchRefRecord,
    ) -> Result<super::BranchRefRecord, Self::Error> {
        Ok(replacement.clone())
    }

    fn unlink(&mut self, _name: &str) -> Result<bool, Self::Error> {
        Ok(true)
    }

    fn entry_fence(&mut self, _expected: EntryFence<'_>) -> Result<(), Self::Error> {
        Err(InjectedFailure::RecordEntryFence)
    }
}

#[test]
fn portable_core_preserves_record_entry_fence_error_source() -> Result<(), Box<dyn Error>> {
    let mut store = FailingRecordStore;
    let Err(core_error) = entry_fence_with_source(&mut store, EntryFence::Absent("typed-fence"))
    else {
        return Err("typed core accepted an injected record entry fence failure".into());
    };
    assert_eq!(
        source_downcast::<InjectedFailure>(&core_error),
        Some(&InjectedFailure::RecordEntryFence)
    );

    assert_eq!(
        entry_fence_compat(&mut store, EntryFence::Absent("typed-fence")),
        Err(InjectedFailure::RecordEntryFence)
    );
    Ok(())
}