monoloop-loop 0.1.2

Minimal extensible Loop: lossless canonical subscription, empty-capable tools
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
//! Per-transaction event publisher (v2 §12 / §22.6 / D-047).
//!
//! Sole allocator of ordinary and terminal event sequences for one transaction.
//! Waits for caller mailbox capacity under the transaction deadline — never
//! silently drops ordinary events after the coordinator has handed them off.
//!
//! Terminal Seal uses a **dedicated** channel so a full ordinary-command queue
//! cannot discard Seal. Seal is an **ordering fence**:
//! 1. close ordinary admission ([`OrdinaryCmdAdmit::close`]);
//! 2. finish in-flight ordinary under the Seal deadline (commit or sticky-fail);
//! 3. asynchronously `recv` the ordinary channel to exhaustion (Disconnected);
//! 4. then attempt `Ended`.

use super::session_identity::ensure_session;
use crate::transaction::sticky_cancel::StickyCancel;
use monoloop_contracts::{
    ChannelId, EventEnqueueError, ExternalSessionId, SessionId, TerminalEventDelivery,
    TransactionEndEvent, TransactionEvent, TransactionEventPayload, TransactionEventSender,
    TransactionId,
};
use std::sync::{Arc, Mutex};
use std::time::Instant as StdInstant;
use tokio::sync::{mpsc, oneshot};
use tokio::time::{sleep_until, Instant as TokioInstant};

/// Ordinary / establish commands from the coordinator (not Seal).
pub enum EventPublisherCommand {
    /// Establish an external session before ordinary events (§22.6).
    EstablishExternal(ExternalSessionId),
    /// Publish one ordinary payload (sequence allocated after capacity reserved).
    Publish(Box<TransactionEventPayload>),
}

/// Closeable ordinary-command admission into the event publisher.
///
/// Cloneable handle. [`Self::close`] drops the gate's Sender so no *new*
/// `send` can start. In-flight `send` futures that already cloned a Sender may
/// still complete; the publisher then drains them with `recv` until
/// Disconnected after Seal.
#[derive(Clone, Debug)]
pub struct OrdinaryCmdAdmit {
    tx: Arc<Mutex<Option<mpsc::Sender<EventPublisherCommand>>>>,
}

impl OrdinaryCmdAdmit {
    /// Create an admit gate and the publisher's receiver.
    pub fn channel(capacity: usize) -> (Self, mpsc::Receiver<EventPublisherCommand>) {
        let (tx, rx) = mpsc::channel(capacity.max(1));
        (
            Self {
                tx: Arc::new(Mutex::new(Some(tx))),
            },
            rx,
        )
    }

    /// Linearization point for Seal: refuse new ordinary admissions.
    pub fn close(&self) {
        let _ = self.tx.lock().unwrap_or_else(|e| e.into_inner()).take();
    }

    /// Whether admission is still open.
    pub fn is_open(&self) -> bool {
        self.tx.lock().unwrap_or_else(|e| e.into_inner()).is_some()
    }

    /// Enqueue an ordinary command; fails if closed or the publisher dropped.
    pub async fn send(&self, cmd: EventPublisherCommand) -> Result<(), ()> {
        let tx = self.tx.lock().unwrap_or_else(|e| e.into_inner()).clone();
        let Some(tx) = tx else {
            return Err(());
        };
        tx.send(cmd).await.map_err(|_| ())
    }

    /// Non-blocking enqueue (tests).
    pub fn try_send(
        &self,
        cmd: EventPublisherCommand,
    ) -> Result<(), mpsc::error::TrySendError<EventPublisherCommand>> {
        let tx = self.tx.lock().unwrap_or_else(|e| e.into_inner()).clone();
        let Some(tx) = tx else {
            return Err(mpsc::error::TrySendError::Closed(cmd));
        };
        tx.try_send(cmd)
    }

    /// Test hook: clone the live Sender (proving pre-close admission), signal
    /// `holding`, then await capacity. Callers MUST await `holding` before Seal
    /// so the send is known to have crossed the admission linearization point
    /// while the ordinary queue is still full.
    #[cfg(test)]
    pub async fn send_after_pre_fence_hold(
        &self,
        cmd: EventPublisherCommand,
        holding: oneshot::Sender<()>,
    ) -> Result<(), ()> {
        let tx = self.tx.lock().unwrap_or_else(|e| e.into_inner()).clone();
        let Some(tx) = tx else {
            return Err(());
        };
        let _ = holding.send(());
        tx.send(cmd).await.map_err(|_| ())
    }
}

