use std::time::{Duration, Instant};
use crate::readiness::POLL_INTERVAL;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Selection<P> {
Current(P),
Committed,
NotCommitted,
}
#[derive(Clone, Copy, Debug)]
pub struct Budget {
pub commit: Duration,
pub proof: Duration,
}
pub trait Evidence {
type Proof;
type Error;
fn committed(&mut self) -> Result<bool, Self::Error>;
fn probe(&mut self, deadline: Instant) -> Result<Option<Self::Proof>, Self::Error>;
fn wait(&mut self, until: Instant) {
#[expect(
clippy::disallowed_methods,
reason = "Default evidence has no wake source; Signaled overrides this wait for channel events."
)]
std::thread::sleep(POLL_INTERVAL.min(until.saturating_duration_since(Instant::now())));
}
}
pub fn await_selection<E: Evidence>(
budget: Budget,
evidence: &mut E,
) -> Result<Selection<E::Proof>, E::Error> {
let start = Instant::now();
let mut watch = Watch::new(budget);
loop {
let committed = evidence.committed()?;
watch.record(start.elapsed(), committed);
let until = start + watch.until();
let proof = evidence.probe(until)?.filter(|_| Instant::now() < until);
let proven = proof.is_some();
match watch.decide(start.elapsed(), proven) {
Decision::Current => return Ok(Selection::Current(proof.expect("proven above"))),
Decision::Committed => return Ok(Selection::Committed),
Decision::NotCommitted => return Ok(Selection::NotCommitted),
Decision::Wait => evidence.wait(start + watch.until()),
}
}
}
#[cfg(feature = "async")]
pub trait AsyncEvidence {
type Proof;
type Error;
fn committed(&mut self) -> impl std::future::Future<Output = Result<bool, Self::Error>> + Send;
fn probe(
&mut self,
) -> impl std::future::Future<Output = Result<Option<Self::Proof>, Self::Error>> + Send;
#[expect(
clippy::disallowed_methods,
reason = "Default evidence has no wake source; Signaled overrides this wait for channel events."
)]
fn wait(
&mut self,
until: tokio::time::Instant,
) -> impl std::future::Future<Output = ()> + Send {
tokio::time::sleep_until((tokio::time::Instant::now() + POLL_INTERVAL).min(until))
}
}
#[cfg(feature = "async")]
pub async fn await_selection_async<E: AsyncEvidence>(
budget: Budget,
evidence: &mut E,
) -> Result<Selection<E::Proof>, E::Error> {
let start = tokio::time::Instant::now();
let mut watch = Watch::new(budget);
loop {
let committed = evidence.committed().await?;
watch.record(start.elapsed(), committed);
let until = start + watch.until();
let proof = match tokio::time::timeout_at(until, evidence.probe()).await {
Ok(proof) => proof?.filter(|_| tokio::time::Instant::now() < until),
Err(_) => None,
};
let proven = proof.is_some();
match watch.decide(start.elapsed(), proven) {
Decision::Current => return Ok(Selection::Current(proof.expect("proven above"))),
Decision::Committed => return Ok(Selection::Committed),
Decision::NotCommitted => return Ok(Selection::NotCommitted),
Decision::Wait => evidence.wait(start + watch.until()).await,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Decision {
Current,
Committed,
NotCommitted,
Wait,
}
struct Watch {
budget: Budget,
committed_at: Option<Duration>,
}
impl Watch {
fn new(budget: Budget) -> Self {
Self {
budget,
committed_at: None,
}
}
fn until(&self) -> Duration {
match self.committed_at {
Some(at) => at.saturating_add(self.budget.proof).min(self.budget.commit),
None => self.budget.commit,
}
}
fn record(&mut self, elapsed: Duration, committed: bool) {
if committed && self.committed_at.is_none() {
self.committed_at = Some(elapsed);
}
}
fn decide(&self, elapsed: Duration, proven: bool) -> Decision {
if proven {
return Decision::Current;
}
let expired = elapsed >= self.until();
match (self.committed_at, expired) {
(_, false) => Decision::Wait,
(Some(_), true) => Decision::Committed,
(None, true) => Decision::NotCommitted,
}
}
}
#[cfg(kani)]
mod proofs {
use super::*;
const ROUNDS: usize = 6;
fn small() -> Duration {
Duration::from_millis(u64::from(kani::any::<u8>() % 8))
}
#[kani::proof]
#[kani::unwind(7)]
fn an_observer_never_claims_what_its_evidence_does_not_show() {
let budget = Budget {
commit: small(),
proof: small(),
};
let mut watch = Watch::new(budget);
let mut elapsed = Duration::ZERO;
let mut seen_commit = false;
for _ in 0..ROUNDS {
elapsed += small();
let committed: bool = kani::any();
watch.record(elapsed, committed);
seen_commit |= committed;
if committed {
assert!(watch.until() <= elapsed + budget.proof);
}
elapsed += small();
let proven: bool = kani::any();
let decision = watch.decide(elapsed, proven);
assert_eq!(decision == Decision::Current, proven);
if decision == Decision::Committed {
assert!(seen_commit && !proven);
}
if decision == Decision::NotCommitted {
assert!(!seen_commit && elapsed >= budget.commit);
}
if elapsed >= budget.commit {
assert!(decision != Decision::Wait);
}
if let Some(at) = watch.committed_at {
if elapsed >= at + budget.proof {
assert!(decision != Decision::Wait);
}
}
if decision != Decision::Wait {
return;
}
}
}
}
#[cfg(test)]
mod properties {
use super::*;
use proptest::prelude::*;
fn budget() -> impl Strategy<Value = Duration> {
prop_oneof![
Just(Duration::ZERO),
(0u64..10_000_000).prop_map(Duration::from_micros)
]
}
fn step() -> impl Strategy<Value = Duration> {
prop_oneof![
4 => (0u64..200_000).prop_map(Duration::from_micros),
1 => (0u64..5_000_000).prop_map(Duration::from_micros),
]
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 512,
failure_persistence: None,
..ProptestConfig::default()
})]
#[test]
fn an_observer_never_claims_what_its_evidence_does_not_show(
commit in budget(),
proof in budget(),
rounds in proptest::collection::vec(
(step(), any::<bool>(), step(), any::<bool>()),
1..128,
),
) {
let budget = Budget { commit, proof };
let mut watch = Watch::new(budget);
let mut elapsed = Duration::ZERO;
let mut seen_commit = false;
for (before_read, committed, probing, proven) in rounds {
elapsed += before_read;
watch.record(elapsed, committed);
seen_commit |= committed;
if committed {
prop_assert!(watch.until() <= elapsed + budget.proof);
}
elapsed += probing;
let decision = watch.decide(elapsed, proven);
prop_assert_eq!(decision == Decision::Current, proven);
if decision == Decision::Committed {
prop_assert!(seen_commit && !proven);
}
if decision == Decision::NotCommitted {
prop_assert!(!seen_commit && elapsed >= budget.commit);
}
if elapsed >= budget.commit {
prop_assert_ne!(decision, Decision::Wait);
}
if let Some(at) = watch.committed_at
&& elapsed >= at + budget.proof
{
prop_assert_ne!(decision, Decision::Wait);
}
if decision != Decision::Wait {
break;
}
}
}
}
}