#![cfg_attr(docsrs, feature(doc_cfg))]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
use std::collections::HashSet;
use std::sync::atomic::{AtomicUsize, Ordering};
use dev_report::{CheckResult, Severity};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailureMode {
IoError,
PartialWrite,
ConnectionReset,
Timeout,
Corruption,
PermissionDenied,
}
impl FailureMode {
pub fn as_str(&self) -> &'static str {
match self {
FailureMode::IoError => "io_error",
FailureMode::PartialWrite => "partial_write",
FailureMode::ConnectionReset => "connection_reset",
FailureMode::Timeout => "timeout",
FailureMode::Corruption => "corruption",
FailureMode::PermissionDenied => "permission_denied",
}
}
}
#[derive(Debug, Clone)]
pub struct InjectedFailure {
pub mode: FailureMode,
pub attempt: usize,
}
impl std::fmt::Display for InjectedFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"injected failure {} at attempt {}",
self.mode.as_str(),
self.attempt
)
}
}
impl std::error::Error for InjectedFailure {}
pub struct FailureSchedule {
failing_attempts: HashSet<usize>,
mode: FailureMode,
invocations: AtomicUsize,
}
impl FailureSchedule {
pub fn on_attempts(attempts: &[usize], mode: FailureMode) -> Self {
Self {
failing_attempts: attempts.iter().copied().collect(),
mode,
invocations: AtomicUsize::new(0),
}
}
pub fn every_n(n: usize, mode: FailureMode) -> Self {
let mut s = HashSet::new();
for k in 1..=1024 {
if k % n == 0 {
s.insert(k);
}
}
Self {
failing_attempts: s,
mode,
invocations: AtomicUsize::new(0),
}
}
pub fn maybe_fail(&self, attempt: usize) -> Result<(), InjectedFailure> {
self.invocations.fetch_add(1, Ordering::Relaxed);
if self.failing_attempts.contains(&attempt) {
Err(InjectedFailure {
mode: self.mode,
attempt,
})
} else {
Ok(())
}
}
pub fn invocation_count(&self) -> usize {
self.invocations.load(Ordering::Relaxed)
}
}
pub fn assert_recovered(
name: impl Into<String>,
expected_failures: usize,
actual_failures: usize,
final_state_ok: bool,
) -> CheckResult {
let name = format!("chaos::{}", name.into());
if !final_state_ok {
return CheckResult::fail(name, Severity::Critical).with_detail(format!(
"system did not recover. expected {expected_failures} injected failures, observed {actual_failures}, final state failed validation"
));
}
if actual_failures < expected_failures {
return CheckResult::warn(name, Severity::Warning).with_detail(format!(
"fewer failures observed than scheduled (expected {expected_failures}, observed {actual_failures})"
));
}
CheckResult::pass(name).with_detail(format!(
"recovered after {actual_failures} injected failure(s)"
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schedule_fails_on_specified_attempts() {
let s = FailureSchedule::on_attempts(&[2, 4], FailureMode::IoError);
assert!(s.maybe_fail(1).is_ok());
assert!(s.maybe_fail(2).is_err());
assert!(s.maybe_fail(3).is_ok());
assert!(s.maybe_fail(4).is_err());
assert_eq!(s.invocation_count(), 4);
}
#[test]
fn every_n_pattern() {
let s = FailureSchedule::every_n(3, FailureMode::Timeout);
assert!(s.maybe_fail(1).is_ok());
assert!(s.maybe_fail(2).is_ok());
assert!(s.maybe_fail(3).is_err());
assert!(s.maybe_fail(6).is_err());
assert!(s.maybe_fail(9).is_err());
}
#[test]
fn recovery_check_pass() {
let c = assert_recovered("write_log", 2, 2, true);
assert!(matches!(c.verdict, dev_report::Verdict::Pass));
}
#[test]
fn recovery_check_fail_when_state_invalid() {
let c = assert_recovered("write_log", 2, 2, false);
assert!(matches!(c.verdict, dev_report::Verdict::Fail));
}
}