Skip to main content

evm_fork_cache/reactive/
ingress_timing.rs

1//! Optional observational clocks supplied by an application-owned transport.
2use alloy_primitives::B256;
3use std::{sync::Arc, time::Instant};
4
5/// One complete message observed before JSON-RPC decoding. Identity must name
6/// the concrete receipt, not merely the payload or block. No execution authority.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct TransportReceipt {
9    identity: B256,
10    received: Instant,
11    decoded: Instant,
12}
13impl TransportReceipt {
14    /// Reject inconsistent clocks instead of fabricating zero elapsed time.
15    pub fn new(identity: B256, received: Instant, decoded: Instant) -> Option<Self> {
16        (received <= decoded).then_some(Self {
17            identity,
18            received,
19            decoded,
20        })
21    }
22    /// Application-defined receipt identity, never a metric label.
23    pub const fn identity(self) -> B256 {
24        self.identity
25    }
26    /// Complete-message receive time, before envelope decoding.
27    pub const fn received(self) -> Instant {
28        self.received
29    }
30    /// JSON-RPC envelope decoded time, before pubsub delivery.
31    pub const fn decoded(self) -> Instant {
32        self.decoded
33    }
34}
35
36/// Lookup for the exact raw payload consumed by a pubsub receiver. Implementations
37/// must be bounded, nonblocking and observational. Missing, evicted or ambiguous
38/// duplicate matches must return None; never pick an arbitrary earlier receipt.
39/// The callback adds no transport read and must not mutate execution state.
40///
41/// A backend-neutral, one-shot in-memory observer. A real transport must maintain
42/// bounded, unambiguous associations; this fixture accepts one known delivery only.
43/// No exporter, metric labels, payload logging, or async runtime is required.
44///
45/// ```
46/// use evm_fork_cache::reactive::{TransportReceipt, TransportReceiptLookup};
47/// use alloy_primitives::B256;
48/// use std::{sync::{Arc, Mutex}, time::Instant};
49///
50/// let received = Instant::now();
51/// let receipt = TransportReceipt::new(B256::repeat_byte(1), received, Instant::now()).unwrap();
52/// let slot = Mutex::new(Some(receipt));
53/// let lookup: TransportReceiptLookup = Arc::new(move |raw| {
54///     if raw != r#"{"fixture":true}"# { return None; }
55///     slot.try_lock().ok()?.take()
56/// });
57/// let observed = lookup(r#"{"fixture":true}"#).unwrap();
58/// tracing::debug!(receipt_id = %observed.identity(),
59///     decode_ns = observed.decoded().duration_since(observed.received()).as_nanos(),
60///     "transport receipt observed");
61/// assert!(lookup(r#"{"fixture":true}"#).is_none());
62/// ```
63pub type TransportReceiptLookup = Arc<dyn Fn(&str) -> Option<TransportReceipt> + Send + Sync>;
64
65/// Timing preserved with a canonical subscriber input through its bounded queue.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct CanonicalIngressTiming {
68    receipt: TransportReceipt,
69    dequeued: Instant,
70    decoded: Instant,
71}
72impl CanonicalIngressTiming {
73    /// Bind source receipt to the actual dequeue and typed payload decode.
74    pub fn new(receipt: TransportReceipt, dequeued: Instant, decoded: Instant) -> Option<Self> {
75        (receipt.decoded <= dequeued && dequeued <= decoded).then_some(Self {
76            receipt,
77            dequeued,
78            decoded,
79        })
80    }
81    /// Concrete transport receipt.
82    pub const fn receipt(self) -> TransportReceipt {
83        self.receipt
84    }
85    /// Raw subscription notification dequeue time.
86    pub const fn dequeued(self) -> Instant {
87        self.dequeued
88    }
89    /// Typed domain payload decode completion time.
90    pub const fn decoded(self) -> Instant {
91        self.decoded
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use std::time::Duration;
99    #[test]
100    fn queue_age_is_retained_and_invalid_clocks_remain_missing() {
101        let received = Instant::now();
102        let envelope = received + Duration::from_micros(5);
103        let dequeue = envelope + Duration::from_millis(30);
104        let decode = dequeue + Duration::from_micros(10);
105        let receipt = TransportReceipt::new(B256::repeat_byte(1), received, envelope).unwrap();
106        let timing = CanonicalIngressTiming::new(receipt, dequeue, decode).unwrap();
107        assert_eq!(timing.receipt().received(), received);
108        assert_eq!(
109            timing.dequeued().duration_since(receipt.decoded()),
110            Duration::from_millis(30)
111        );
112        assert!(TransportReceipt::new(B256::ZERO, envelope, received).is_none());
113        assert!(CanonicalIngressTiming::new(receipt, received, decode).is_none());
114    }
115}