use std::fmt::Debug;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use async_trait::async_trait;
use sha2::{Digest as _, Sha256};
use crate::core::{Digest, Epoch, RunId, Seq, StoreError};
use crate::journal::{Append, Head, JournalStore, Lease, Record};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Fault {
FailedClean,
CommittedThenLost,
Fenced,
}
#[derive(Debug, Clone, Default)]
pub struct Schedule {
seed: u64,
every: Option<(u64, Fault)>,
at: Vec<(u64, Fault)>,
on_kind: Vec<(&'static str, Fault)>,
}
impl Schedule {
#[must_use]
pub const fn healthy() -> Self {
Self {
seed: 0,
every: None,
at: Vec::new(),
on_kind: Vec::new(),
}
}
#[must_use]
pub const fn seeded(seed: u64) -> Self {
Self {
seed,
every: None,
at: Vec::new(),
on_kind: Vec::new(),
}
}
#[must_use]
pub fn at(mut self, n: u64, fault: Fault) -> Self {
self.at.push((n, fault));
self
}
#[must_use]
pub fn on_kind(mut self, kind: &'static str, fault: Fault) -> Self {
self.on_kind.push((kind, fault));
self
}
#[must_use]
pub const fn every(mut self, n: u64, fault: Fault) -> Self {
self.every = Some((n, fault));
self
}
fn decide(&self, n: u64, kinds: &[&str]) -> Option<Fault> {
for &(at, fault) in &self.at {
if at == n {
return Some(fault);
}
}
for &(kind, fault) in &self.on_kind {
if kinds.contains(&kind) {
return Some(fault);
}
}
let (period, fault) = self.every?;
let mut h = Sha256::new();
h.update(self.seed.to_be_bytes());
h.update(n.to_be_bytes());
let d = h.finalize();
let draw = u64::from_be_bytes(d[..8].try_into().unwrap_or([0; 8]));
(period != 0 && draw % period == 0).then_some(fault)
}
}
#[derive(Debug)]
pub struct Faulty {
inner: Arc<dyn JournalStore>,
schedule: Schedule,
calls: AtomicU64,
injected: Arc<std::sync::Mutex<Vec<(u64, Fault)>>>,
runs: Arc<std::sync::Mutex<Vec<RunId>>>,
}
impl Faulty {
#[must_use]
pub fn new(inner: Arc<dyn JournalStore>, schedule: Schedule) -> Self {
Self {
inner,
schedule,
calls: AtomicU64::new(0),
injected: Arc::new(std::sync::Mutex::new(Vec::new())),
runs: Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
#[must_use]
pub fn injected(&self) -> Vec<(u64, Fault)> {
self.injected
.lock()
.map_or_else(|e| e.into_inner().clone(), |g| g.clone())
}
#[must_use]
pub fn runs(&self) -> Vec<RunId> {
self.runs
.lock()
.map_or_else(|e| e.into_inner().clone(), |g| g.clone())
}
fn record(&self, n: u64, fault: Fault) {
if let Ok(mut g) = self.injected.lock() {
g.push((n, fault));
}
}
}
#[async_trait]
impl JournalStore for Faulty {
async fn append(&self, epoch: Epoch, batch: Vec<Append>) -> Result<Vec<Record>, StoreError> {
let n = self.calls.fetch_add(1, Ordering::SeqCst) + 1;
let kinds: Vec<&str> = batch.iter().map(|a| a.kind.kind_str()).collect();
if let (Some(a), Ok(mut seen)) = (batch.first(), self.runs.lock())
&& !seen.contains(&a.run)
{
seen.push(a.run);
}
match self.schedule.decide(n, &kinds) {
None => self.inner.append(epoch, batch).await,
Some(f @ Fault::FailedClean) => {
self.record(n, f);
Err(StoreError::Backend(
"injected: append failed, nothing written".into(),
))
}
Some(f @ Fault::CommittedThenLost) => {
self.inner.append(epoch, batch).await?;
self.record(n, f);
Err(StoreError::Backend(
"injected: connection lost after commit".into(),
))
}
Some(f @ Fault::Fenced) => {
self.record(n, f);
Err(StoreError::Fenced {
run: batch
.first()
.map_or_else(String::new, |a| a.run.to_string()),
held: epoch,
current: epoch + 1,
})
}
}
}
async fn read(&self, run: RunId, from: Seq) -> Result<Vec<Record>, StoreError> {
self.inner.read(run, from).await
}
async fn runs_by_outcome(
&self,
outcome: &str,
limit: usize,
) -> Result<Vec<crate::core::RunId>, StoreError> {
self.inner.runs_by_outcome(outcome, limit).await
}
async fn recent_runs(&self) -> Result<Vec<(RunId, u64)>, StoreError> {
self.inner.recent_runs().await
}
async fn case_history(
&self,
case: crate::core::CaseId,
limit: usize,
) -> Result<Vec<Record>, StoreError> {
self.inner.case_history(case, limit).await
}
async fn head(&self, run: RunId) -> Result<Head, StoreError> {
self.inner.head(run).await
}
async fn acquire(&self, run: RunId, owner: &str, ttl: Duration) -> Result<Lease, StoreError> {
self.inner.acquire(run, owner, ttl).await
}
async fn release_lease(&self, run: RunId, epoch: Epoch) -> Result<(), StoreError> {
self.inner.release_lease(run, epoch).await
}
async fn seal(&self, run: RunId, epoch: Epoch, outcome: &str) -> Result<Digest, StoreError> {
self.inner.seal(run, epoch, outcome).await
}
async fn checkpoint(&self) -> Result<crate::journal::Checkpoint, StoreError> {
self.inner.checkpoint().await
}
async fn consistency_proof(&self, old_size: u64) -> Result<Vec<Digest>, StoreError> {
self.inner.consistency_proof(old_size).await
}
async fn inclusion_proof(
&self,
run: RunId,
) -> Result<Option<crate::journal::Inclusion>, StoreError> {
self.inner.inclusion_proof(run).await
}
async fn request_cancel(
&self,
run: RunId,
actor: &str,
reason: &str,
) -> Result<bool, StoreError> {
self.inner.request_cancel(run, actor, reason).await
}
async fn cancellation(
&self,
run: RunId,
) -> Result<Option<crate::journal::Cancellation>, StoreError> {
self.inner.cancellation(run).await
}
}