Skip to main content

rustev_runtime/
sink.rs

1//! Evidence delivery (spec 003, 3.9.4 to 3.9.7). One of three policies; the
2//! queued policies share a single worker task with a bounded queue, so at most
3//! `max_pending` records wait and one is in delivery. The runtime never
4//! retries a delivery.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicU64, Ordering};
8
9use rustev_contract::run::RunRecord;
10use rustev_core::seams::EvidenceSink;
11use tokio::sync::{mpsc, oneshot};
12
13use crate::clock::Clock;
14use crate::wait::{CatchUnwind, cut};
15
16/// How evidence reaches the sink, and what the caller receives.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum SinkPolicy {
19    /// Deliver inline; the decision fails unless the sink acknowledges within
20    /// `timeout_ms`.
21    FailDecision { timeout_ms: u64 },
22    /// Queue for the worker, waiting at most `enqueue_timeout_ms` for one of
23    /// `max_pending` slots; the decision fails if none frees.
24    Backpressure {
25        max_pending: usize,
26        enqueue_timeout_ms: u64,
27        delivery_timeout_ms: u64,
28    },
29    /// Queue if a slot is free; otherwise drop the record and count it.
30    DropCounted {
31        max_pending: usize,
32        delivery_timeout_ms: u64,
33    },
34}
35
36/// Why a delivery did not produce a receipt.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct DeliveryFailure {
39    pub detail: String,
40    /// The sink may have received the record (after a timeout).
41    pub uncertain: bool,
42}
43
44/// Resolves when the worker has delivered, or failed to deliver, a queued
45/// record.
46pub struct DeliveryTicket(pub(crate) oneshot::Receiver<Result<String, DeliveryFailure>>);
47
48impl DeliveryTicket {
49    pub async fn wait(self) -> Result<String, DeliveryFailure> {
50        self.0.await.unwrap_or_else(|_| {
51            Err(DeliveryFailure {
52                detail: "the delivery worker stopped".into(),
53                uncertain: true,
54            })
55        })
56    }
57}
58
59impl std::fmt::Debug for DeliveryTicket {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.write_str("DeliveryTicket")
62    }
63}
64
65/// In-memory counts; they reset with the process and are not durable loss
66/// accounting (spec 003, 3.9.7).
67#[derive(Debug, Default)]
68pub(crate) struct Stats {
69    pub acknowledged: AtomicU64,
70    pub failed: AtomicU64,
71    pub timed_out: AtomicU64,
72    pub dropped: AtomicU64,
73    pub queued: AtomicU64,
74    pub in_delivery: AtomicU64,
75}
76
77/// A snapshot of the delivery counters.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
79pub struct SinkStats {
80    pub acknowledged: u64,
81    pub failed: u64,
82    pub timed_out: u64,
83    pub dropped: u64,
84    /// Records waiting in the queue now.
85    pub queued: u64,
86    /// Records in delivery now (at most one).
87    pub in_delivery: u64,
88}
89
90impl Stats {
91    pub(crate) fn snapshot(&self) -> SinkStats {
92        SinkStats {
93            acknowledged: self.acknowledged.load(Ordering::SeqCst),
94            failed: self.failed.load(Ordering::SeqCst),
95            timed_out: self.timed_out.load(Ordering::SeqCst),
96            dropped: self.dropped.load(Ordering::SeqCst),
97            queued: self.queued.load(Ordering::SeqCst),
98            in_delivery: self.in_delivery.load(Ordering::SeqCst),
99        }
100    }
101}
102
103pub(crate) struct Job {
104    pub record: Arc<RunRecord>,
105    pub done: oneshot::Sender<Result<String, DeliveryFailure>>,
106}
107
108/// Deliver one record with a timeout, containing a panic in the sink.
109pub(crate) async fn deliver_once(
110    sink: &dyn EvidenceSink,
111    clock: &dyn Clock,
112    record: &RunRecord,
113    timeout_ms: u64,
114    stats: &Stats,
115) -> Result<String, DeliveryFailure> {
116    // Contain a panic while building the future as well as while polling it.
117    let work = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sink.deliver(record)))
118    {
119        Ok(f) => f,
120        Err(p) => {
121            stats.failed.fetch_add(1, Ordering::SeqCst);
122            return Err(DeliveryFailure {
123                detail: cut(
124                    &format!(
125                        "the sink panicked: {}",
126                        crate::wait::panic_detail(p.as_ref())
127                    ),
128                    256,
129                ),
130                uncertain: true,
131            });
132        }
133    };
134    let mut work = CatchUnwind::new(work);
135    let mut timer = clock.sleep_until(clock.now().saturating_add(timeout_ms));
136    let never = rustev_core::seams::CancelSignal::new();
137    let r = match crate::wait::race(&mut work, &never, &mut timer).await {
138        crate::wait::Raced::Done(Ok(Ok(receipt))) => Ok(receipt),
139        crate::wait::Raced::Done(Ok(Err(e))) => Err(DeliveryFailure {
140            detail: cut(&e, 256),
141            uncertain: false,
142        }),
143        crate::wait::Raced::Done(Err(panic)) => Err(DeliveryFailure {
144            detail: cut(&format!("the sink panicked: {panic}"), 256),
145            uncertain: true,
146        }),
147        crate::wait::Raced::Expired | crate::wait::Raced::Cancelled => {
148            stats.timed_out.fetch_add(1, Ordering::SeqCst);
149            Err(DeliveryFailure {
150                detail: format!("no answer within {timeout_ms} ms"),
151                uncertain: true,
152            })
153        }
154    };
155    match &r {
156        Ok(_) => stats.acknowledged.fetch_add(1, Ordering::SeqCst),
157        Err(_) => stats.failed.fetch_add(1, Ordering::SeqCst),
158    };
159    r
160}
161
162/// The single delivery worker. Ends when every sender is gone.
163pub(crate) async fn worker(
164    mut rx: mpsc::Receiver<Job>,
165    sink: Arc<dyn EvidenceSink>,
166    clock: Arc<dyn Clock>,
167    timeout_ms: u64,
168    stats: Arc<Stats>,
169) {
170    while let Some(job) = rx.recv().await {
171        stats.queued.fetch_sub(1, Ordering::SeqCst);
172        stats.in_delivery.fetch_add(1, Ordering::SeqCst);
173        let r = deliver_once(&*sink, &*clock, &job.record, timeout_ms, &stats).await;
174        stats.in_delivery.fetch_sub(1, Ordering::SeqCst);
175        let _ = job.done.send(r);
176    }
177}