use tsoracle_consensus::ConsensusError;
use tsoracle_core::Epoch;
#[derive(Debug)]
pub(crate) enum PersistDisposition {
SteppedDown { fenced_by: Option<Epoch> },
Transient(Box<dyn std::error::Error + Send + Sync>),
Permanent(Box<dyn std::error::Error + Send + Sync>),
OutOfRange(u64),
}
pub(crate) fn classify(error: ConsensusError) -> PersistDisposition {
match error {
ConsensusError::Fenced { current, .. } => PersistDisposition::SteppedDown {
fenced_by: Some(current),
},
ConsensusError::NotLeader { .. } => PersistDisposition::SteppedDown { fenced_by: None },
ConsensusError::TransientDriver(source) => PersistDisposition::Transient(source),
ConsensusError::PermanentDriver(source) => PersistDisposition::Permanent(source),
ConsensusError::AdvanceOutOfRange(at_least) => PersistDisposition::OutOfRange(at_least),
dense_error @ (ConsensusError::DenseUnsupported
| ConsensusError::SeqKeyCardinalityExceeded { .. }
| ConsensusError::SeqOverflow
| ConsensusError::DenseNotActivated { .. }) => {
PersistDisposition::Permanent(Box::new(dense_error))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fenced_carries_the_current_epoch_into_the_hint() {
let disposition = classify(ConsensusError::Fenced {
expected: Epoch(1),
current: Epoch(2),
});
assert!(matches!(
disposition,
PersistDisposition::SteppedDown {
fenced_by: Some(Epoch(2))
}
));
}
#[test]
fn not_leader_steps_down_without_an_epoch() {
let disposition = classify(ConsensusError::NotLeader {
observed: Some(Epoch(7)),
});
assert!(matches!(
disposition,
PersistDisposition::SteppedDown { fenced_by: None }
));
}
#[test]
fn transient_preserves_the_source_text() {
let disposition = classify(ConsensusError::TransientDriver(Box::new(
std::io::Error::other("flap"),
)));
match disposition {
PersistDisposition::Transient(source) => assert_eq!(source.to_string(), "flap"),
other => panic!("expected Transient, got a different disposition: {other:?}"),
}
}
#[test]
fn permanent_preserves_the_source_text() {
let disposition = classify(ConsensusError::PermanentDriver(Box::new(
std::io::Error::other("corrupted"),
)));
match disposition {
PersistDisposition::Permanent(source) => assert_eq!(source.to_string(), "corrupted"),
other => panic!("expected Permanent, got a different disposition: {other:?}"),
}
}
#[test]
fn advance_out_of_range_carries_the_offending_value_structurally() {
let disposition = classify(ConsensusError::AdvanceOutOfRange(
tsoracle_core::PHYSICAL_MS_MAX + 1,
));
match disposition {
PersistDisposition::OutOfRange(at_least) => {
assert_eq!(at_least, tsoracle_core::PHYSICAL_MS_MAX + 1);
}
other => panic!("expected OutOfRange, got a different disposition: {other:?}"),
}
}
#[test]
fn dense_variants_propagate_the_actual_error_as_permanent() {
let disposition = classify(ConsensusError::SeqKeyCardinalityExceeded { cap: 10_000 });
match disposition {
PersistDisposition::Permanent(source) => {
assert_eq!(
source.to_string(),
"dense key-cardinality cap 10000 reached"
);
assert!(source.downcast_ref::<ConsensusError>().is_some());
}
other => panic!("expected Permanent, got a different disposition: {other:?}"),
}
}
}