Skip to main content

bombay_machine_executor/
lib.rs

1//! Concurrent execution policies for pure representable machines.
2//!
3//! [`ExclusiveExecutor`] directly returns outputs to a caller that serializes
4//! turns through exclusive access. [`SerializedExecutor`] provides
5//! run-to-completion turns: it queues inputs
6//! and does not advance the next transition until the preceding output handler
7//! returns. [`LinearizedExecutor`] advances inputs immediately under its lock,
8//! then dispatches already-ordered outputs. The latter policy is appropriate
9//! only when transition linearization may precede completion of earlier work.
10
11#![deny(missing_docs)]
12
13#[cfg(loom)]
14use loom::sync::{Arc, Condvar, Mutex};
15use std::collections::VecDeque;
16#[cfg(not(loom))]
17use std::sync::{Arc, Condvar, Mutex};
18
19pub use bombay_transition::Machine;
20
21#[derive(Debug)]
22enum ExclusiveSeat<M> {
23    Ready(M),
24    Poisoned,
25}
26
27/// Observable state of an [`ExclusiveExecutor`].
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ExclusiveState {
30    /// The successor machine is available for another turn.
31    Ready,
32    /// A machine transition panicked and consumed the previous machine.
33    Poisoned,
34}
35
36/// Failure to recover a machine consumed by a panicking transition.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
38#[error("executor was poisoned by a previous panic")]
39pub struct ExclusivePoisoned;
40
41/// Allocation-free execution of an affine machine through exclusive access.
42///
43/// A turn installs the poisoned state before calling [`Machine::step`]. If the
44/// transition unwinds, no machine remains accessible and all future inputs are
45/// rejected intact. Output consumption after a successful turn is outside this
46/// poison boundary.
47#[derive(Debug)]
48pub struct ExclusiveExecutor<M: Machine> {
49    seat: ExclusiveSeat<M>,
50}
51
52impl<M: Machine> ExclusiveExecutor<M> {
53    /// Construct an executor containing `machine`.
54    #[must_use]
55    pub const fn new(machine: M) -> Self {
56        Self {
57            seat: ExclusiveSeat::Ready(machine),
58        }
59    }
60
61    /// Execute one immediate turn and install its successor.
62    ///
63    /// # Errors
64    ///
65    /// Returns the supplied input without accepting it if an earlier transition
66    /// poisoned the executor. An input consumed by a transition that panics is
67    /// not recoverable.
68    ///
69    /// # Panics
70    ///
71    /// Propagates a panic from [`Machine::step`] after poisoning the executor.
72    pub fn turn(&mut self, input: M::Input) -> Result<M::Output, PoisonedInput<M::Input>> {
73        let machine = match core::mem::replace(&mut self.seat, ExclusiveSeat::Poisoned) {
74            ExclusiveSeat::Ready(machine) => machine,
75            ExclusiveSeat::Poisoned => return Err(PoisonedInput(input)),
76        };
77        let (output, successor) = machine.step(input);
78        self.seat = ExclusiveSeat::Ready(successor);
79        Ok(output)
80    }
81
82    /// Report whether a successor machine remains available.
83    #[must_use]
84    pub const fn state(&self) -> ExclusiveState {
85        match &self.seat {
86            ExclusiveSeat::Ready(_) => ExclusiveState::Ready,
87            ExclusiveSeat::Poisoned => ExclusiveState::Poisoned,
88        }
89    }
90
91    /// Borrow the current successor machine, or `None` if it was poisoned.
92    #[must_use]
93    pub const fn machine(&self) -> Option<&M> {
94        match &self.seat {
95            ExclusiveSeat::Ready(machine) => Some(machine),
96            ExclusiveSeat::Poisoned => None,
97        }
98    }
99
100    /// Recover the current successor machine.
101    ///
102    /// # Errors
103    ///
104    /// Returns [`ExclusivePoisoned`] if a panicking transition consumed it.
105    pub fn into_inner(self) -> Result<M, ExclusivePoisoned> {
106        match self.seat {
107            ExclusiveSeat::Ready(machine) => Ok(machine),
108            ExclusiveSeat::Poisoned => Err(ExclusivePoisoned),
109        }
110    }
111}
112
113/// Handles one machine output synchronously.
114pub trait OutputHandler<O> {
115    /// Handle the complete output of one transition.
116    fn handle(&self, output: O);
117}
118
119impl<O, F> OutputHandler<O> for F
120where
121    F: Fn(O),
122{
123    fn handle(&self, output: O) {
124        self(output);
125    }
126}
127
128/// Extracts small copyable evidence before an output is queued for dispatch.
129pub trait OutputEvidence {
130    /// Evidence returned to the submitting caller.
131    type Evidence;
132
133    /// Extract evidence without consuming the output.
134    fn evidence(&self) -> Self::Evidence;
135}
136
137/// Result of waiting for a serialized turn.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum TurnOutcome {
140    /// The transition and its complete synchronous output handling finished.
141    Completed,
142    /// The executor was poisoned by a transition or output-handler panic.
143    Poisoned,
144}
145
146/// Completion receipt for one serialized input.
147pub struct TurnReceipt(Arc<TurnCompletion>);
148
149struct TurnCompletion {
150    outcome: Mutex<Option<TurnOutcome>>,
151    ready: Condvar,
152}
153
154impl TurnReceipt {
155    /// Return the outcome without blocking, if the turn has finished.
156    ///
157    /// # Panics
158    ///
159    /// Panics if receipt synchronization was poisoned.
160    #[must_use]
161    pub fn outcome(&self) -> Option<TurnOutcome> {
162        *self.0.outcome.lock().expect("turn receipt lock poisoned")
163    }
164
165    /// Block until the turn completes or its executor is poisoned.
166    ///
167    /// # Panics
168    ///
169    /// Panics if receipt synchronization was poisoned.
170    #[must_use]
171    pub fn wait(self) -> TurnOutcome {
172        let mut outcome = self.0.outcome.lock().expect("turn receipt lock poisoned");
173        loop {
174            if let Some(outcome) = *outcome {
175                return outcome;
176            }
177            outcome = self
178                .0
179                .ready
180                .wait(outcome)
181                .expect("turn receipt lock poisoned");
182        }
183    }
184}
185
186fn complete(completion: &TurnCompletion, outcome: TurnOutcome) {
187    *completion
188        .outcome
189        .lock()
190        .expect("turn receipt lock poisoned") = Some(outcome);
191    // wait(self) consumes the receipt, so at most one waiter exists.
192    completion.ready.notify_one();
193}
194
195/// Rejection of an input after a serialized executor was poisoned.
196#[derive(Debug, thiserror::Error)]
197#[error("executor was poisoned by a previous panic")]
198pub struct PoisonedInput<I>(
199    /// Input whose ownership was not accepted.
200    pub I,
201);
202
203/// Serialized run-to-completion execution of one machine.
204pub struct SerializedExecutor<M: Machine> {
205    execution: Mutex<SerializedExecution<M>>,
206}
207
208struct SerializedExecution<M: Machine> {
209    machine: Option<M>,
210    inputs: VecDeque<(M::Input, Arc<TurnCompletion>)>,
211    turn: TurnState,
212}
213
214/// Ownership phase of the serialized turn drain.
215enum TurnState {
216    /// No caller is draining turns.
217    Idle,
218    /// One caller owns the drain loop.
219    Running,
220    /// A transition or handler panic poisoned the executor.
221    Poisoned,
222}
223
224impl<M: Machine> SerializedExecutor<M> {
225    /// Construct a serialized executor with an empty input queue.
226    #[must_use]
227    pub fn new(machine: M) -> Self {
228        Self {
229            execution: Mutex::new(SerializedExecution {
230                machine: Some(machine),
231                inputs: VecDeque::new(),
232                turn: TurnState::Idle,
233            }),
234        }
235    }
236
237    /// Queue one input and, when this caller acquires ownership, drain turns.
238    ///
239    /// Reentrant and concurrent calls enqueue their input and return a receipt;
240    /// they never advance a transition while an earlier output is being handled.
241    /// Only the drain owner's `handler` processes outputs: a caller that loses
242    /// ownership has its input handled by the owner's handler, while its
243    /// receipt still reports completion of its own turn. Waiting on a receipt
244    /// from inside `handler` would deadlock and must be deferred until the
245    /// outer turn returns.
246    ///
247    /// # Errors
248    ///
249    /// Returns input ownership when a previous transition or handler panicked.
250    ///
251    /// # Panics
252    ///
253    /// Propagates a machine transition or output-handler panic after poisoning
254    /// this executor and resolving every outstanding receipt.
255    pub fn submit<H>(
256        &self,
257        input: M::Input,
258        handler: &H,
259    ) -> Result<TurnReceipt, PoisonedInput<M::Input>>
260    where
261        H: OutputHandler<M::Output>,
262    {
263        let completion = Arc::new(TurnCompletion {
264            outcome: Mutex::new(None),
265            ready: Condvar::new(),
266        });
267        let owns = {
268            let mut execution = self.execution.lock().expect("executor lock poisoned");
269            match execution.turn {
270                TurnState::Poisoned => return Err(PoisonedInput(input)),
271                TurnState::Running => {
272                    execution.inputs.push_back((input, Arc::clone(&completion)));
273                    false
274                }
275                TurnState::Idle => {
276                    execution.inputs.push_back((input, Arc::clone(&completion)));
277                    execution.turn = TurnState::Running;
278                    true
279                }
280            }
281        };
282        if owns {
283            self.drain(handler);
284        }
285        Ok(TurnReceipt(completion))
286    }
287
288    fn drain<H>(&self, handler: &H)
289    where
290        H: OutputHandler<M::Output>,
291    {
292        let mut ownership = SerializedOwnership::new(&self.execution);
293        loop {
294            let Some((machine, input, completion)) = ownership.take_turn() else {
295                return;
296            };
297            let (output, successor) = machine.step(input);
298            ownership.install(successor, &completion);
299            handler.handle(output);
300            complete(&completion, TurnOutcome::Completed);
301            ownership.turn_completed();
302        }
303    }
304}
305
306struct SerializedOwnership<'a, M: Machine> {
307    execution: Option<&'a Mutex<SerializedExecution<M>>>,
308    active: Option<Arc<TurnCompletion>>,
309}
310
311impl<'a, M: Machine> SerializedOwnership<'a, M> {
312    fn new(execution: &'a Mutex<SerializedExecution<M>>) -> Self {
313        Self {
314            execution: Some(execution),
315            active: None,
316        }
317    }
318
319    fn take_turn(&mut self) -> Option<(M, M::Input, Arc<TurnCompletion>)> {
320        let execution = self.execution?;
321        let mut state = execution.lock().expect("executor lock poisoned");
322        let Some((input, completion)) = state.inputs.pop_front() else {
323            state.turn = TurnState::Idle;
324            // Normal exhaustion disarms the guard: dropping it must not poison.
325            self.execution = None;
326            return None;
327        };
328        let machine = state.machine.take().expect("executor machine missing");
329        self.active = Some(Arc::clone(&completion));
330        Some((machine, input, completion))
331    }
332
333    fn install(&self, machine: M, completion: &Arc<TurnCompletion>) {
334        self.execution
335            .expect("ownership armed")
336            .lock()
337            .expect("executor lock poisoned")
338            .machine = Some(machine);
339        debug_assert!(Arc::ptr_eq(
340            self.active.as_ref().expect("active turn"),
341            completion
342        ));
343    }
344
345    fn turn_completed(&mut self) {
346        self.active = None;
347    }
348}
349
350impl<M: Machine> Drop for SerializedOwnership<'_, M> {
351    fn drop(&mut self) {
352        let Some(execution) = self.execution.take() else {
353            return;
354        };
355        let mut state = execution
356            .lock()
357            .unwrap_or_else(std::sync::PoisonError::into_inner);
358        state.turn = TurnState::Poisoned;
359        if let Some(active) = self.active.take() {
360            complete(&active, TurnOutcome::Poisoned);
361        }
362        state
363            .inputs
364            .drain(..)
365            .for_each(|(_, receipt)| complete(&receipt, TurnOutcome::Poisoned));
366    }
367}
368
369/// Transition-linearized execution with separately ordered output dispatch.
370pub struct LinearizedExecutor<M>
371where
372    M: Machine,
373    M::Output: OutputEvidence,
374    <M::Output as OutputEvidence>::Evidence: Clone,
375{
376    execution: Mutex<LinearizedExecution<M, M::Output, <M::Output as OutputEvidence>::Evidence>>,
377}
378
379struct LinearizedExecution<M, O, E> {
380    machine: LinearizedMachine<M>,
381    outputs: VecDeque<O>,
382    evidence: Option<E>,
383    dispatch: DispatchState,
384}
385
386enum LinearizedMachine<M> {
387    Ready(M),
388    Poisoned,
389}
390
391/// Ownership phase of output dispatch.
392enum DispatchState {
393    /// No caller is dispatching queued outputs.
394    Idle,
395    /// One caller owns output dispatch.
396    Dispatching,
397}
398
399impl<M> LinearizedExecutor<M>
400where
401    M: Machine,
402    M::Output: OutputEvidence,
403    <M::Output as OutputEvidence>::Evidence: Clone,
404{
405    /// Construct an executor with an empty output queue.
406    #[must_use]
407    pub fn new(machine: M) -> Self {
408        Self {
409            execution: Mutex::new(LinearizedExecution {
410                machine: LinearizedMachine::Ready(machine),
411                outputs: VecDeque::new(),
412                evidence: None,
413                dispatch: DispatchState::Idle,
414            }),
415        }
416    }
417
418    /// Advance and enqueue one output at the same linearization point.
419    ///
420    /// # Panics
421    ///
422    /// Panics after synchronization poison or a transition panic that consumed
423    /// the affine machine state.
424    pub fn submit(&self, input: M::Input) -> <M::Output as OutputEvidence>::Evidence {
425        let mut execution = self.execution.lock().expect("executor lock poisoned");
426        let LinearizedMachine::Ready(machine) =
427            core::mem::replace(&mut execution.machine, LinearizedMachine::Poisoned)
428        else {
429            panic!("executor machine poisoned");
430        };
431        let (output, successor) = machine.step(input);
432        let evidence = output.evidence();
433        execution.evidence = Some(evidence.clone());
434        execution.machine = LinearizedMachine::Ready(successor);
435        execution.outputs.push_back(output);
436        evidence
437    }
438
439    /// Clone the evidence installed by the latest linearized transition.
440    ///
441    /// # Panics
442    ///
443    /// Panics if executor synchronization was poisoned.
444    #[must_use]
445    pub fn evidence(&self) -> Option<<M::Output as OutputEvidence>::Evidence> {
446        self.execution
447            .lock()
448            .expect("executor lock poisoned")
449            .evidence
450            .clone()
451    }
452
453    /// Dispatch queued outputs until empty, or contribute them to another owner.
454    ///
455    /// [`DispatchOutcome::OwnedElsewhere`] means another caller owns dispatch
456    /// and this call is fire-and-forget; it does not mean the caller's output
457    /// completed. If a handler panics, its owned output is dropped exactly once
458    /// and a later call resumes with the remaining queue.
459    ///
460    /// # Panics
461    ///
462    /// Panics if executor synchronization was poisoned by a transition panic
463    /// in [`LinearizedExecutor::submit`].
464    pub fn dispatch_pending<H>(&self, handler: &H) -> DispatchOutcome
465    where
466        H: OutputHandler<M::Output>,
467    {
468        let Some(mut ownership) = DispatchOwnership::acquire(&self.execution) else {
469            return DispatchOutcome::OwnedElsewhere;
470        };
471        while let Some(output) = ownership.next() {
472            handler.handle(output);
473        }
474        DispatchOutcome::Drained
475    }
476}
477
478/// Ownership result of one [`LinearizedExecutor::dispatch_pending`] call.
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum DispatchOutcome {
481    /// This call owned dispatch and drained the output queue.
482    Drained,
483    /// Another caller owns dispatch; queued outputs will be handled there.
484    OwnedElsewhere,
485}
486
487struct DispatchOwnership<'a, M, O, E> {
488    execution: Option<&'a Mutex<LinearizedExecution<M, O, E>>>,
489}
490
491impl<'a, M, O, E> DispatchOwnership<'a, M, O, E> {
492    fn acquire(execution: &'a Mutex<LinearizedExecution<M, O, E>>) -> Option<Self> {
493        let mut state = execution.lock().expect("executor lock poisoned");
494        match state.dispatch {
495            DispatchState::Dispatching => None,
496            DispatchState::Idle => {
497                state.dispatch = DispatchState::Dispatching;
498                Some(Self {
499                    execution: Some(execution),
500                })
501            }
502        }
503    }
504
505    fn next(&mut self) -> Option<O> {
506        let execution = self.execution?;
507        let mut state = execution.lock().expect("executor lock poisoned");
508        let output = state.outputs.pop_front();
509        if output.is_none() {
510            state.dispatch = DispatchState::Idle;
511            // An exhausted queue releases ownership; dropping must not repeat it.
512            self.execution = None;
513        }
514        output
515    }
516}
517
518impl<M, O, E> Drop for DispatchOwnership<'_, M, O, E> {
519    fn drop(&mut self) {
520        if let Some(execution) = self.execution.take() {
521            // This drop can run while its own `next` unwinds after another
522            // thread poisoned the executor; a second panic here would abort
523            // the process. Recovering the guard preserves the documented
524            // panic-only contract.
525            execution
526                .lock()
527                .unwrap_or_else(std::sync::PoisonError::into_inner)
528                .dispatch = DispatchState::Idle;
529        }
530    }
531}
532
533#[cfg(all(test, not(loom)))]
534mod tests {
535    use std::panic::{AssertUnwindSafe, catch_unwind};
536    use std::sync::{Arc, Mutex, Weak};
537
538    use bombay_transition::{Base, Topology, Vertex, VertexId};
539
540    use super::{
541        ExclusiveExecutor, ExclusiveState, LinearizedExecutor, Machine, OutputEvidence,
542        OutputHandler, SerializedExecutor, TurnOutcome, TurnReceipt,
543    };
544
545    const VERTICES: &[Vertex] = &[Vertex {
546        id: VertexId(0),
547        label: "ready",
548    }];
549    const TOPOLOGY: Topology = Topology {
550        name: "test",
551        initial: VertexId(0),
552        vertices: VERTICES,
553        transitions: &[],
554    };
555
556    #[derive(Debug)]
557    struct Output(u8);
558
559    impl OutputEvidence for Output {
560        type Evidence = u8;
561        fn evidence(&self) -> Self::Evidence {
562            self.0
563        }
564    }
565
566    fn machine() -> Base<u8, impl FnMut(u8, u8) -> (Output, u8), u8, Output> {
567        Base::new(0, TOPOLOGY.validated().unwrap(), |state, input| {
568            (Output(input), state + input)
569        })
570    }
571
572    #[derive(Debug)]
573    struct ExclusiveTestMachine {
574        state: usize,
575        steps: Arc<std::sync::atomic::AtomicUsize>,
576        panic: bool,
577    }
578
579    #[derive(Debug, PartialEq, Eq)]
580    struct OwnedOutput(Box<str>);
581
582    impl Machine for ExclusiveTestMachine {
583        type Input = usize;
584        type Output = OwnedOutput;
585
586        fn step(self, input: Self::Input) -> (Self::Output, Self) {
587            self.steps.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
588            assert!(!self.panic, "transition failure");
589            let successor = Self {
590                state: self.state + input,
591                steps: self.steps,
592                panic: false,
593            };
594            (OwnedOutput(format!("output-{input}").into()), successor)
595        }
596
597        fn describe<V: bombay_transition::Structure>(&self, visitor: &mut V) -> V::Output {
598            visitor.base(TOPOLOGY)
599        }
600    }
601
602    #[test]
603    fn exclusive_turn_returns_output_and_installs_successor() {
604        let steps = Arc::new(std::sync::atomic::AtomicUsize::new(0));
605        let mut executor = ExclusiveExecutor::new(ExclusiveTestMachine {
606            state: 1,
607            steps: Arc::clone(&steps),
608            panic: false,
609        });
610
611        assert_eq!(executor.state(), ExclusiveState::Ready);
612        assert_eq!(executor.machine().unwrap().state, 1);
613        assert_eq!(executor.turn(2).unwrap(), OwnedOutput("output-2".into()));
614        assert_eq!(executor.machine().unwrap().state, 3);
615        assert_eq!(executor.turn(4).unwrap(), OwnedOutput("output-4".into()));
616        assert_eq!(executor.into_inner().unwrap().state, 7);
617        assert_eq!(steps.load(std::sync::atomic::Ordering::SeqCst), 2);
618    }
619
620    #[test]
621    fn exclusive_transition_panic_permanently_refuses_later_input() {
622        let steps = Arc::new(std::sync::atomic::AtomicUsize::new(0));
623        let mut executor = ExclusiveExecutor::new(ExclusiveTestMachine {
624            state: 0,
625            steps: Arc::clone(&steps),
626            panic: true,
627        });
628
629        assert!(
630            catch_unwind(AssertUnwindSafe(|| {
631                let _ = executor.turn(1);
632            }))
633            .is_err()
634        );
635        assert_eq!(executor.state(), ExclusiveState::Poisoned);
636        assert!(executor.machine().is_none());
637        let Err(rejected) = executor.turn(9) else {
638            panic!("poisoned executor accepted input")
639        };
640        assert_eq!(rejected.0, 9);
641        assert_eq!(steps.load(std::sync::atomic::Ordering::SeqCst), 1);
642        assert!(matches!(
643            executor.into_inner(),
644            Err(super::ExclusivePoisoned)
645        ));
646    }
647
648    #[test]
649    fn exclusive_executor_inherits_machine_auto_traits() {
650        const fn assert_send_sync<T: Send + Sync>() {}
651        assert_send_sync::<ExclusiveExecutor<ExclusiveTestMachine>>();
652    }
653
654    #[derive(Debug)]
655    struct DropSentinel(Arc<std::sync::atomic::AtomicUsize>);
656
657    impl Drop for DropSentinel {
658        fn drop(&mut self) {
659            self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
660        }
661    }
662
663    #[derive(Debug)]
664    struct TrackedInput {
665        id: usize,
666        _drop: DropSentinel,
667    }
668
669    #[derive(Debug)]
670    struct TrackedOutput {
671        id: usize,
672        _drop: DropSentinel,
673    }
674
675    #[derive(Debug)]
676    struct OwnershipMachine {
677        panic: bool,
678        steps: Arc<std::sync::atomic::AtomicUsize>,
679        _machine_drop: DropSentinel,
680        successor_drops: Arc<std::sync::atomic::AtomicUsize>,
681        output_drops: Arc<std::sync::atomic::AtomicUsize>,
682    }
683
684    impl Machine for OwnershipMachine {
685        type Input = TrackedInput;
686        type Output = TrackedOutput;
687
688        fn step(self, input: Self::Input) -> (Self::Output, Self) {
689            self.steps.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
690            assert!(!self.panic, "transition failure");
691            let output = TrackedOutput {
692                id: input.id,
693                _drop: DropSentinel(Arc::clone(&self.output_drops)),
694            };
695            drop(input);
696            let successor = Self {
697                panic: false,
698                steps: Arc::clone(&self.steps),
699                _machine_drop: DropSentinel(Arc::clone(&self.successor_drops)),
700                successor_drops: self.successor_drops,
701                output_drops: self.output_drops,
702            };
703            (output, successor)
704        }
705
706        fn describe<V: bombay_transition::Structure>(&self, visitor: &mut V) -> V::Output {
707            visitor.base(TOPOLOGY)
708        }
709    }
710
711    #[test]
712    fn exclusive_success_moves_each_owned_payload_exactly_once() {
713        let original_machine_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
714        let successor_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
715        let input_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
716        let output_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
717        let steps = Arc::new(std::sync::atomic::AtomicUsize::new(0));
718        let mut executor = ExclusiveExecutor::new(OwnershipMachine {
719            panic: false,
720            steps: Arc::clone(&steps),
721            _machine_drop: DropSentinel(Arc::clone(&original_machine_drops)),
722            successor_drops: Arc::clone(&successor_drops),
723            output_drops: Arc::clone(&output_drops),
724        });
725
726        let output = executor
727            .turn(TrackedInput {
728                id: 41,
729                _drop: DropSentinel(Arc::clone(&input_drops)),
730            })
731            .unwrap();
732        assert_eq!(output.id, 41);
733        assert_eq!(steps.load(std::sync::atomic::Ordering::SeqCst), 1);
734        assert_eq!(
735            original_machine_drops.load(std::sync::atomic::Ordering::SeqCst),
736            1
737        );
738        assert_eq!(input_drops.load(std::sync::atomic::Ordering::SeqCst), 1);
739        assert_eq!(successor_drops.load(std::sync::atomic::Ordering::SeqCst), 0);
740        assert_eq!(output_drops.load(std::sync::atomic::Ordering::SeqCst), 0);
741
742        drop(output);
743        drop(executor.into_inner().unwrap());
744        assert_eq!(output_drops.load(std::sync::atomic::Ordering::SeqCst), 1);
745        assert_eq!(successor_drops.load(std::sync::atomic::Ordering::SeqCst), 1);
746    }
747
748    #[test]
749    fn exclusive_panic_consumes_active_values_but_returns_later_input() {
750        let machine_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
751        let accepted_input_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
752        let rejected_input_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
753        let steps = Arc::new(std::sync::atomic::AtomicUsize::new(0));
754        let mut executor = ExclusiveExecutor::new(OwnershipMachine {
755            panic: true,
756            steps: Arc::clone(&steps),
757            _machine_drop: DropSentinel(Arc::clone(&machine_drops)),
758            successor_drops: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
759            output_drops: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
760        });
761
762        assert!(
763            catch_unwind(AssertUnwindSafe(|| {
764                let _ = executor.turn(TrackedInput {
765                    id: 1,
766                    _drop: DropSentinel(Arc::clone(&accepted_input_drops)),
767                });
768            }))
769            .is_err()
770        );
771        assert_eq!(machine_drops.load(std::sync::atomic::Ordering::SeqCst), 1);
772        assert_eq!(
773            accepted_input_drops.load(std::sync::atomic::Ordering::SeqCst),
774            1
775        );
776
777        let Err(rejected) = executor.turn(TrackedInput {
778            id: 73,
779            _drop: DropSentinel(Arc::clone(&rejected_input_drops)),
780        }) else {
781            panic!("poisoned executor accepted input")
782        };
783        assert_eq!(rejected.0.id, 73);
784        assert_eq!(steps.load(std::sync::atomic::Ordering::SeqCst), 1);
785        assert_eq!(
786            rejected_input_drops.load(std::sync::atomic::Ordering::SeqCst),
787            0
788        );
789        drop(rejected);
790        assert_eq!(
791            rejected_input_drops.load(std::sync::atomic::Ordering::SeqCst),
792            1
793        );
794        drop(executor);
795        assert_eq!(machine_drops.load(std::sync::atomic::Ordering::SeqCst), 1);
796    }
797
798    #[test]
799    fn poisoned_input_reports_the_rejection() {
800        assert_eq!(
801            super::PoisonedInput(7_u8).to_string(),
802            "executor was poisoned by a previous panic"
803        );
804    }
805
806    #[test]
807    fn serialized_turns_finish_effects_before_the_next_transition() {
808        let executor = SerializedExecutor::new(machine());
809        let trace = Mutex::new(Vec::new());
810        let receipt = executor
811            .submit(1, &|output: Output| trace.lock().unwrap().push(output.0))
812            .unwrap();
813        assert_eq!(receipt.wait(), TurnOutcome::Completed);
814        assert_eq!(*trace.lock().unwrap(), [1]);
815    }
816
817    type TestMachine = Base<u8, fn(u8, u8) -> (Output, u8), u8, Output>;
818
819    struct ReentrantHandler {
820        executor: Weak<SerializedExecutor<TestMachine>>,
821        trace: Arc<Mutex<Vec<u8>>>,
822    }
823
824    impl OutputHandler<Output> for ReentrantHandler {
825        fn handle(&self, output: Output) {
826            self.trace.lock().unwrap().push(output.0);
827            if output.0 == 1 {
828                let executor = self.executor.upgrade().unwrap();
829                let receipt = executor.submit(2, self).unwrap();
830                assert_eq!(receipt.outcome(), None);
831            }
832        }
833    }
834
835    #[test]
836    fn reentrant_serialized_submission_waits_for_current_handler() {
837        fn transition(state: u8, input: u8) -> (Output, u8) {
838            (Output(input), state + input)
839        }
840        let executor = Arc::new(SerializedExecutor::new(Base::new(
841            0,
842            TOPOLOGY.validated().unwrap(),
843            transition as fn(u8, u8) -> (Output, u8),
844        )));
845        let trace = Arc::new(Mutex::new(Vec::new()));
846        let handler = ReentrantHandler {
847            executor: Arc::downgrade(&executor),
848            trace: Arc::clone(&trace),
849        };
850        assert_eq!(
851            executor.submit(1, &handler).unwrap().wait(),
852            TurnOutcome::Completed
853        );
854        assert_eq!(*trace.lock().unwrap(), [1, 2]);
855    }
856
857    #[test]
858    fn linearized_dispatch_resumes_after_handler_panic() {
859        let executor = LinearizedExecutor::new(machine());
860        assert_eq!(executor.submit(1), 1);
861        assert_eq!(executor.submit(2), 2);
862        assert!(
863            catch_unwind(AssertUnwindSafe(|| {
864                executor.dispatch_pending(&|_: Output| panic!("handler"));
865            }))
866            .is_err()
867        );
868        let seen = Mutex::new(Vec::new());
869        assert_eq!(
870            executor.dispatch_pending(&|output: Output| seen.lock().unwrap().push(output.0)),
871            super::DispatchOutcome::Drained
872        );
873        assert_eq!(*seen.lock().unwrap(), [2]);
874    }
875
876    #[test]
877    fn serialized_handler_panic_poisons_future_submissions() {
878        let executor = SerializedExecutor::new(machine());
879        assert!(
880            catch_unwind(AssertUnwindSafe(|| {
881                let _ = executor.submit(1, &|_: Output| panic!("handler"));
882            }))
883            .is_err()
884        );
885        let Err(rejected) = executor.submit(2, &|_: Output| {}) else {
886            panic!("poisoned executor accepted input")
887        };
888        assert_eq!(rejected.0, 2);
889    }
890
891    struct ReentrantPoisonHandler {
892        executor: Weak<SerializedExecutor<TestMachine>>,
893        queued: Mutex<Option<TurnReceipt>>,
894    }
895
896    impl OutputHandler<Output> for ReentrantPoisonHandler {
897        fn handle(&self, output: Output) {
898            if output.0 == 1 {
899                let receipt = self.executor.upgrade().unwrap().submit(2, self).unwrap();
900                *self.queued.lock().unwrap() = Some(receipt);
901                panic!("handler");
902            }
903        }
904    }
905
906    #[test]
907    fn serialized_handler_panic_resolves_queued_receipt_as_poisoned() {
908        fn transition(state: u8, input: u8) -> (Output, u8) {
909            (Output(input), state + input)
910        }
911        let executor = Arc::new(SerializedExecutor::new(Base::new(
912            0,
913            TOPOLOGY.validated().unwrap(),
914            transition as fn(u8, u8) -> (Output, u8),
915        )));
916        let handler = ReentrantPoisonHandler {
917            executor: Arc::downgrade(&executor),
918            queued: Mutex::new(None),
919        };
920
921        assert!(catch_unwind(AssertUnwindSafe(|| executor.submit(1, &handler))).is_err());
922        let queued = handler.queued.lock().unwrap().take().unwrap();
923        assert_eq!(queued.outcome(), Some(TurnOutcome::Poisoned));
924        assert_eq!(queued.wait(), TurnOutcome::Poisoned);
925        assert!(matches!(
926            executor.submit(3, &handler),
927            Err(super::PoisonedInput(3))
928        ));
929    }
930
931    impl super::OutputEvidence for usize {
932        type Evidence = usize;
933
934        fn evidence(&self) -> Self::Evidence {
935            *self
936        }
937    }
938
939    #[test]
940    fn dispatch_guard_drop_recovers_during_poison_unwind() {
941        use std::sync::Condvar;
942        use std::thread;
943
944        let machine = Base::new(0_usize, TOPOLOGY.validated().unwrap(), |state, input| {
945            assert_ne!(input, 9, "transition failure");
946            (input, state + input)
947        });
948        let executor = Arc::new(LinearizedExecutor::new(machine));
949        assert_eq!(executor.submit(1), 1);
950        let gate = Arc::new((Mutex::new((false, false)), Condvar::new()));
951
952        let dispatcher = {
953            let executor = Arc::clone(&executor);
954            let gate = Arc::clone(&gate);
955            thread::spawn(move || {
956                catch_unwind(AssertUnwindSafe(|| {
957                    executor.dispatch_pending(&|_output| {
958                        let (lock, ready) = &*gate;
959                        let mut phase = lock.lock().unwrap();
960                        phase.0 = true;
961                        ready.notify_one();
962                        // Hold dispatch ownership until the transition panic
963                        // has landed; returning loops into next(), which then
964                        // observes the poisoned executor.
965                        while !phase.1 {
966                            phase = ready.wait(phase).unwrap();
967                        }
968                    });
969                }))
970            })
971        };
972        {
973            let (lock, ready) = &*gate;
974            let mut phase = lock.lock().unwrap();
975            while !phase.0 {
976                phase = ready.wait(phase).unwrap();
977            }
978        }
979        // The dispatcher holds dispatch ownership inside the handler; this
980        // transition panic poisons the executor underneath it.
981        let _ = catch_unwind(AssertUnwindSafe(|| {
982            executor.submit(9);
983        }));
984        {
985            let (lock, ready) = &*gate;
986            *lock.lock().unwrap() = (true, true);
987            ready.notify_one();
988        }
989        let outcome = dispatcher.join().expect("dispatcher thread aborted");
990        assert!(outcome.is_err(), "next() must observe the poisoned lock");
991        assert!(
992            catch_unwind(AssertUnwindSafe(|| {
993                executor.submit(2);
994            }))
995            .is_err()
996        );
997    }
998}
999
1000#[cfg(all(test, loom))]
1001mod loom_tests {
1002    use loom::sync::atomic::{AtomicUsize, Ordering};
1003    use loom::sync::{Arc, Mutex};
1004    use loom::thread;
1005
1006    use bombay_transition::{Base, Topology, Vertex, VertexId};
1007
1008    use super::{LinearizedExecutor, OutputEvidence, SerializedExecutor, TurnOutcome};
1009
1010    const VERTICES: &[Vertex] = &[Vertex {
1011        id: VertexId(0),
1012        label: "ready",
1013    }];
1014    const TOPOLOGY: Topology = Topology {
1015        name: "loom",
1016        initial: VertexId(0),
1017        vertices: VERTICES,
1018        transitions: &[],
1019    };
1020
1021    struct Output(usize, Arc<AtomicUsize>);
1022
1023    impl Drop for Output {
1024        fn drop(&mut self) {
1025            self.1.fetch_add(1, Ordering::SeqCst);
1026        }
1027    }
1028
1029    impl OutputEvidence for Output {
1030        type Evidence = usize;
1031
1032        fn evidence(&self) -> Self::Evidence {
1033            self.0
1034        }
1035    }
1036
1037    #[test]
1038    fn real_linearized_executor_handles_submit_dispatch_boundary() {
1039        loom::model(|| {
1040            let drops = Arc::new(AtomicUsize::new(0));
1041            let machine_drops = Arc::clone(&drops);
1042            let machine = Base::new(0, TOPOLOGY.validated().unwrap(), move |state, input| {
1043                (Output(input, Arc::clone(&machine_drops)), state + input)
1044            });
1045            let executor = Arc::new(LinearizedExecutor::new(machine));
1046            let seen = Arc::new(Mutex::new(Vec::new()));
1047
1048            let submitter = {
1049                let executor = Arc::clone(&executor);
1050                thread::spawn(move || {
1051                    executor.submit(1);
1052                    executor.submit(2);
1053                })
1054            };
1055            let dispatcher = {
1056                let executor = Arc::clone(&executor);
1057                let seen = Arc::clone(&seen);
1058                thread::spawn(move || {
1059                    executor.dispatch_pending(&|output: Output| {
1060                        seen.lock().unwrap().push(output.0);
1061                    });
1062                })
1063            };
1064            submitter.join().unwrap();
1065            dispatcher.join().unwrap();
1066            executor.dispatch_pending(&|output: Output| {
1067                seen.lock().unwrap().push(output.0);
1068            });
1069            assert_eq!(*seen.lock().unwrap(), [1, 2]);
1070            assert_eq!(drops.load(Ordering::SeqCst), 2);
1071        });
1072    }
1073
1074    #[test]
1075    fn real_serialized_executor_keeps_each_turn_contiguous() {
1076        loom::model(|| {
1077            let trace = Arc::new(Mutex::new(Vec::new()));
1078            let machine_trace = Arc::clone(&trace);
1079            let machine = Base::new((), TOPOLOGY.validated().unwrap(), move |(), input| {
1080                machine_trace.lock().unwrap().push(input * 10);
1081                (input, ())
1082            });
1083            let executor = Arc::new(SerializedExecutor::new(machine));
1084            let mut threads = Vec::new();
1085            for input in [1, 2] {
1086                let executor = Arc::clone(&executor);
1087                let trace = Arc::clone(&trace);
1088                threads.push(thread::spawn(move || {
1089                    let receipt = executor
1090                        .submit(input, &|output| {
1091                            trace.lock().unwrap().push(output * 10 + 1);
1092                        })
1093                        .unwrap();
1094                    assert_eq!(receipt.wait(), TurnOutcome::Completed);
1095                }));
1096            }
1097            threads
1098                .into_iter()
1099                .for_each(|thread| thread.join().unwrap());
1100            let trace = trace.lock().unwrap();
1101            assert!(matches!(
1102                trace.as_slice(),
1103                [10, 11, 20, 21] | [20, 21, 10, 11]
1104            ));
1105        });
1106    }
1107}