/// Terminal Seal on the dedicated priority channel (D-047).
pub struct SealCommand {
    /// Terminal event body (emitted_events filled by publisher).
    pub terminal: TransactionEndEvent,
    /// Reply with publication result + last committed sequence.
    pub reply: oneshot::Sender<TerminalPublicationResult>,
    /// Authoritative absolute deadline for terminal `Ended` enqueue
    /// (`terminal_event_delivery_deadline`). Shared with the Finalizer wait so
    /// completion cannot publish before a later successful terminal delivery.
    pub deadline: StdInstant,
}

/// Result of Seal.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TerminalPublicationResult {
    /// How the terminal event was (or was not) delivered.
    pub delivery: TerminalEventDelivery,
    /// Last committed sequence (includes terminal if published).
    pub last_sequence: u64,
}

/// Run the publisher until Seal completes (or both channels close).
///
/// Seal arrives on a dedicated channel so a full ordinary-command queue cannot
/// discard it. Arrival establishes a fence: close [`OrdinaryCmdAdmit`], finish
/// in-flight ordinary under the Seal deadline, `recv` to Disconnected, then
/// `Ended`. Failure to deliver the backlog sticky-fails terminal delivery.
#[allow(clippy::too_many_arguments)] // publisher state machine ports
pub async fn run_event_publisher(
    transaction_id: TransactionId,
    channel_id: ChannelId,
    session_id: Option<SessionId>,
    event_tx: TransactionEventSender,
    mut cmd_rx: mpsc::Receiver<EventPublisherCommand>,
    admit: OrdinaryCmdAdmit,
    mut seal_rx: mpsc::Receiver<SealCommand>,
    _cancel: Arc<StickyCancel>,
    deadline: StdInstant,
) -> TerminalPublicationResult {
    let mut next_seq: u64 = 1;
    let mut last_committed: u64 = 0;
    let mut session = session_id;
    let mut session_established = false;
    let mut sticky_fail: Option<TerminalEventDelivery> = None;
    let mut cmd_closed = false;
    let mut seal_closed = false;

    loop {
        if let Some(fail) = sticky_fail {
            return match seal_rx.recv().await {
                Some(cmd) => {
                    admit.close();
                    let _ = drain_to_disconnect(&mut cmd_rx, cmd.deadline).await;
                    reply_sticky(cmd, fail, last_committed)
                }
                None => {
                    admit.close();
                    TerminalPublicationResult {
                        delivery: fail,
                        last_sequence: last_committed,
                    }
                }
            };
        }

        tokio::select! {
            biased;
            seal = seal_rx.recv(), if !seal_closed => {
                match seal {
                    Some(cmd) => {
                        admit.close();
                        return fence_drain_and_seal(
                            cmd,
                            transaction_id,
                            &channel_id,
                            &event_tx,
                            &mut cmd_rx,
                            &mut session,
                            &mut next_seq,
                            &mut last_committed,
                            &mut session_established,
                            None,
                        )
                        .await;
                    }
                    None => {
                        seal_closed = true;
                        // Drop gate Sender so cmd_rx can disconnect once external
                        // admits are gone (publisher itself holds an Admit clone).
                        admit.close();
                        if cmd_closed {
                            return TerminalPublicationResult {
                                delivery: TerminalEventDelivery::QueueClosed,
                                last_sequence: last_committed,
                            };
                        }
                    }
                }
            }
            cmd = cmd_rx.recv(), if !cmd_closed => {
                match cmd {
                    Some(EventPublisherCommand::EstablishExternal(external)) => {
                        match establish_external(
                            &event_tx,
                            transaction_id,
                            &channel_id,
                            external,
                            &mut session,
                            &mut next_seq,
                            &mut last_committed,
                            &mut session_established,
                            deadline,
                            &mut seal_rx,
                        )
                        .await
                        {
                            Ok(()) => {}
                            Err(WaitEnd::Sealed { cmd, ordinary }) => {
                                admit.close();
                                let fail =
                                    apply_ordinary_outcome(ordinary, &mut next_seq, &mut last_committed);
                                return fence_drain_and_seal(
                                    cmd,
                                    transaction_id,
                                    &channel_id,
                                    &event_tx,
                                    &mut cmd_rx,
                                    &mut session,
                                    &mut next_seq,
                                    &mut last_committed,
                                    &mut session_established,
                                    fail,
                                )
                                .await;
                            }
                            Err(WaitEnd::Failed(fail)) => sticky_fail = Some(fail),
                        }
                    }
                    Some(EventPublisherCommand::Publish(payload)) => {
                        let payload = *payload;
                        let sid = ensure_session(&mut session, None, transaction_id);
                        let seq = next_seq;
                        let event = TransactionEvent {
                            transaction_id,
                            channel_id: channel_id.clone(),
                            session_id: sid,
                            sequence: seq,
                            payload,
                        };
                        match wait_send_or_seal(&event_tx, event, deadline, &mut seal_rx).await {
                            Ok(()) => {
                                last_committed = seq;
                                next_seq = seq.saturating_add(1);
                            }
                            Err(WaitEnd::Sealed { cmd, ordinary }) => {
                                admit.close();
                                let fail =
                                    apply_ordinary_outcome(ordinary, &mut next_seq, &mut last_committed);
                                return fence_drain_and_seal(
                                    cmd,
                                    transaction_id,
                                    &channel_id,
                                    &event_tx,
                                    &mut cmd_rx,
                                    &mut session,
                                    &mut next_seq,
                                    &mut last_committed,
                                    &mut session_established,
                                    fail,
                                )
                                .await;
                            }
                            Err(WaitEnd::Failed(fail)) => sticky_fail = Some(fail),
                        }
                    }
                    None => {
                        cmd_closed = true;
                        if seal_closed {
                            return TerminalPublicationResult {
                                delivery: TerminalEventDelivery::QueueClosed,
                                last_sequence: last_committed,
                            };
                        }
                    }
                }
            }
        }
    }
}

