1#![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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ExclusiveState {
30 Ready,
32 Poisoned,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
38#[error("executor was poisoned by a previous panic")]
39pub struct ExclusivePoisoned;
40
41#[derive(Debug)]
48pub struct ExclusiveExecutor<M: Machine> {
49 seat: ExclusiveSeat<M>,
50}
51
52impl<M: Machine> ExclusiveExecutor<M> {
53 #[must_use]
55 pub const fn new(machine: M) -> Self {
56 Self {
57 seat: ExclusiveSeat::Ready(machine),
58 }
59 }
60
61 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 #[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 #[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 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
113pub trait OutputHandler<O> {
115 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
128pub trait OutputEvidence {
130 type Evidence;
132
133 fn evidence(&self) -> Self::Evidence;
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum TurnOutcome {
140 Completed,
142 Poisoned,
144}
145
146pub struct TurnReceipt(Arc<TurnCompletion>);
148
149struct TurnCompletion {
150 outcome: Mutex<Option<TurnOutcome>>,
151 ready: Condvar,
152}
153
154impl TurnReceipt {
155 #[must_use]
161 pub fn outcome(&self) -> Option<TurnOutcome> {
162 *self.0.outcome.lock().expect("turn receipt lock poisoned")
163 }
164
165 #[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 completion.ready.notify_one();
193}
194
195#[derive(Debug, thiserror::Error)]
197#[error("executor was poisoned by a previous panic")]
198pub struct PoisonedInput<I>(
199 pub I,
201);
202
203pub 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
214enum TurnState {
216 Idle,
218 Running,
220 Poisoned,
222}
223
224impl<M: Machine> SerializedExecutor<M> {
225 #[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 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 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
369pub 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
391enum DispatchState {
393 Idle,
395 Dispatching,
397}
398
399impl<M> LinearizedExecutor<M>
400where
401 M: Machine,
402 M::Output: OutputEvidence,
403 <M::Output as OutputEvidence>::Evidence: Clone,
404{
405 #[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 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 #[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480pub enum DispatchOutcome {
481 Drained,
483 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 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 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 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 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}