use std::future::Future;
use std::pin::pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use std::task::{Context, Poll, Wake, Waker};
use crate::entropy::SeededEntropy;
pub(crate) const CRASH_STREAM_SALT: u64 = 0xC7A5_4EAD_C7A5_4EAD;
pub(crate) const CRASH_AWAIT_BOUNDARIES: u64 = 4;
pub(crate) const DEFAULT_CRASH_SCHEDULE_LEN: usize = 8;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrashPoint {
pub seq: u64,
pub await_index: u64,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrashSchedule {
points: Vec<CrashPoint>,
}
impl CrashSchedule {
#[must_use]
pub(crate) fn derive(seed: u64, len: usize) -> Self {
let stream = SeededEntropy::shared(seed ^ CRASH_STREAM_SALT);
let points = (0..len as u64)
.map(|seq| CrashPoint {
seq,
await_index: stream.next_u64() % CRASH_AWAIT_BOUNDARIES,
})
.collect();
Self { points }
}
#[must_use]
pub fn points(&self) -> &[CrashPoint] {
&self.points
}
#[must_use]
pub fn first(&self) -> Option<&CrashPoint> {
self.points.first()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CrashOutcome<T> {
Completed(T),
Crashed {
await_index: u64,
},
}
impl<T> CrashOutcome<T> {
#[must_use]
pub const fn is_crashed(&self) -> bool {
matches!(self, Self::Crashed { .. })
}
#[must_use]
pub fn completed(self) -> Option<T> {
match self {
Self::Completed(value) => Some(value),
Self::Crashed { .. } => None,
}
}
}
pub async fn crash_at<F: Future>(await_index: u64, op: F) -> CrashOutcome<F::Output> {
let mut op = pin!(op);
let wake = Arc::new(WakeFlag::default());
let waker = Waker::from(Arc::clone(&wake));
let mut suspensions = 0_u64;
let mut polled = false;
std::future::poll_fn(move |cx| {
wake.set_parent(cx.waker());
if polled && !wake.take() {
return Poll::Pending;
}
polled = true;
match op.as_mut().poll(&mut Context::from_waker(&waker)) {
Poll::Ready(value) => Poll::Ready(CrashOutcome::Completed(value)),
Poll::Pending if suspensions == await_index => {
Poll::Ready(CrashOutcome::Crashed { await_index })
}
Poll::Pending => {
suspensions += 1;
Poll::Pending
}
}
})
.await
}
#[derive(Default)]
struct WakeFlag {
woken: AtomicBool,
parent: Mutex<Option<Waker>>,
}
impl WakeFlag {
fn set_parent(&self, waker: &Waker) {
let mut parent = self.parent.lock().unwrap_or_else(PoisonError::into_inner);
if !parent
.as_ref()
.is_some_and(|current| current.will_wake(waker))
{
*parent = Some(waker.clone());
}
}
fn take(&self) -> bool {
self.woken.swap(false, Ordering::AcqRel)
}
}
impl Wake for WakeFlag {
fn wake(self: Arc<Self>) {
self.wake_by_ref();
}
fn wake_by_ref(self: &Arc<Self>) {
self.woken.store(true, Ordering::Release);
let parent = self
.parent
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
if let Some(parent) = parent {
parent.wake();
}
}
}
#[cfg(test)]
mod tests {
use super::{CRASH_AWAIT_BOUNDARIES, CrashSchedule, DEFAULT_CRASH_SCHEDULE_LEN};
#[test]
fn schedule_is_seed_deterministic() {
let a = CrashSchedule::derive(42, DEFAULT_CRASH_SCHEDULE_LEN);
let b = CrashSchedule::derive(42, DEFAULT_CRASH_SCHEDULE_LEN);
assert_eq!(a, b, "same seed must replay an identical crash schedule");
}
#[test]
fn different_seeds_diverge() {
let a = CrashSchedule::derive(42, DEFAULT_CRASH_SCHEDULE_LEN);
let b = CrashSchedule::derive(43, DEFAULT_CRASH_SCHEDULE_LEN);
assert_ne!(
a, b,
"different seeds should (overwhelmingly likely) diverge"
);
}
#[test]
fn schedule_len_and_bounds_hold() {
let s = CrashSchedule::derive(7, DEFAULT_CRASH_SCHEDULE_LEN);
assert_eq!(s.points().len(), DEFAULT_CRASH_SCHEDULE_LEN);
for (i, point) in s.points().iter().enumerate() {
assert_eq!(point.seq, i as u64, "seq numbers are dense and ordered");
assert!(
point.await_index < CRASH_AWAIT_BOUNDARIES,
"await index stays in range"
);
}
assert_eq!(s.first(), s.points().first());
}
}