/// Outcome of the ordinary event that was in flight when Seal arrived.
enum OrdinarySealOutcome {
    /// Already committed to the host mailbox at `seq`.
    Committed { seq: u64 },
    /// Could not deliver under the Seal deadline / send error.
    Failed(TerminalEventDelivery),
}

enum WaitEnd {
    Sealed {
        cmd: SealCommand,
        ordinary: OrdinarySealOutcome,
    },
    Failed(TerminalEventDelivery),
}

fn apply_ordinary_outcome(
    ordinary: OrdinarySealOutcome,
    next_seq: &mut u64,
    last_committed: &mut u64,
) -> Option<TerminalEventDelivery> {
    match ordinary {
        OrdinarySealOutcome::Committed { seq } => {
            *last_committed = seq;
            *next_seq = seq.saturating_add(1);
            None
        }
        OrdinarySealOutcome::Failed(f) => Some(f),
    }
}

fn reply_sticky(
    cmd: SealCommand,
    fail: TerminalEventDelivery,
    last_committed: u64,
) -> TerminalPublicationResult {
    let result = TerminalPublicationResult {
        delivery: fail,
        last_sequence: last_committed,
    };
    let _ = cmd.reply.send(result.clone());
    result
}

/// Discard remaining ordinary commands after sticky failure (admission already closed).
async fn drain_to_disconnect(
    cmd_rx: &mut mpsc::Receiver<EventPublisherCommand>,
    deadline: StdInstant,
) -> Result<(), TerminalEventDelivery> {
    let tokio_deadline = tokio_deadline_from(deadline);
    loop {
        tokio::select! {
            biased;
            _ = sleep_until(tokio_deadline) => {
                return Err(TerminalEventDelivery::DeadlineExceeded);
            }
            cmd = cmd_rx.recv() => {
                if cmd.is_none() {
                    return Ok(());
                }
                // Discard — sticky fail already decided.
            }
        }
    }
}

