Skip to main content

monoloop_loop/transaction/
events.rs

1//! Runtime-owned event delivery task (ordered, backpressured).
2
3use super::executor_spawn::try_spawn;
4use super::finalization::EventSequencer;
5use monoloop_contracts::{
6    ChannelId, EventDeliveryError, SessionId, TransactionEvent, TransactionEventPayload,
7    TransactionEventSink, TransactionId,
8};
9use std::panic::{catch_unwind, AssertUnwindSafe};
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::Arc;
12use std::time::Duration;
13use tokio::runtime::Handle;
14use tokio::sync::{mpsc, Mutex};
15
16/// Queued event for delivery.
17pub struct QueuedEvent {
18    /// Event to deliver.
19    pub event: TransactionEvent,
20    /// Optional oneshot for terminal delivery ack.
21    pub ack: Option<tokio::sync::oneshot::Sender<Result<(), EventDeliveryError>>>,
22    /// Approximate serialized size for byte-queue accounting (D-015).
23    pub approx_bytes: usize,
24}
25
26impl QueuedEvent {
27    /// Build a queued event with a conservative byte estimate.
28    pub fn new(
29        event: TransactionEvent,
30        ack: Option<tokio::sync::oneshot::Sender<Result<(), EventDeliveryError>>>,
31    ) -> Self {
32        let approx_bytes = estimate_event_bytes(&event);
33        Self {
34            event,
35            ack,
36            approx_bytes,
37        }
38    }
39}
40
41fn estimate_event_bytes(event: &TransactionEvent) -> usize {
42    // Prefer exact JSON size when cheap; fall back to a floor for accounting.
43    serde_json::to_vec(event)
44        .map(|b| b.len().max(64))
45        .unwrap_or(256)
46}
47
48/// Bounded event sender: item capacity + byte budget (D-015).
49#[derive(Clone)]
50pub struct BoundedEventSender {
51    tx: mpsc::Sender<QueuedEvent>,
52    queued_bytes: Arc<AtomicUsize>,
53    max_bytes: usize,
54}
55
56impl BoundedEventSender {
57    /// Wrap an mpsc sender with a shared byte counter.
58    pub fn new(tx: mpsc::Sender<QueuedEvent>, max_bytes: usize) -> Self {
59        Self {
60            tx,
61            queued_bytes: Arc::new(AtomicUsize::new(0)),
62            max_bytes: max_bytes.max(1),
63        }
64    }
65
66    /// Try to enqueue; fails closed when item or byte budget is exceeded.
67    ///
68    /// Byte reservation is cancellation-safe (D-027): if the caller cancels while
69    /// awaiting item capacity, the Drop guard restores the reserved bytes so a
70    /// later terminal event can still be queued.
71    pub async fn send(&self, item: QueuedEvent) -> Result<(), EventQueueFull> {
72        let bytes = item.approx_bytes;
73        loop {
74            let cur = self.queued_bytes.load(Ordering::SeqCst);
75            if cur.saturating_add(bytes) > self.max_bytes {
76                return Err(EventQueueFull::Bytes);
77            }
78            if self
79                .queued_bytes
80                .compare_exchange(cur, cur + bytes, Ordering::SeqCst, Ordering::SeqCst)
81                .is_ok()
82            {
83                break;
84            }
85        }
86        // Holds the reservation until send completes or this future is dropped.
87        let mut reservation = ByteReservation {
88            counter: &self.queued_bytes,
89            bytes,
90            released: false,
91        };
92        match self.tx.send(item).await {
93            Ok(()) => {
94                reservation.released = true;
95                Ok(())
96            }
97            Err(_) => {
98                reservation.release();
99                Err(EventQueueFull::Closed)
100            }
101        }
102    }
103
104    /// Shared counter for the delivery task.
105    pub fn byte_counter(&self) -> Arc<AtomicUsize> {
106        Arc::clone(&self.queued_bytes)
107    }
108}
109
110/// Actor-owned publisher: sole ordinary allocator of public event sequences (D-036).
111///
112/// Child tasks must not call [`EventSequencer::allocate`] directly. All ordinary
113/// and terminal publishes go through this type so allocate+enqueue stay ordered.
114#[derive(Clone)]
115pub struct OrderedEventPublisher {
116    order: Arc<Mutex<()>>,
117    event_tx: BoundedEventSender,
118    sequencer: Arc<EventSequencer>,
119}
120
121impl OrderedEventPublisher {
122    /// Create a publisher bound to one transaction's sequencer and queue.
123    pub fn new(event_tx: BoundedEventSender, sequencer: Arc<EventSequencer>) -> Self {
124        Self {
125            order: Arc::new(Mutex::new(())),
126            event_tx,
127            sequencer,
128        }
129    }
130
131    /// Sequencer handle (finalization accounting only; do not allocate here).
132    pub fn sequencer(&self) -> &Arc<EventSequencer> {
133        &self.sequencer
134    }
135
136    /// Publish one ordinary event; returns its sequence number.
137    pub async fn publish(
138        &self,
139        transaction_id: TransactionId,
140        channel_id: ChannelId,
141        session_id: SessionId,
142        payload: TransactionEventPayload,
143    ) -> Result<u64, EventQueueFull> {
144        self.publish_inner(transaction_id, channel_id, session_id, payload, None)
145            .await
146    }
147
148    /// Publish terminal `Ended` with delivery ack.
149    pub async fn publish_terminal(
150        &self,
151        transaction_id: TransactionId,
152        channel_id: ChannelId,
153        session_id: SessionId,
154        payload: TransactionEventPayload,
155        ack: tokio::sync::oneshot::Sender<Result<(), EventDeliveryError>>,
156    ) -> Result<u64, EventQueueFull> {
157        self.publish_inner(transaction_id, channel_id, session_id, payload, Some(ack))
158            .await
159    }
160
161    async fn publish_inner(
162        &self,
163        transaction_id: TransactionId,
164        channel_id: ChannelId,
165        session_id: SessionId,
166        payload: TransactionEventPayload,
167        ack: Option<tokio::sync::oneshot::Sender<Result<(), EventDeliveryError>>>,
168    ) -> Result<u64, EventQueueFull> {
169        // Serialize producers so delivery order matches sequence (D-036).
170        let _guard = self.order.lock().await;
171        let seq = self.sequencer.peek_next();
172        let event = TransactionEvent {
173            transaction_id,
174            channel_id,
175            session_id,
176            sequence: seq,
177            payload,
178        };
179        self.event_tx.send(QueuedEvent::new(event, ack)).await?;
180        let got = self.sequencer.allocate();
181        debug_assert_eq!(got, seq);
182        Ok(seq)
183    }
184}
185
186/// Event queue rejection.
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188pub enum EventQueueFull {
189    /// Byte budget exceeded.
190    Bytes,
191    /// Receiver closed.
192    Closed,
193}
194
195/// RAII guard that restores reserved event-queue bytes on cancel/drop (D-027).
196struct ByteReservation<'a> {
197    counter: &'a AtomicUsize,
198    bytes: usize,
199    released: bool,
200}
201
202impl ByteReservation<'_> {
203    fn release(&mut self) {
204        if !self.released {
205            self.counter.fetch_sub(self.bytes, Ordering::SeqCst);
206            self.released = true;
207        }
208    }
209}
210
211impl Drop for ByteReservation<'_> {
212    fn drop(&mut self) {
213        self.release();
214    }
215}
216
217/// Spawn the sequential delivery task for one transaction on `executor` (D-032).
218pub fn spawn_delivery_task(
219    executor: &Handle,
220    mut rx: mpsc::Receiver<QueuedEvent>,
221    sink: Arc<dyn TransactionEventSink>,
222    on_fail: mpsc::Sender<()>,
223    byte_counter: Arc<AtomicUsize>,
224    deliver_deadline: Duration,
225) -> Result<tokio::task::JoinHandle<()>, ()> {
226    let executor_child = executor.clone();
227    try_spawn(executor, async move {
228        while let Some(item) = rx.recv().await {
229            let bytes = item.approx_bytes;
230            // D-021: host sink panics (invoke or poll) must not kill delivery.
231            let result =
232                deliver_isolated(&executor_child, &sink, item.event, deliver_deadline).await;
233            byte_counter.fetch_sub(
234                bytes.min(byte_counter.load(Ordering::SeqCst)),
235                Ordering::SeqCst,
236            );
237            let ok = result.is_ok();
238            if let Some(ack) = item.ack {
239                let _ = ack.send(if ok {
240                    Ok(())
241                } else {
242                    Err(EventDeliveryError::Failed)
243                });
244            }
245            if !ok {
246                let _ = on_fail.try_send(());
247                while let Some(rest) = rx.recv().await {
248                    byte_counter.fetch_sub(
249                        rest.approx_bytes.min(byte_counter.load(Ordering::SeqCst)),
250                        Ordering::SeqCst,
251                    );
252                    if let Some(ack) = rest.ack {
253                        let _ = ack.send(Err(EventDeliveryError::Failed));
254                    }
255                }
256                break;
257            }
258        }
259    })
260}
261
262/// Invoke sink.deliver and await its future with panic + deadline isolation (D-021).
263async fn deliver_isolated(
264    executor: &Handle,
265    sink: &Arc<dyn TransactionEventSink>,
266    event: TransactionEvent,
267    deadline: Duration,
268) -> Result<(), EventDeliveryError> {
269    let deliver_fut = catch_unwind(AssertUnwindSafe(|| sink.deliver(event)));
270    let fut = match deliver_fut {
271        Ok(f) => f,
272        Err(_) => return Err(EventDeliveryError::Failed),
273    };
274    // Owned child task: Future::poll panics become JoinError, not delivery-task death.
275    let handle = match try_spawn(executor, fut) {
276        Ok(h) => h,
277        Err(()) => return Err(EventDeliveryError::Failed),
278    };
279    let abort = handle.abort_handle();
280    match tokio::time::timeout(deadline, handle).await {
281        Ok(Ok(r)) => r,
282        Ok(Err(_)) => Err(EventDeliveryError::Failed),
283        Err(_) => {
284            abort.abort();
285            Err(EventDeliveryError::Failed)
286        }
287    }
288}