1#![forbid(unsafe_code)]
10#![warn(missing_docs)]
11
12use std::fmt::Debug;
13
14use pacta_contract::{Pact, Registry, Retainer, Timestamp};
15use uuid::Uuid;
16
17pub const LEASE_MILLIS: u64 = 1000;
19
20const DOCKET: &str = "conformance";
21
22fn at(millis: u64) -> Timestamp {
23 Timestamp::from_millis(millis)
24}
25
26fn a_pact_on(docket: &str) -> Pact {
27 Pact::new(
28 Uuid::new_v4(),
29 docket.to_string(),
30 "conformance".to_string(),
31 Vec::new(),
32 )
33}
34
35fn a_pact() -> Pact {
36 a_pact_on(DOCKET)
37}
38
39pub fn run<R, F>(make: F)
47where
48 R: Registry,
49 R::Error: Debug,
50 F: Fn(Vec<Pact>, u64) -> R,
51{
52 no_available_pact_returns_none(&make);
53 unrequested_docket_is_not_claimed(&make);
54 claim_returns_claim_with_lease(&make);
55 held_pact_not_reclaimable_before_expiry(&make);
56 expired_lease_lapses_and_reclaims_with_rotated_retainer(&make);
57 stale_retainer_settle_rejected_after_reclaim(&make);
58 late_fulfill_before_reclaim_succeeds(&make);
59 fulfill_settles_and_pact_not_claimable(&make);
60 breach_settles_terminally(&make);
61 released_pact_withheld_until_reclaimable(&make);
62 released_pact_reclaimable_at_its_instant(&make);
63 immediate_reclaim_reclaims_like_lapse(&make);
64 release_rotates_authority_from_prior_holder(&make);
65 heartbeat_extends_lease_preventing_lapse(&make);
66 heartbeat_at_expiry_boundary_succeeds(&make);
67 heartbeat_on_lapsed_lease_rejected(&make);
68 heartbeat_unknown_retainer_rejected(&make);
69}
70
71pub const CONTENTION_ROUNDS: usize = 2000;
77
78pub fn run_contention<R, F>(make: F)
91where
92 R: Registry + Send + Sync + 'static,
93 R::Error: Debug + Send,
94 F: Fn(Vec<Pact>, u64) -> R,
95{
96 settle_contention(&make);
97 claim_contention(&make);
98}
99
100fn settle_contention<R, F>(make: &F)
104where
105 R: Registry + Send + Sync + 'static,
106 R::Error: Debug + Send,
107 F: Fn(Vec<Pact>, u64) -> R,
108{
109 use std::sync::Arc;
110
111 for _ in 0..CONTENTION_ROUNDS {
112 let registry = Arc::new(make(vec![a_pact()], LEASE_MILLIS));
113 let retainer = registry
114 .claim(&[DOCKET], at(0))
115 .expect("claim should not error")
116 .expect("a pact should be claimable")
117 .retainer;
118
119 let a = {
120 let registry = Arc::clone(®istry);
121 let retainer = retainer.clone();
122 std::thread::spawn(move || registry.fulfill(&retainer))
123 };
124 let b = {
125 let registry = Arc::clone(®istry);
126 let retainer = retainer.clone();
127 std::thread::spawn(move || registry.fulfill(&retainer))
128 };
129 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
130
131 let winners = [ra.is_ok(), rb.is_ok()]
132 .into_iter()
133 .filter(|&ok| ok)
134 .count();
135 assert_eq!(
136 winners, 1,
137 "a settlement must apply exactly once under contention: a={ra:?} b={rb:?}"
138 );
139 assert!(
140 registry
141 .claim(&[DOCKET], at(0))
142 .expect("claim should not error")
143 .is_none(),
144 "a settled pact must not be claimable again"
145 );
146 }
147}
148
149fn claim_contention<R, F>(make: &F)
153where
154 R: Registry + Send + Sync + 'static,
155 R::Error: Debug + Send,
156 F: Fn(Vec<Pact>, u64) -> R,
157{
158 use std::sync::Arc;
159
160 for _ in 0..CONTENTION_ROUNDS {
161 let registry = Arc::new(make(vec![a_pact()], LEASE_MILLIS));
162 let a = {
163 let registry = Arc::clone(®istry);
164 std::thread::spawn(move || {
165 registry
166 .claim(&[DOCKET], at(0))
167 .expect("claim should not error")
168 .map(|claim| claim.retainer.id())
169 })
170 };
171 let b = {
172 let registry = Arc::clone(®istry);
173 std::thread::spawn(move || {
174 registry
175 .claim(&[DOCKET], at(0))
176 .expect("claim should not error")
177 .map(|claim| claim.retainer.id())
178 })
179 };
180 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
181
182 let claims = [ra, rb].into_iter().flatten().collect::<Vec<_>>();
183 assert_eq!(
184 claims.len(),
185 1,
186 "exactly one worker must claim the single available pact: {claims:?}"
187 );
188 }
189}
190
191#[cfg(feature = "async")]
208mod async_runner {
209 use core::future::Future;
210
211 use pacta_contract::AsyncRegistry;
212 use pacta_contract::{Claim, Pact, Registry, Retainer, Timestamp, Transition};
213
214 pub trait BlockingDriver {
225 fn drive<F: Future>(&self, future: F) -> F::Output;
227 }
228
229 #[derive(Clone, Copy, Debug, Default)]
235 pub struct SelfProgress;
236
237 impl BlockingDriver for SelfProgress {
238 fn drive<F: Future>(&self, future: F) -> F::Output {
239 block_on(future)
240 }
241 }
242
243 fn block_on<F: Future>(future: F) -> F::Output {
246 use core::task::{Context, Poll};
247
248 let mut future = core::pin::pin!(future);
249 let mut cx = Context::from_waker(core::task::Waker::noop());
250 loop {
251 match future.as_mut().poll(&mut cx) {
252 Poll::Ready(output) => return output,
253 Poll::Pending => core::hint::spin_loop(),
254 }
255 }
256 }
257
258 struct BlockOn<R, D> {
264 registry: R,
265 driver: D,
266 }
267
268 impl<R: AsyncRegistry, D: BlockingDriver> Registry for BlockOn<R, D> {
269 type Error = R::Error;
270
271 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
272 self.driver.drive(self.registry.claim(dockets, now))
273 }
274
275 fn lease_millis(&self) -> u64 {
276 self.registry.lease_millis()
277 }
278
279 fn apply(
280 &self,
281 retainer: &Retainer,
282 transition: &Transition<'_>,
283 ) -> Result<(), Self::Error> {
284 self.driver.drive(self.registry.apply(retainer, transition))
285 }
286 }
287
288 pub fn run_async_with<R, F, D>(make: F, driver: D)
297 where
298 R: AsyncRegistry,
299 R::Error: core::fmt::Debug,
300 F: Fn(Vec<Pact>, u64) -> R,
301 D: BlockingDriver + Copy,
302 {
303 crate::run(move |pacts, lease_millis| BlockOn {
304 registry: make(pacts, lease_millis),
305 driver,
306 });
307 }
308
309 pub fn run_async<R, F>(make: F)
317 where
318 R: AsyncRegistry,
319 R::Error: core::fmt::Debug,
320 F: Fn(Vec<Pact>, u64) -> R,
321 {
322 run_async_with(make, SelfProgress);
323 }
324
325 pub fn run_async_contention<R, F>(make: F)
340 where
341 R: AsyncRegistry + Send + Sync + 'static,
342 R::Error: core::fmt::Debug + Send,
343 F: Fn(Vec<Pact>, u64) -> R,
344 {
345 crate::settle_contention(&|pacts, lease| BlockOn {
349 registry: make(pacts, lease),
350 driver: SelfProgress,
351 });
352 crate::claim_contention(&|pacts, lease| BlockOn {
353 registry: make(pacts, lease),
354 driver: SelfProgress,
355 });
356 }
357
358 #[cfg(test)]
365 mod reactor_fixture {
366 use super::*;
367 use core::time::Duration;
368 use pacta_contract::lifecycle::{self, State};
369 use std::sync::Mutex;
370 use uuid::Uuid;
371
372 #[derive(Debug, PartialEq, Eq)]
373 struct NotHeld;
374 impl core::fmt::Display for NotHeld {
375 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
376 f.write_str("not held")
377 }
378 }
379 impl std::error::Error for NotHeld {}
380 impl From<lifecycle::NotCurrentHolder> for NotHeld {
381 fn from(_: lifecycle::NotCurrentHolder) -> Self {
382 NotHeld
383 }
384 }
385
386 struct ReactorBacked {
390 records: Mutex<Vec<(Pact, State)>>,
391 lease_millis: u64,
392 }
393
394 impl ReactorBacked {
395 fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
396 Self {
397 records: Mutex::new(pacts.into_iter().map(|p| (p, State::Available)).collect()),
398 lease_millis,
399 }
400 }
401
402 async fn park() {
403 tokio::time::sleep(Duration::from_millis(1)).await;
406 }
407 }
408
409 impl AsyncRegistry for ReactorBacked {
410 type Error = NotHeld;
411
412 async fn claim(
413 &self,
414 dockets: &[&str],
415 now: Timestamp,
416 ) -> Result<Option<Claim>, NotHeld> {
417 Self::park().await;
418 let mut records = self.records.lock().unwrap();
419 let Some(index) = records.iter().position(|(pact, state)| {
420 dockets.contains(&pact.docket.as_str()) && lifecycle::is_claimable(state, now)
421 }) else {
422 return Ok(None);
423 };
424 let retainer = Retainer::new(Uuid::new_v4());
425 records[index].1 = lifecycle::on_claim(&retainer, now, self.lease_millis);
426 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
427 Ok(Some(Claim::new(records[index].0.clone(), retainer, expiry)))
428 }
429
430 fn lease_millis(&self) -> u64 {
431 self.lease_millis
432 }
433
434 async fn apply(
435 &self,
436 retainer: &Retainer,
437 transition: &Transition<'_>,
438 ) -> Result<(), NotHeld> {
439 Self::park().await;
440 let mut records = self.records.lock().unwrap();
441 let (_, state) = records
442 .iter_mut()
443 .find(|(_, state)| {
444 matches!(state, State::Held { retainer: held, .. } if held == retainer)
445 })
446 .ok_or(NotHeld)?;
447 *state = transition(state)?;
448 Ok(())
449 }
450 }
451
452 #[derive(Clone, Copy)]
453 struct TokioDriver<'a>(&'a tokio::runtime::Runtime);
454 impl BlockingDriver for TokioDriver<'_> {
455 fn drive<F: Future>(&self, future: F) -> F::Output {
456 self.0.block_on(future)
457 }
458 }
459
460 #[test]
461 fn reactor_backed_backend_runs_the_suite_via_run_async_with() {
462 let runtime = tokio::runtime::Builder::new_current_thread()
463 .enable_time()
464 .build()
465 .expect("current-thread runtime with a timer");
466 run_async_with(ReactorBacked::seeded, TokioDriver(&runtime));
469 }
470 }
471}
472
473#[cfg(feature = "async")]
474pub use async_runner::{
475 BlockingDriver, SelfProgress, run_async, run_async_contention, run_async_with,
476};
477
478fn no_available_pact_returns_none<R, F>(make: &F)
479where
480 R: Registry,
481 R::Error: Debug,
482 F: Fn(Vec<Pact>, u64) -> R,
483{
484 let registry = make(Vec::new(), LEASE_MILLIS);
485 assert!(
486 registry
487 .claim(&[DOCKET], at(0))
488 .expect("claim should not error")
489 .is_none(),
490 "an empty registry must yield no claim"
491 );
492}
493
494fn unrequested_docket_is_not_claimed<R, F>(make: &F)
495where
496 R: Registry,
497 R::Error: Debug,
498 F: Fn(Vec<Pact>, u64) -> R,
499{
500 let registry = make(vec![a_pact_on("other")], LEASE_MILLIS);
501 assert!(
502 registry
503 .claim(&[DOCKET], at(0))
504 .expect("claim should not error")
505 .is_none(),
506 "a pact on an unrequested docket must not be claimed"
507 );
508 assert!(
509 registry
510 .claim(&["other"], at(0))
511 .expect("claim should not error")
512 .is_some(),
513 "the same pact must be claimable from its own docket"
514 );
515}
516
517fn claim_returns_claim_with_lease<R, F>(make: &F)
518where
519 R: Registry,
520 R::Error: Debug,
521 F: Fn(Vec<Pact>, u64) -> R,
522{
523 let registry = make(vec![a_pact()], LEASE_MILLIS);
524 let claim = registry
525 .claim(&[DOCKET], at(100))
526 .expect("claim should not error")
527 .expect("a pact should be claimable");
528 assert_eq!(
529 claim.lease_expiry,
530 at(100 + LEASE_MILLIS),
531 "lease expiry must be now plus the lease duration"
532 );
533}
534
535fn held_pact_not_reclaimable_before_expiry<R, F>(make: &F)
536where
537 R: Registry,
538 R::Error: Debug,
539 F: Fn(Vec<Pact>, u64) -> R,
540{
541 let registry = make(vec![a_pact()], LEASE_MILLIS);
542 let _first = registry
543 .claim(&[DOCKET], at(0))
544 .expect("claim should not error")
545 .expect("a pact should be claimable");
546 assert!(
547 registry
548 .claim(&[DOCKET], at(500))
549 .expect("claim should not error")
550 .is_none(),
551 "a held pact must not be reclaimable before its lease expires"
552 );
553}
554
555fn expired_lease_lapses_and_reclaims_with_rotated_retainer<R, F>(make: &F)
556where
557 R: Registry,
558 R::Error: Debug,
559 F: Fn(Vec<Pact>, u64) -> R,
560{
561 let registry = make(vec![a_pact()], LEASE_MILLIS);
562 let first = registry
563 .claim(&[DOCKET], at(0))
564 .expect("claim should not error")
565 .expect("a pact should be claimable");
566 let second = registry
567 .claim(&[DOCKET], at(1500))
568 .expect("claim should not error")
569 .expect("an expired pact should be reclaimable through the claim path");
570 assert_ne!(
571 first.retainer.id(),
572 second.retainer.id(),
573 "reclaiming a lapsed pact must rotate the retainer"
574 );
575 assert_eq!(
576 second.lease_expiry,
577 at(1500 + LEASE_MILLIS),
578 "the reclaim must set a fresh lease"
579 );
580}
581
582fn stale_retainer_settle_rejected_after_reclaim<R, F>(make: &F)
583where
584 R: Registry,
585 R::Error: Debug,
586 F: Fn(Vec<Pact>, u64) -> R,
587{
588 let registry = make(vec![a_pact()], LEASE_MILLIS);
589 let first = registry
590 .claim(&[DOCKET], at(0))
591 .expect("claim should not error")
592 .expect("a pact should be claimable");
593 let _second = registry
594 .claim(&[DOCKET], at(1500))
595 .expect("claim should not error")
596 .expect("an expired pact should be reclaimable");
597 assert!(
598 registry.fulfill(&first.retainer).is_err(),
599 "the prior holder must not settle after a reclaim (at-least-once safety)"
600 );
601}
602
603fn late_fulfill_before_reclaim_succeeds<R, F>(make: &F)
604where
605 R: Registry,
606 R::Error: Debug,
607 F: Fn(Vec<Pact>, u64) -> R,
608{
609 let registry = make(vec![a_pact()], LEASE_MILLIS);
610 let claim = registry
611 .claim(&[DOCKET], at(0))
612 .expect("claim should not error")
613 .expect("a pact should be claimable");
614 assert!(
617 registry.fulfill(&claim.retainer).is_ok(),
618 "a late fulfill before any reclaim must settle"
619 );
620 assert!(
621 registry
622 .claim(&[DOCKET], at(9999))
623 .expect("claim should not error")
624 .is_none(),
625 "a settled pact must not be claimable"
626 );
627}
628
629fn fulfill_settles_and_pact_not_claimable<R, F>(make: &F)
630where
631 R: Registry,
632 R::Error: Debug,
633 F: Fn(Vec<Pact>, u64) -> R,
634{
635 let registry = make(vec![a_pact()], LEASE_MILLIS);
636 let claim = registry
637 .claim(&[DOCKET], at(0))
638 .expect("claim should not error")
639 .expect("a pact should be claimable");
640 registry
641 .fulfill(&claim.retainer)
642 .expect("fulfill should settle");
643 assert!(
644 registry
645 .claim(&[DOCKET], at(0))
646 .expect("claim should not error")
647 .is_none(),
648 "a fulfilled pact must not be claimable"
649 );
650}
651
652fn breach_settles_terminally<R, F>(make: &F)
653where
654 R: Registry,
655 R::Error: Debug,
656 F: Fn(Vec<Pact>, u64) -> R,
657{
658 let registry = make(vec![a_pact()], LEASE_MILLIS);
659 let claim = registry
660 .claim(&[DOCKET], at(0))
661 .expect("claim should not error")
662 .expect("a pact should be claimable");
663 registry
664 .breach(&claim.retainer)
665 .expect("breach should settle");
666 assert!(
667 registry
668 .claim(&[DOCKET], at(5000))
669 .expect("claim should not error")
670 .is_none(),
671 "a breached pact must not be claimable, even after its lease would have expired"
672 );
673}
674
675fn released_pact_withheld_until_reclaimable<R, F>(make: &F)
676where
677 R: Registry,
678 R::Error: Debug,
679 F: Fn(Vec<Pact>, u64) -> R,
680{
681 let registry = make(vec![a_pact()], LEASE_MILLIS);
682 let claim = registry
683 .claim(&[DOCKET], at(0))
684 .expect("claim should not error")
685 .expect("a pact should be claimable");
686 registry
687 .release(&claim.retainer, at(5000))
688 .expect("release should succeed for the current holder");
689 assert!(
692 registry
693 .claim(&[DOCKET], at(3000))
694 .expect("claim should not error")
695 .is_none(),
696 "a released pact must not be claimable before its reclaimable instant"
697 );
698}
699
700fn released_pact_reclaimable_at_its_instant<R, F>(make: &F)
701where
702 R: Registry,
703 R::Error: Debug,
704 F: Fn(Vec<Pact>, u64) -> R,
705{
706 let registry = make(vec![a_pact()], LEASE_MILLIS);
707 let first = registry
708 .claim(&[DOCKET], at(0))
709 .expect("claim should not error")
710 .expect("a pact should be claimable");
711 registry
712 .release(&first.retainer, at(5000))
713 .expect("release should succeed");
714 let second = registry
715 .claim(&[DOCKET], at(5000))
716 .expect("claim should not error")
717 .expect("a released pact must be claimable at its reclaimable instant");
718 assert_ne!(
719 first.retainer.id(),
720 second.retainer.id(),
721 "reclaiming a released pact must rotate the retainer"
722 );
723}
724
725fn immediate_reclaim_reclaims_like_lapse<R, F>(make: &F)
726where
727 R: Registry,
728 R::Error: Debug,
729 F: Fn(Vec<Pact>, u64) -> R,
730{
731 let registry = make(vec![a_pact()], LEASE_MILLIS);
732 let claim = registry
733 .claim(&[DOCKET], at(0))
734 .expect("claim should not error")
735 .expect("a pact should be claimable");
736 registry
737 .release(&claim.retainer, at(0))
738 .expect("release with an immediate reclaim should succeed");
739 assert!(
740 registry
741 .claim(&[DOCKET], at(0))
742 .expect("claim should not error")
743 .is_some(),
744 "an immediate reclaim must make the pact claimable at once, as a voluntary lapse"
745 );
746}
747
748fn release_rotates_authority_from_prior_holder<R, F>(make: &F)
749where
750 R: Registry,
751 R::Error: Debug,
752 F: Fn(Vec<Pact>, u64) -> R,
753{
754 let registry = make(vec![a_pact()], LEASE_MILLIS);
755 let claim = registry
756 .claim(&[DOCKET], at(0))
757 .expect("claim should not error")
758 .expect("a pact should be claimable");
759 registry
760 .release(&claim.retainer, at(0))
761 .expect("release should succeed");
762 assert!(
763 registry.fulfill(&claim.retainer).is_err(),
764 "the prior holder must not settle after releasing (release rotates authority)"
765 );
766}
767
768fn heartbeat_extends_lease_preventing_lapse<R, F>(make: &F)
769where
770 R: Registry,
771 R::Error: Debug,
772 F: Fn(Vec<Pact>, u64) -> R,
773{
774 let registry = make(vec![a_pact()], LEASE_MILLIS);
775 let claim = registry
776 .claim(&[DOCKET], at(0))
777 .expect("claim should not error")
778 .expect("a pact should be claimable");
779 registry
780 .heartbeat(&claim.retainer, at(800))
781 .expect("an in-window heartbeat should extend the lease");
782 assert!(
785 registry
786 .claim(&[DOCKET], at(1500))
787 .expect("claim should not error")
788 .is_none(),
789 "a heartbeat within the window must prevent a lapse"
790 );
791}
792
793fn heartbeat_at_expiry_boundary_succeeds<R, F>(make: &F)
794where
795 R: Registry,
796 R::Error: Debug,
797 F: Fn(Vec<Pact>, u64) -> R,
798{
799 let registry = make(vec![a_pact()], LEASE_MILLIS);
800 let claim = registry
801 .claim(&[DOCKET], at(0))
802 .expect("claim should not error")
803 .expect("a pact should be claimable");
804 registry
808 .heartbeat(&claim.retainer, at(LEASE_MILLIS))
809 .expect("a heartbeat at now == expiry must extend the lease, not be rejected");
810 assert!(
812 registry
813 .claim(&[DOCKET], at(LEASE_MILLIS + LEASE_MILLIS / 2))
814 .expect("claim should not error")
815 .is_none(),
816 "a heartbeat at the expiry boundary must extend the lease and prevent a lapse"
817 );
818}
819
820fn heartbeat_on_lapsed_lease_rejected<R, F>(make: &F)
821where
822 R: Registry,
823 R::Error: Debug,
824 F: Fn(Vec<Pact>, u64) -> R,
825{
826 let registry = make(vec![a_pact()], LEASE_MILLIS);
827 let claim = registry
828 .claim(&[DOCKET], at(0))
829 .expect("claim should not error")
830 .expect("a pact should be claimable");
831 assert!(
832 registry.heartbeat(&claim.retainer, at(1200)).is_err(),
833 "a heartbeat after the lease expired must be rejected, forcing a re-claim"
834 );
835}
836
837fn heartbeat_unknown_retainer_rejected<R, F>(make: &F)
838where
839 R: Registry,
840 R::Error: Debug,
841 F: Fn(Vec<Pact>, u64) -> R,
842{
843 let registry = make(vec![a_pact()], LEASE_MILLIS);
844 let _claim = registry
845 .claim(&[DOCKET], at(0))
846 .expect("claim should not error")
847 .expect("a pact should be claimable");
848 let unknown = Retainer::new(Uuid::new_v4());
849 assert!(
850 registry.heartbeat(&unknown, at(100)).is_err(),
851 "a heartbeat with an unissued retainer must be rejected"
852 );
853}
854
855#[cfg(test)]
856mod contention_guard {
857 use super::{Registry, claim_contention, run_contention, settle_contention};
861 use pacta_contract::lifecycle::{self, State};
862 use pacta_contract::{Claim, Pact, Retainer, Timestamp, Transition};
863 use std::sync::{Barrier, Mutex};
864 use uuid::Uuid;
865
866 #[derive(Debug, PartialEq, Eq)]
867 struct NotHeld;
868 impl std::fmt::Display for NotHeld {
869 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
870 f.write_str("not held")
871 }
872 }
873 impl std::error::Error for NotHeld {}
874 impl From<lifecycle::NotCurrentHolder> for NotHeld {
875 fn from(_: lifecycle::NotCurrentHolder) -> Self {
876 NotHeld
877 }
878 }
879
880 #[derive(Clone, Copy, PartialEq, Eq)]
884 enum Mode {
885 Atomic,
886 BrokenApply,
887 BrokenClaim,
888 }
889
890 struct TestBackend {
896 records: Mutex<Vec<(Pact, State)>>,
897 lease_millis: u64,
898 mode: Mode,
899 barrier: Option<Barrier>,
900 }
901
902 impl TestBackend {
903 fn build(pacts: Vec<Pact>, lease_millis: u64, mode: Mode) -> Self {
904 Self {
905 records: Mutex::new(pacts.into_iter().map(|p| (p, State::Available)).collect()),
906 lease_millis,
907 mode,
908 barrier: (mode != Mode::Atomic).then(|| Barrier::new(2)),
909 }
910 }
911
912 fn atomic(pacts: Vec<Pact>, lease_millis: u64) -> Self {
913 Self::build(pacts, lease_millis, Mode::Atomic)
914 }
915
916 fn broken_apply(pacts: Vec<Pact>, lease_millis: u64) -> Self {
917 Self::build(pacts, lease_millis, Mode::BrokenApply)
918 }
919
920 fn broken_claim(pacts: Vec<Pact>, lease_millis: u64) -> Self {
921 Self::build(pacts, lease_millis, Mode::BrokenClaim)
922 }
923
924 fn held_index(records: &[(Pact, State)], retainer: &Retainer) -> Option<usize> {
925 records
926 .iter()
927 .position(|(_, state)| matches!(state, State::Held { retainer: held, .. } if held == retainer))
928 }
929
930 fn available_index(
931 records: &[(Pact, State)],
932 dockets: &[&str],
933 now: Timestamp,
934 ) -> Option<usize> {
935 records.iter().position(|(pact, state)| {
936 dockets.contains(&pact.docket.as_str()) && lifecycle::is_claimable(state, now)
937 })
938 }
939 }
940
941 impl Registry for TestBackend {
942 type Error = NotHeld;
943
944 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, NotHeld> {
945 let mint = |records: &mut Vec<(Pact, State)>, index: usize| {
946 let retainer = Retainer::new(Uuid::new_v4());
947 records[index].1 = lifecycle::on_claim(&retainer, now, self.lease_millis);
948 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
949 Claim::new(records[index].0.clone(), retainer, expiry)
950 };
951 match &self.barrier {
952 Some(barrier) if self.mode == Mode::BrokenClaim => {
955 let index = {
956 let records = self.records.lock().unwrap();
957 Self::available_index(&records, dockets, now)
958 };
959 let Some(index) = index else { return Ok(None) };
960 barrier.wait();
961 let mut records = self.records.lock().unwrap();
962 Ok(Some(mint(&mut records, index)))
963 }
964 _ => {
966 let mut records = self.records.lock().unwrap();
967 let Some(index) = Self::available_index(&records, dockets, now) else {
968 return Ok(None);
969 };
970 Ok(Some(mint(&mut records, index)))
971 }
972 }
973 }
974
975 fn lease_millis(&self) -> u64 {
976 self.lease_millis
977 }
978
979 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), NotHeld> {
980 match &self.barrier {
981 Some(barrier) if self.mode == Mode::BrokenApply => {
984 let (index, state) = {
985 let records = self.records.lock().unwrap();
986 let index = Self::held_index(&records, retainer).ok_or(NotHeld)?;
987 (index, records[index].1.clone())
988 };
989 barrier.wait();
990 let next = transition(&state)?;
991 let mut records = self.records.lock().unwrap();
992 records[index].1 = next;
993 Ok(())
994 }
995 _ => {
997 let mut records = self.records.lock().unwrap();
998 let index = Self::held_index(&records, retainer).ok_or(NotHeld)?;
999 records[index].1 = transition(&records[index].1)?;
1000 Ok(())
1001 }
1002 }
1003 }
1004 }
1005
1006 fn panicked(body: impl FnOnce() + std::panic::UnwindSafe) -> bool {
1009 let previous = std::panic::take_hook();
1010 std::panic::set_hook(Box::new(|_| {}));
1011 let result = std::panic::catch_unwind(body);
1012 std::panic::set_hook(previous);
1013 result.is_err()
1014 }
1015
1016 #[test]
1017 fn harness_catches_a_non_atomic_apply() {
1018 assert!(
1019 panicked(|| settle_contention(&TestBackend::broken_apply)),
1020 "the settlement-contention check must fail against a non-atomic apply"
1021 );
1022 }
1023
1024 #[test]
1025 fn harness_catches_a_non_atomic_claim() {
1026 assert!(
1027 panicked(|| claim_contention(&TestBackend::broken_claim)),
1028 "the claim-contention check must fail against a non-atomic claim (double-issue)"
1029 );
1030 }
1031
1032 #[test]
1033 fn harness_passes_a_fully_atomic_backend() {
1034 run_contention(TestBackend::atomic);
1037 }
1038}