/// After admission is closed: publish every remaining ordinary command (including
/// those that complete from pre-fence parked `send`s) under the Seal deadline,
/// then publish `Ended`.
#[allow(clippy::too_many_arguments)]
async fn fence_drain_and_seal(
    cmd: SealCommand,
    transaction_id: TransactionId,
    channel_id: &ChannelId,
    event_tx: &TransactionEventSender,
    cmd_rx: &mut mpsc::Receiver<EventPublisherCommand>,
    session: &mut Option<SessionId>,
    next_seq: &mut u64,
    last_committed: &mut u64,
    session_established: &mut bool,
    mut sticky_fail: Option<TerminalEventDelivery>,
) -> TerminalPublicationResult {
    let seal_deadline = cmd.deadline;
    let tokio_deadline = tokio_deadline_from(seal_deadline);

    // Async drain to Disconnected — not try_recv-until-Empty (parked send race).
    while sticky_fail.is_none() {
        let next = tokio::select! {
            biased;
            _ = sleep_until(tokio_deadline) => {
                sticky_fail = Some(TerminalEventDelivery::DeadlineExceeded);
                break;
            }
            cmd = cmd_rx.recv() => cmd,
        };
        match next {
            None => break,
            Some(EventPublisherCommand::EstablishExternal(external)) => {
                if *session_established || *next_seq != 1 {
                    continue;
                }
                let sid = SessionId::from_external(&external);
                let seq = *next_seq;
                let event = TransactionEvent {
                    transaction_id,
                    channel_id: channel_id.clone(),
                    session_id: sid.clone(),
                    sequence: seq,
                    payload: TransactionEventPayload::SessionEstablished {
                        external_session_id: external,
                    },
                };
                match enqueue_under_deadline(event_tx, event, seal_deadline).await {
                    Ok(()) => {
                        let _ = ensure_session(session, Some(sid), transaction_id);
                        *last_committed = seq;
                        *next_seq = seq.saturating_add(1);
                        *session_established = true;
                    }
                    Err(fail) => sticky_fail = Some(fail),
                }
            }
            Some(EventPublisherCommand::Publish(payload)) => {
                let sid = ensure_session(session, None, transaction_id);
                let seq = *next_seq;
                let event = TransactionEvent {
                    transaction_id,
                    channel_id: channel_id.clone(),
                    session_id: sid,
                    sequence: seq,
                    payload: *payload,
                };
                match enqueue_under_deadline(event_tx, event, seal_deadline).await {
                    Ok(()) => {
                        *last_committed = seq;
                        *next_seq = seq.saturating_add(1);
                    }
                    Err(fail) => sticky_fail = Some(fail),
                }
            }
        }
    }
    if sticky_fail.is_some() {
        let _ = drain_to_disconnect(cmd_rx, seal_deadline).await;
    }

    finish_seal(
        cmd,
        transaction_id,
        channel_id,
        event_tx,
        session,
        next_seq,
        last_committed,
        sticky_fail,
    )
    .await
}

#[allow(clippy::too_many_arguments)] // publisher locals
async fn finish_seal(
    cmd: SealCommand,
    transaction_id: TransactionId,
    channel_id: &ChannelId,
    event_tx: &TransactionEventSender,
    session: &mut Option<SessionId>,
    next_seq: &mut u64,
    last_committed: &mut u64,
    sticky_fail: Option<TerminalEventDelivery>,
) -> TerminalPublicationResult {
    if let Some(fail) = sticky_fail {
        return reply_sticky(cmd, fail, *last_committed);
    }
    let SealCommand {
        mut terminal,
        reply,
        deadline,
    } = cmd;
    let sid = ensure_session(session, terminal.session_id.clone(), transaction_id);
    let seq = *next_seq;
    terminal.emitted_events = seq;
    terminal.session_id = Some(sid.clone());
    let event = TransactionEvent {
        transaction_id,
        channel_id: channel_id.clone(),
        session_id: sid,
        sequence: seq,
        payload: TransactionEventPayload::EndedEvent(terminal),
    };
    let delivery = match enqueue_under_deadline(event_tx, event, deadline).await {
        Ok(()) => {
            *last_committed = seq;
            TerminalEventDelivery::Published
        }
        Err(fail) => fail,
    };
    let result = TerminalPublicationResult {
        delivery,
        last_sequence: *last_committed,
    };
    let _ = reply.send(result.clone());
    result
}

fn tokio_deadline_from(deadline: StdInstant) -> TokioInstant {
    let now = StdInstant::now();
    if deadline > now {
        TokioInstant::now() + deadline.saturating_duration_since(now)
    } else {
        TokioInstant::now()
    }
}

fn map_send_err(err: EventEnqueueError) -> TerminalEventDelivery {
    match err {
        EventEnqueueError::Closed => TerminalEventDelivery::QueueClosed,
        EventEnqueueError::EventTooLarge
        | EventEnqueueError::ByteCapacityExceeded
        | EventEnqueueError::ItemCapacityExceeded => TerminalEventDelivery::LimitExceeded,
    }
}

