use std::collections::VecDeque;
use crate::error::StoreError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum FailurePoint {
AfterInteractionPersistence,
BeforeJournalInsert,
AfterJournalInsertBeforeCommit,
AfterCommitBeforeEventReadback,
BeforeOutboxDispatch,
AfterOutboxDispatch,
BeforeResponsePersistence,
}
impl FailurePoint {
pub const ALL: [Self; 7] = [
Self::AfterInteractionPersistence,
Self::BeforeJournalInsert,
Self::AfterJournalInsertBeforeCommit,
Self::AfterCommitBeforeEventReadback,
Self::BeforeOutboxDispatch,
Self::AfterOutboxDispatch,
Self::BeforeResponsePersistence,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::AfterInteractionPersistence => "after_interaction_persistence",
Self::BeforeJournalInsert => "before_journal_insert",
Self::AfterJournalInsertBeforeCommit => "after_journal_insert_before_commit",
Self::AfterCommitBeforeEventReadback => "after_commit_before_event_readback",
Self::BeforeOutboxDispatch => "before_outbox_dispatch",
Self::AfterOutboxDispatch => "after_outbox_dispatch",
Self::BeforeResponsePersistence => "before_response_persistence",
}
}
pub fn parse(name: &str) -> Result<Self, UnknownFailurePoint> {
Self::ALL
.into_iter()
.find(|point| point.as_str() == name)
.ok_or_else(|| UnknownFailurePoint {
name: name.to_owned(),
})
}
#[must_use]
pub fn leaves_write_visible(self) -> bool {
matches!(
self,
Self::AfterInteractionPersistence
| Self::AfterJournalInsertBeforeCommit
| Self::AfterCommitBeforeEventReadback
)
}
}
impl core::fmt::Display for FailurePoint {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown crash boundary: {name}")]
pub struct UnknownFailurePoint {
pub name: String,
}
#[derive(Debug, Default)]
pub(super) struct FaultQueue {
armed: VecDeque<(FailurePoint, StoreError)>,
}
impl FaultQueue {
pub(super) fn arm(&mut self, point: FailurePoint, error: StoreError) {
self.armed.push_back((point, error));
}
pub(super) fn take(&mut self, point: FailurePoint) -> Option<StoreError> {
let index = self.armed.iter().position(|(armed, _)| *armed == point)?;
self.armed.remove(index).map(|(_, error)| error)
}
pub(super) fn armed_points(&self) -> Vec<FailurePoint> {
self.armed.iter().map(|(point, _)| *point).collect()
}
pub(super) fn clear(&mut self) {
self.armed.clear();
}
}
#[cfg(test)]
mod tests {
#[test]
fn every_boundary_has_a_stable_name_that_round_trips() {
let mut seen = std::collections::BTreeSet::new();
for point in FailurePoint::ALL {
let name = point.as_str();
assert!(seen.insert(name), "boundary names must be unique: {name}");
assert_eq!(FailurePoint::parse(name), Ok(point));
assert_eq!(point.to_string(), name);
}
assert!(FailurePoint::parse("nope").is_err());
}
use super::*;
#[test]
fn queue_fires_in_arming_order_per_point() {
let mut queue = FaultQueue::default();
queue.arm(FailurePoint::BeforeJournalInsert, StoreError::Unavailable);
queue.arm(FailurePoint::AfterOutboxDispatch, StoreError::Timeout);
queue.arm(FailurePoint::BeforeJournalInsert, StoreError::Timeout);
assert_eq!(
queue.armed_points(),
vec![
FailurePoint::BeforeJournalInsert,
FailurePoint::AfterOutboxDispatch,
FailurePoint::BeforeJournalInsert
]
);
assert_eq!(
queue.take(FailurePoint::BeforeJournalInsert),
Some(StoreError::Unavailable)
);
assert_eq!(
queue.take(FailurePoint::BeforeJournalInsert),
Some(StoreError::Timeout)
);
assert_eq!(queue.take(FailurePoint::BeforeJournalInsert), None);
assert_eq!(
queue.armed_points(),
vec![FailurePoint::AfterOutboxDispatch]
);
queue.clear();
assert!(queue.armed_points().is_empty());
}
#[test]
fn write_visibility_is_declared_per_point() {
for point in FailurePoint::ALL {
let expected = matches!(
point,
FailurePoint::AfterInteractionPersistence
| FailurePoint::AfterJournalInsertBeforeCommit
| FailurePoint::AfterCommitBeforeEventReadback
);
assert_eq!(point.leaves_write_visible(), expected, "{point:?}");
}
}
}