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,
®istry,
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,
®istry,
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,
®istry,
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(())
}