use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use rustev_contract::run::RunRecord;
use rustev_core::seams::EvidenceSink;
use tokio::sync::{mpsc, oneshot};
use crate::clock::Clock;
use crate::wait::{CatchUnwind, cut};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SinkPolicy {
FailDecision { timeout_ms: u64 },
Backpressure {
max_pending: usize,
enqueue_timeout_ms: u64,
delivery_timeout_ms: u64,
},
DropCounted {
max_pending: usize,
delivery_timeout_ms: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeliveryFailure {
pub detail: String,
pub uncertain: bool,
}
pub struct DeliveryTicket(pub(crate) oneshot::Receiver<Result<String, DeliveryFailure>>);
impl DeliveryTicket {
pub async fn wait(self) -> Result<String, DeliveryFailure> {
self.0.await.unwrap_or_else(|_| {
Err(DeliveryFailure {
detail: "the delivery worker stopped".into(),
uncertain: true,
})
})
}
}
impl std::fmt::Debug for DeliveryTicket {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("DeliveryTicket")
}
}
#[derive(Debug, Default)]
pub(crate) struct Stats {
pub acknowledged: AtomicU64,
pub failed: AtomicU64,
pub timed_out: AtomicU64,
pub dropped: AtomicU64,
pub queued: AtomicU64,
pub in_delivery: AtomicU64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SinkStats {
pub acknowledged: u64,
pub failed: u64,
pub timed_out: u64,
pub dropped: u64,
pub queued: u64,
pub in_delivery: u64,
}
impl Stats {
pub(crate) fn snapshot(&self) -> SinkStats {
SinkStats {
acknowledged: self.acknowledged.load(Ordering::SeqCst),
failed: self.failed.load(Ordering::SeqCst),
timed_out: self.timed_out.load(Ordering::SeqCst),
dropped: self.dropped.load(Ordering::SeqCst),
queued: self.queued.load(Ordering::SeqCst),
in_delivery: self.in_delivery.load(Ordering::SeqCst),
}
}
}
pub(crate) struct Job {
pub record: Arc<RunRecord>,
pub done: oneshot::Sender<Result<String, DeliveryFailure>>,
}
pub(crate) async fn deliver_once(
sink: &dyn EvidenceSink,
clock: &dyn Clock,
record: &RunRecord,
timeout_ms: u64,
stats: &Stats,
) -> Result<String, DeliveryFailure> {
let work = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink.deliver(record)))
{
Ok(f) => f,
Err(p) => {
stats.failed.fetch_add(1, Ordering::SeqCst);
return Err(DeliveryFailure {
detail: cut(
&format!(
"the sink panicked: {}",
crate::wait::panic_detail(p.as_ref())
),
256,
),
uncertain: true,
});
}
};
let mut work = CatchUnwind::new(work);
let mut timer = clock.sleep_until(clock.now().saturating_add(timeout_ms));
let never = rustev_core::seams::CancelSignal::new();
let r = match crate::wait::race(&mut work, &never, &mut timer).await {
crate::wait::Raced::Done(Ok(Ok(receipt))) => Ok(receipt),
crate::wait::Raced::Done(Ok(Err(e))) => Err(DeliveryFailure {
detail: cut(&e, 256),
uncertain: false,
}),
crate::wait::Raced::Done(Err(panic)) => Err(DeliveryFailure {
detail: cut(&format!("the sink panicked: {panic}"), 256),
uncertain: true,
}),
crate::wait::Raced::Expired | crate::wait::Raced::Cancelled => {
stats.timed_out.fetch_add(1, Ordering::SeqCst);
Err(DeliveryFailure {
detail: format!("no answer within {timeout_ms} ms"),
uncertain: true,
})
}
};
match &r {
Ok(_) => stats.acknowledged.fetch_add(1, Ordering::SeqCst),
Err(_) => stats.failed.fetch_add(1, Ordering::SeqCst),
};
r
}
pub(crate) async fn worker(
mut rx: mpsc::Receiver<Job>,
sink: Arc<dyn EvidenceSink>,
clock: Arc<dyn Clock>,
timeout_ms: u64,
stats: Arc<Stats>,
) {
while let Some(job) = rx.recv().await {
stats.queued.fetch_sub(1, Ordering::SeqCst);
stats.in_delivery.fetch_add(1, Ordering::SeqCst);
let r = deliver_once(&*sink, &*clock, &job.record, timeout_ms, &stats).await;
stats.in_delivery.fetch_sub(1, Ordering::SeqCst);
let _ = job.done.send(r);
}
}