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 use std::sync::Arc;
346
347 for _ in 0..crate::CONTENTION_ROUNDS {
349 let reg = Arc::new(make(vec![crate::a_pact()], crate::LEASE_MILLIS));
350 let retainer = block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
351 .expect("claim should not error")
352 .expect("a pact should be claimable")
353 .retainer;
354
355 let a = {
356 let reg = Arc::clone(®);
357 let retainer = retainer.clone();
358 std::thread::spawn(move || block_on(reg.fulfill(&retainer)))
359 };
360 let b = {
361 let reg = Arc::clone(®);
362 let retainer = retainer.clone();
363 std::thread::spawn(move || block_on(reg.fulfill(&retainer)))
364 };
365 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
366
367 let winners = [ra.is_ok(), rb.is_ok()]
368 .into_iter()
369 .filter(|&ok| ok)
370 .count();
371 assert_eq!(
372 winners, 1,
373 "settlement must apply exactly once: a={ra:?} b={rb:?}"
374 );
375 assert!(
376 block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
377 .expect("claim should not error")
378 .is_none(),
379 "a settled pact must not be claimable again"
380 );
381 }
382
383 for _ in 0..crate::CONTENTION_ROUNDS {
385 let reg = Arc::new(make(vec![crate::a_pact()], crate::LEASE_MILLIS));
386 let a = {
387 let reg = Arc::clone(®);
388 std::thread::spawn(move || {
389 block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
390 .expect("claim should not error")
391 .map(|claim| claim.retainer.id())
392 })
393 };
394 let b = {
395 let reg = Arc::clone(®);
396 std::thread::spawn(move || {
397 block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
398 .expect("claim should not error")
399 .map(|claim| claim.retainer.id())
400 })
401 };
402 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
403
404 let claims = [ra, rb].into_iter().flatten().collect::<Vec<_>>();
405 assert_eq!(
406 claims.len(),
407 1,
408 "exactly one worker must claim the single available pact: {claims:?}"
409 );
410 }
411 }
412
413 #[cfg(test)]
420 mod reactor_fixture {
421 use super::*;
422 use core::time::Duration;
423 use pacta_contract::lifecycle::{self, State};
424 use std::sync::Mutex;
425 use uuid::Uuid;
426
427 #[derive(Debug, PartialEq, Eq)]
428 struct NotHeld;
429 impl core::fmt::Display for NotHeld {
430 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
431 f.write_str("not held")
432 }
433 }
434 impl std::error::Error for NotHeld {}
435 impl From<lifecycle::NotCurrentHolder> for NotHeld {
436 fn from(_: lifecycle::NotCurrentHolder) -> Self {
437 NotHeld
438 }
439 }
440
441 struct ReactorBacked {
445 records: Mutex<Vec<(Pact, State)>>,
446 lease_millis: u64,
447 }
448
449 impl ReactorBacked {
450 fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
451 Self {
452 records: Mutex::new(pacts.into_iter().map(|p| (p, State::Available)).collect()),
453 lease_millis,
454 }
455 }
456
457 async fn park() {
458 tokio::time::sleep(Duration::from_millis(1)).await;
461 }
462 }
463
464 impl AsyncRegistry for ReactorBacked {
465 type Error = NotHeld;
466
467 async fn claim(
468 &self,
469 dockets: &[&str],
470 now: Timestamp,
471 ) -> Result<Option<Claim>, NotHeld> {
472 Self::park().await;
473 let mut records = self.records.lock().unwrap();
474 let Some(index) = records.iter().position(|(pact, state)| {
475 dockets.contains(&pact.docket.as_str()) && lifecycle::is_claimable(state, now)
476 }) else {
477 return Ok(None);
478 };
479 let retainer = Retainer::new(Uuid::new_v4());
480 records[index].1 = lifecycle::on_claim(&retainer, now, self.lease_millis);
481 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
482 Ok(Some(Claim::new(records[index].0.clone(), retainer, expiry)))
483 }
484
485 fn lease_millis(&self) -> u64 {
486 self.lease_millis
487 }
488
489 async fn apply(
490 &self,
491 retainer: &Retainer,
492 transition: &Transition<'_>,
493 ) -> Result<(), NotHeld> {
494 Self::park().await;
495 let mut records = self.records.lock().unwrap();
496 let (_, state) = records
497 .iter_mut()
498 .find(|(_, state)| {
499 matches!(state, State::Held { retainer: held, .. } if held == retainer)
500 })
501 .ok_or(NotHeld)?;
502 *state = transition(state)?;
503 Ok(())
504 }
505 }
506
507 #[derive(Clone, Copy)]
508 struct TokioDriver<'a>(&'a tokio::runtime::Runtime);
509 impl BlockingDriver for TokioDriver<'_> {
510 fn drive<F: Future>(&self, future: F) -> F::Output {
511 self.0.block_on(future)
512 }
513 }
514
515 #[test]
516 fn reactor_backed_backend_runs_the_suite_via_run_async_with() {
517 let runtime = tokio::runtime::Builder::new_current_thread()
518 .enable_time()
519 .build()
520 .expect("current-thread runtime with a timer");
521 run_async_with(ReactorBacked::seeded, TokioDriver(&runtime));
524 }
525 }
526}
527
528#[cfg(feature = "async")]
529pub use async_runner::{
530 BlockingDriver, SelfProgress, run_async, run_async_contention, run_async_with,
531};
532
533fn no_available_pact_returns_none<R, F>(make: &F)
534where
535 R: Registry,
536 R::Error: Debug,
537 F: Fn(Vec<Pact>, u64) -> R,
538{
539 let registry = make(Vec::new(), LEASE_MILLIS);
540 assert!(
541 registry
542 .claim(&[DOCKET], at(0))
543 .expect("claim should not error")
544 .is_none(),
545 "an empty registry must yield no claim"
546 );
547}
548
549fn unrequested_docket_is_not_claimed<R, F>(make: &F)
550where
551 R: Registry,
552 R::Error: Debug,
553 F: Fn(Vec<Pact>, u64) -> R,
554{
555 let registry = make(vec![a_pact_on("other")], LEASE_MILLIS);
556 assert!(
557 registry
558 .claim(&[DOCKET], at(0))
559 .expect("claim should not error")
560 .is_none(),
561 "a pact on an unrequested docket must not be claimed"
562 );
563 assert!(
564 registry
565 .claim(&["other"], at(0))
566 .expect("claim should not error")
567 .is_some(),
568 "the same pact must be claimable from its own docket"
569 );
570}
571
572fn claim_returns_claim_with_lease<R, F>(make: &F)
573where
574 R: Registry,
575 R::Error: Debug,
576 F: Fn(Vec<Pact>, u64) -> R,
577{
578 let registry = make(vec![a_pact()], LEASE_MILLIS);
579 let claim = registry
580 .claim(&[DOCKET], at(100))
581 .expect("claim should not error")
582 .expect("a pact should be claimable");
583 assert_eq!(
584 claim.lease_expiry,
585 at(100 + LEASE_MILLIS),
586 "lease expiry must be now plus the lease duration"
587 );
588}
589
590fn held_pact_not_reclaimable_before_expiry<R, F>(make: &F)
591where
592 R: Registry,
593 R::Error: Debug,
594 F: Fn(Vec<Pact>, u64) -> R,
595{
596 let registry = make(vec![a_pact()], LEASE_MILLIS);
597 let _first = registry
598 .claim(&[DOCKET], at(0))
599 .expect("claim should not error")
600 .expect("a pact should be claimable");
601 assert!(
602 registry
603 .claim(&[DOCKET], at(500))
604 .expect("claim should not error")
605 .is_none(),
606 "a held pact must not be reclaimable before its lease expires"
607 );
608}
609
610fn expired_lease_lapses_and_reclaims_with_rotated_retainer<R, F>(make: &F)
611where
612 R: Registry,
613 R::Error: Debug,
614 F: Fn(Vec<Pact>, u64) -> R,
615{
616 let registry = make(vec![a_pact()], LEASE_MILLIS);
617 let first = registry
618 .claim(&[DOCKET], at(0))
619 .expect("claim should not error")
620 .expect("a pact should be claimable");
621 let second = registry
622 .claim(&[DOCKET], at(1500))
623 .expect("claim should not error")
624 .expect("an expired pact should be reclaimable through the claim path");
625 assert_ne!(
626 first.retainer.id(),
627 second.retainer.id(),
628 "reclaiming a lapsed pact must rotate the retainer"
629 );
630 assert_eq!(
631 second.lease_expiry,
632 at(1500 + LEASE_MILLIS),
633 "the reclaim must set a fresh lease"
634 );
635}
636
637fn stale_retainer_settle_rejected_after_reclaim<R, F>(make: &F)
638where
639 R: Registry,
640 R::Error: Debug,
641 F: Fn(Vec<Pact>, u64) -> R,
642{
643 let registry = make(vec![a_pact()], LEASE_MILLIS);
644 let first = registry
645 .claim(&[DOCKET], at(0))
646 .expect("claim should not error")
647 .expect("a pact should be claimable");
648 let _second = registry
649 .claim(&[DOCKET], at(1500))
650 .expect("claim should not error")
651 .expect("an expired pact should be reclaimable");
652 assert!(
653 registry.fulfill(&first.retainer).is_err(),
654 "the prior holder must not settle after a reclaim (at-least-once safety)"
655 );
656}
657
658fn late_fulfill_before_reclaim_succeeds<R, F>(make: &F)
659where
660 R: Registry,
661 R::Error: Debug,
662 F: Fn(Vec<Pact>, u64) -> R,
663{
664 let registry = make(vec![a_pact()], LEASE_MILLIS);
665 let claim = registry
666 .claim(&[DOCKET], at(0))
667 .expect("claim should not error")
668 .expect("a pact should be claimable");
669 assert!(
672 registry.fulfill(&claim.retainer).is_ok(),
673 "a late fulfill before any reclaim must settle"
674 );
675 assert!(
676 registry
677 .claim(&[DOCKET], at(9999))
678 .expect("claim should not error")
679 .is_none(),
680 "a settled pact must not be claimable"
681 );
682}
683
684fn fulfill_settles_and_pact_not_claimable<R, F>(make: &F)
685where
686 R: Registry,
687 R::Error: Debug,
688 F: Fn(Vec<Pact>, u64) -> R,
689{
690 let registry = make(vec![a_pact()], LEASE_MILLIS);
691 let claim = registry
692 .claim(&[DOCKET], at(0))
693 .expect("claim should not error")
694 .expect("a pact should be claimable");
695 registry
696 .fulfill(&claim.retainer)
697 .expect("fulfill should settle");
698 assert!(
699 registry
700 .claim(&[DOCKET], at(0))
701 .expect("claim should not error")
702 .is_none(),
703 "a fulfilled pact must not be claimable"
704 );
705}
706
707fn breach_settles_terminally<R, F>(make: &F)
708where
709 R: Registry,
710 R::Error: Debug,
711 F: Fn(Vec<Pact>, u64) -> R,
712{
713 let registry = make(vec![a_pact()], LEASE_MILLIS);
714 let claim = registry
715 .claim(&[DOCKET], at(0))
716 .expect("claim should not error")
717 .expect("a pact should be claimable");
718 registry
719 .breach(&claim.retainer)
720 .expect("breach should settle");
721 assert!(
722 registry
723 .claim(&[DOCKET], at(5000))
724 .expect("claim should not error")
725 .is_none(),
726 "a breached pact must not be claimable, even after its lease would have expired"
727 );
728}
729
730fn released_pact_withheld_until_reclaimable<R, F>(make: &F)
731where
732 R: Registry,
733 R::Error: Debug,
734 F: Fn(Vec<Pact>, u64) -> R,
735{
736 let registry = make(vec![a_pact()], LEASE_MILLIS);
737 let claim = registry
738 .claim(&[DOCKET], at(0))
739 .expect("claim should not error")
740 .expect("a pact should be claimable");
741 registry
742 .release(&claim.retainer, at(5000))
743 .expect("release should succeed for the current holder");
744 assert!(
747 registry
748 .claim(&[DOCKET], at(3000))
749 .expect("claim should not error")
750 .is_none(),
751 "a released pact must not be claimable before its reclaimable instant"
752 );
753}
754
755fn released_pact_reclaimable_at_its_instant<R, F>(make: &F)
756where
757 R: Registry,
758 R::Error: Debug,
759 F: Fn(Vec<Pact>, u64) -> R,
760{
761 let registry = make(vec![a_pact()], LEASE_MILLIS);
762 let first = registry
763 .claim(&[DOCKET], at(0))
764 .expect("claim should not error")
765 .expect("a pact should be claimable");
766 registry
767 .release(&first.retainer, at(5000))
768 .expect("release should succeed");
769 let second = registry
770 .claim(&[DOCKET], at(5000))
771 .expect("claim should not error")
772 .expect("a released pact must be claimable at its reclaimable instant");
773 assert_ne!(
774 first.retainer.id(),
775 second.retainer.id(),
776 "reclaiming a released pact must rotate the retainer"
777 );
778}
779
780fn immediate_reclaim_reclaims_like_lapse<R, F>(make: &F)
781where
782 R: Registry,
783 R::Error: Debug,
784 F: Fn(Vec<Pact>, u64) -> R,
785{
786 let registry = make(vec![a_pact()], LEASE_MILLIS);
787 let claim = registry
788 .claim(&[DOCKET], at(0))
789 .expect("claim should not error")
790 .expect("a pact should be claimable");
791 registry
792 .release(&claim.retainer, at(0))
793 .expect("release with an immediate reclaim should succeed");
794 assert!(
795 registry
796 .claim(&[DOCKET], at(0))
797 .expect("claim should not error")
798 .is_some(),
799 "an immediate reclaim must make the pact claimable at once, as a voluntary lapse"
800 );
801}
802
803fn release_rotates_authority_from_prior_holder<R, F>(make: &F)
804where
805 R: Registry,
806 R::Error: Debug,
807 F: Fn(Vec<Pact>, u64) -> R,
808{
809 let registry = make(vec![a_pact()], LEASE_MILLIS);
810 let claim = registry
811 .claim(&[DOCKET], at(0))
812 .expect("claim should not error")
813 .expect("a pact should be claimable");
814 registry
815 .release(&claim.retainer, at(0))
816 .expect("release should succeed");
817 assert!(
818 registry.fulfill(&claim.retainer).is_err(),
819 "the prior holder must not settle after releasing (release rotates authority)"
820 );
821}
822
823fn heartbeat_extends_lease_preventing_lapse<R, F>(make: &F)
824where
825 R: Registry,
826 R::Error: Debug,
827 F: Fn(Vec<Pact>, u64) -> R,
828{
829 let registry = make(vec![a_pact()], LEASE_MILLIS);
830 let claim = registry
831 .claim(&[DOCKET], at(0))
832 .expect("claim should not error")
833 .expect("a pact should be claimable");
834 registry
835 .heartbeat(&claim.retainer, at(800))
836 .expect("an in-window heartbeat should extend the lease");
837 assert!(
840 registry
841 .claim(&[DOCKET], at(1500))
842 .expect("claim should not error")
843 .is_none(),
844 "a heartbeat within the window must prevent a lapse"
845 );
846}
847
848fn heartbeat_at_expiry_boundary_succeeds<R, F>(make: &F)
849where
850 R: Registry,
851 R::Error: Debug,
852 F: Fn(Vec<Pact>, u64) -> R,
853{
854 let registry = make(vec![a_pact()], LEASE_MILLIS);
855 let claim = registry
856 .claim(&[DOCKET], at(0))
857 .expect("claim should not error")
858 .expect("a pact should be claimable");
859 registry
863 .heartbeat(&claim.retainer, at(LEASE_MILLIS))
864 .expect("a heartbeat at now == expiry must extend the lease, not be rejected");
865 assert!(
867 registry
868 .claim(&[DOCKET], at(LEASE_MILLIS + LEASE_MILLIS / 2))
869 .expect("claim should not error")
870 .is_none(),
871 "a heartbeat at the expiry boundary must extend the lease and prevent a lapse"
872 );
873}
874
875fn heartbeat_on_lapsed_lease_rejected<R, F>(make: &F)
876where
877 R: Registry,
878 R::Error: Debug,
879 F: Fn(Vec<Pact>, u64) -> R,
880{
881 let registry = make(vec![a_pact()], LEASE_MILLIS);
882 let claim = registry
883 .claim(&[DOCKET], at(0))
884 .expect("claim should not error")
885 .expect("a pact should be claimable");
886 assert!(
887 registry.heartbeat(&claim.retainer, at(1200)).is_err(),
888 "a heartbeat after the lease expired must be rejected, forcing a re-claim"
889 );
890}
891
892fn heartbeat_unknown_retainer_rejected<R, F>(make: &F)
893where
894 R: Registry,
895 R::Error: Debug,
896 F: Fn(Vec<Pact>, u64) -> R,
897{
898 let registry = make(vec![a_pact()], LEASE_MILLIS);
899 let _claim = registry
900 .claim(&[DOCKET], at(0))
901 .expect("claim should not error")
902 .expect("a pact should be claimable");
903 let unknown = Retainer::new(Uuid::new_v4());
904 assert!(
905 registry.heartbeat(&unknown, at(100)).is_err(),
906 "a heartbeat with an unissued retainer must be rejected"
907 );
908}
909
910#[cfg(test)]
911mod contention_guard {
912 use super::{Registry, claim_contention, run_contention, settle_contention};
916 use pacta_contract::lifecycle::{self, State};
917 use pacta_contract::{Claim, Pact, Retainer, Timestamp, Transition};
918 use std::sync::{Barrier, Mutex};
919 use uuid::Uuid;
920
921 #[derive(Debug, PartialEq, Eq)]
922 struct NotHeld;
923 impl std::fmt::Display for NotHeld {
924 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
925 f.write_str("not held")
926 }
927 }
928 impl std::error::Error for NotHeld {}
929 impl From<lifecycle::NotCurrentHolder> for NotHeld {
930 fn from(_: lifecycle::NotCurrentHolder) -> Self {
931 NotHeld
932 }
933 }
934
935 #[derive(Clone, Copy, PartialEq, Eq)]
939 enum Mode {
940 Atomic,
941 BrokenApply,
942 BrokenClaim,
943 }
944
945 struct TestBackend {
951 records: Mutex<Vec<(Pact, State)>>,
952 lease_millis: u64,
953 mode: Mode,
954 barrier: Option<Barrier>,
955 }
956
957 impl TestBackend {
958 fn build(pacts: Vec<Pact>, lease_millis: u64, mode: Mode) -> Self {
959 Self {
960 records: Mutex::new(pacts.into_iter().map(|p| (p, State::Available)).collect()),
961 lease_millis,
962 mode,
963 barrier: (mode != Mode::Atomic).then(|| Barrier::new(2)),
964 }
965 }
966
967 fn atomic(pacts: Vec<Pact>, lease_millis: u64) -> Self {
968 Self::build(pacts, lease_millis, Mode::Atomic)
969 }
970
971 fn broken_apply(pacts: Vec<Pact>, lease_millis: u64) -> Self {
972 Self::build(pacts, lease_millis, Mode::BrokenApply)
973 }
974
975 fn broken_claim(pacts: Vec<Pact>, lease_millis: u64) -> Self {
976 Self::build(pacts, lease_millis, Mode::BrokenClaim)
977 }
978
979 fn held_index(records: &[(Pact, State)], retainer: &Retainer) -> Option<usize> {
980 records
981 .iter()
982 .position(|(_, state)| matches!(state, State::Held { retainer: held, .. } if held == retainer))
983 }
984
985 fn available_index(
986 records: &[(Pact, State)],
987 dockets: &[&str],
988 now: Timestamp,
989 ) -> Option<usize> {
990 records.iter().position(|(pact, state)| {
991 dockets.contains(&pact.docket.as_str()) && lifecycle::is_claimable(state, now)
992 })
993 }
994 }
995
996 impl Registry for TestBackend {
997 type Error = NotHeld;
998
999 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, NotHeld> {
1000 let mint = |records: &mut Vec<(Pact, State)>, index: usize| {
1001 let retainer = Retainer::new(Uuid::new_v4());
1002 records[index].1 = lifecycle::on_claim(&retainer, now, self.lease_millis);
1003 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
1004 Claim::new(records[index].0.clone(), retainer, expiry)
1005 };
1006 match &self.barrier {
1007 Some(barrier) if self.mode == Mode::BrokenClaim => {
1010 let index = {
1011 let records = self.records.lock().unwrap();
1012 Self::available_index(&records, dockets, now)
1013 };
1014 let Some(index) = index else { return Ok(None) };
1015 barrier.wait();
1016 let mut records = self.records.lock().unwrap();
1017 Ok(Some(mint(&mut records, index)))
1018 }
1019 _ => {
1021 let mut records = self.records.lock().unwrap();
1022 let Some(index) = Self::available_index(&records, dockets, now) else {
1023 return Ok(None);
1024 };
1025 Ok(Some(mint(&mut records, index)))
1026 }
1027 }
1028 }
1029
1030 fn lease_millis(&self) -> u64 {
1031 self.lease_millis
1032 }
1033
1034 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), NotHeld> {
1035 match &self.barrier {
1036 Some(barrier) if self.mode == Mode::BrokenApply => {
1039 let (index, state) = {
1040 let records = self.records.lock().unwrap();
1041 let index = Self::held_index(&records, retainer).ok_or(NotHeld)?;
1042 (index, records[index].1.clone())
1043 };
1044 barrier.wait();
1045 let next = transition(&state)?;
1046 let mut records = self.records.lock().unwrap();
1047 records[index].1 = next;
1048 Ok(())
1049 }
1050 _ => {
1052 let mut records = self.records.lock().unwrap();
1053 let index = Self::held_index(&records, retainer).ok_or(NotHeld)?;
1054 records[index].1 = transition(&records[index].1)?;
1055 Ok(())
1056 }
1057 }
1058 }
1059 }
1060
1061 fn panicked(body: impl FnOnce() + std::panic::UnwindSafe) -> bool {
1064 let previous = std::panic::take_hook();
1065 std::panic::set_hook(Box::new(|_| {}));
1066 let result = std::panic::catch_unwind(body);
1067 std::panic::set_hook(previous);
1068 result.is_err()
1069 }
1070
1071 #[test]
1072 fn harness_catches_a_non_atomic_apply() {
1073 assert!(
1074 panicked(|| settle_contention(&TestBackend::broken_apply)),
1075 "the settlement-contention check must fail against a non-atomic apply"
1076 );
1077 }
1078
1079 #[test]
1080 fn harness_catches_a_non_atomic_claim() {
1081 assert!(
1082 panicked(|| claim_contention(&TestBackend::broken_claim)),
1083 "the claim-contention check must fail against a non-atomic claim (double-issue)"
1084 );
1085 }
1086
1087 #[test]
1088 fn harness_passes_a_fully_atomic_backend() {
1089 run_contention(TestBackend::atomic);
1092 }
1093}