/// Ordinary enqueue under `deadline`. On Seal, do **not** drop the in-flight
/// event: finish it under the Seal budget, then return the fence.
async fn wait_send_or_seal(
    event_tx: &TransactionEventSender,
    event: TransactionEvent,
    deadline: StdInstant,
    seal_rx: &mut mpsc::Receiver<SealCommand>,
) -> Result<(), WaitEnd> {
    let seq = event.sequence;
    let tokio_deadline = tokio_deadline_from(deadline);
    let send_fut = event_tx.send(event.clone());
    tokio::pin!(send_fut);
    let mut seal_alive = true;
    loop {
        tokio::select! {
            biased;
            seal = seal_rx.recv(), if seal_alive => match seal {
                Some(cmd) => {
                    let seal_deadline = tokio_deadline_from(cmd.deadline);
                    tokio::select! {
                        biased;
                        _ = sleep_until(seal_deadline) => {
                            return Err(WaitEnd::Sealed {
                                cmd,
                                ordinary: OrdinarySealOutcome::Failed(
                                    TerminalEventDelivery::DeadlineExceeded,
                                ),
                            });
                        }
                        res = &mut send_fut => {
                            let ordinary = match res {
                                Ok(()) => OrdinarySealOutcome::Committed { seq },
                                Err(e) => OrdinarySealOutcome::Failed(map_send_err(e)),
                            };
                            return Err(WaitEnd::Sealed { cmd, ordinary });
                        }
                    }
                }
                None => {
                    seal_alive = false;
                }
            },
            _ = sleep_until(tokio_deadline) => {
                return Err(WaitEnd::Failed(TerminalEventDelivery::DeadlineExceeded));
            }
            res = &mut send_fut => {
                return match res {
                    Ok(()) => Ok(()),
                    Err(e) => Err(WaitEnd::Failed(map_send_err(e))),
                };
            }
        }
    }
}

#[allow(clippy::too_many_arguments)]
async fn establish_external(
    event_tx: &TransactionEventSender,
    transaction_id: TransactionId,
    channel_id: &ChannelId,
    external: ExternalSessionId,
    session: &mut Option<SessionId>,
    next_seq: &mut u64,
    last_committed: &mut u64,
    session_established: &mut bool,
    deadline: StdInstant,
    seal_rx: &mut mpsc::Receiver<SealCommand>,
) -> Result<(), WaitEnd> {
    if *session_established || *next_seq != 1 {
        return Ok(());
    }
    let sid = SessionId::from_external(&external);
    let seq = *next_seq;
    let event = TransactionEvent {
        transaction_id,
        channel_id: channel_id.clone(),
        session_id: sid.clone(),
        sequence: seq,
        payload: TransactionEventPayload::SessionEstablished {
            external_session_id: external,
        },
    };
    match wait_send_or_seal(event_tx, event, deadline, seal_rx).await {
        Ok(()) => {
            let _ = ensure_session(session, Some(sid), transaction_id);
            *last_committed = seq;
            *next_seq = seq.saturating_add(1);
            *session_established = true;
            Ok(())
        }
        Err(WaitEnd::Sealed { cmd, ordinary }) => {
            // Mirror commit into session identity when the send already landed.
            if matches!(ordinary, OrdinarySealOutcome::Committed { .. }) {
                let _ = ensure_session(session, Some(sid), transaction_id);
                *session_established = true;
            }
            Err(WaitEnd::Sealed { cmd, ordinary })
        }
        Err(WaitEnd::Failed(fail)) => Err(WaitEnd::Failed(fail)),
    }
}

/// Enqueue one event, waiting only until `deadline`.
async fn enqueue_under_deadline(
    event_tx: &TransactionEventSender,
    event: TransactionEvent,
    deadline: StdInstant,
) -> Result<(), TerminalEventDelivery> {
    let tokio_deadline = tokio_deadline_from(deadline);
    tokio::select! {
        biased;
        _ = sleep_until(tokio_deadline) => Err(TerminalEventDelivery::DeadlineExceeded),
        res = event_tx.send(event) => match res {
            Ok(()) => Ok(()),
            Err(e) => Err(map_send_err(e)),
        },
    }
}