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)
45where
46 R: Registry,
47 R::Error: Debug,
48 F: Fn(Vec<Pact>, u64) -> R,
49{
50 no_available_pact_returns_none(&make);
51 unrequested_docket_is_not_claimed(&make);
52 claim_returns_claim_with_lease(&make);
53 held_pact_not_reclaimable_before_expiry(&make);
54 expired_lease_lapses_and_reclaims_with_rotated_retainer(&make);
55 stale_retainer_settle_rejected_after_reclaim(&make);
56 late_fulfill_before_reclaim_succeeds(&make);
57 fulfill_settles_and_pact_not_claimable(&make);
58 breach_settles_terminally(&make);
59 released_pact_withheld_until_reclaimable(&make);
60 released_pact_reclaimable_at_its_instant(&make);
61 immediate_reclaim_reclaims_like_lapse(&make);
62 release_rotates_authority_from_prior_holder(&make);
63 heartbeat_extends_lease_preventing_lapse(&make);
64 heartbeat_at_expiry_boundary_succeeds(&make);
65 heartbeat_on_lapsed_lease_rejected(&make);
66 heartbeat_unknown_retainer_rejected(&make);
67}
68
69pub const CONTENTION_ROUNDS: usize = 2000;
75
76pub fn run_contention<R, F>(make: F)
86where
87 R: Registry + Send + Sync + 'static,
88 R::Error: Debug + Send,
89 F: Fn(Vec<Pact>, u64) -> R,
90{
91 settle_contention(&make);
92 claim_contention(&make);
93}
94
95fn settle_contention<R, F>(make: &F)
99where
100 R: Registry + Send + Sync + 'static,
101 R::Error: Debug + Send,
102 F: Fn(Vec<Pact>, u64) -> R,
103{
104 use std::sync::Arc;
105
106 for _ in 0..CONTENTION_ROUNDS {
107 let registry = Arc::new(make(vec![a_pact()], LEASE_MILLIS));
108 let retainer = registry
109 .claim(&[DOCKET], at(0))
110 .expect("claim should not error")
111 .expect("a pact should be claimable")
112 .retainer;
113
114 let a = {
115 let registry = Arc::clone(®istry);
116 let retainer = retainer.clone();
117 std::thread::spawn(move || registry.fulfill(&retainer))
118 };
119 let b = {
120 let registry = Arc::clone(®istry);
121 let retainer = retainer.clone();
122 std::thread::spawn(move || registry.fulfill(&retainer))
123 };
124 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
125
126 let winners = [ra.is_ok(), rb.is_ok()]
127 .into_iter()
128 .filter(|&ok| ok)
129 .count();
130 assert_eq!(
131 winners, 1,
132 "a settlement must apply exactly once under contention: a={ra:?} b={rb:?}"
133 );
134 assert!(
135 registry
136 .claim(&[DOCKET], at(0))
137 .expect("claim should not error")
138 .is_none(),
139 "a settled pact must not be claimable again"
140 );
141 }
142}
143
144fn claim_contention<R, F>(make: &F)
148where
149 R: Registry + Send + Sync + 'static,
150 R::Error: Debug + Send,
151 F: Fn(Vec<Pact>, u64) -> R,
152{
153 use std::sync::Arc;
154
155 for _ in 0..CONTENTION_ROUNDS {
156 let registry = Arc::new(make(vec![a_pact()], LEASE_MILLIS));
157 let a = {
158 let registry = Arc::clone(®istry);
159 std::thread::spawn(move || {
160 registry
161 .claim(&[DOCKET], at(0))
162 .expect("claim should not error")
163 .map(|claim| claim.retainer.id())
164 })
165 };
166 let b = {
167 let registry = Arc::clone(®istry);
168 std::thread::spawn(move || {
169 registry
170 .claim(&[DOCKET], at(0))
171 .expect("claim should not error")
172 .map(|claim| claim.retainer.id())
173 })
174 };
175 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
176
177 let claims = [ra, rb].into_iter().flatten().collect::<Vec<_>>();
178 assert_eq!(
179 claims.len(),
180 1,
181 "exactly one worker must claim the single available pact: {claims:?}"
182 );
183 }
184}
185
186#[cfg(feature = "async")]
201mod async_runner {
202 use core::future::Future;
203
204 use pacta_contract::AsyncRegistry;
205 use pacta_contract::{Claim, Pact, Registry, Retainer, Timestamp, Transition};
206
207 pub trait BlockingDriver {
216 fn drive<F: Future>(&self, future: F) -> F::Output;
218 }
219
220 #[derive(Clone, Copy, Debug, Default)]
226 pub struct SelfProgress;
227
228 impl BlockingDriver for SelfProgress {
229 fn drive<F: Future>(&self, future: F) -> F::Output {
230 block_on(future)
231 }
232 }
233
234 fn block_on<F: Future>(future: F) -> F::Output {
237 use core::task::{Context, Poll};
238
239 let mut future = core::pin::pin!(future);
240 let mut cx = Context::from_waker(core::task::Waker::noop());
241 loop {
242 match future.as_mut().poll(&mut cx) {
243 Poll::Ready(output) => return output,
244 Poll::Pending => core::hint::spin_loop(),
245 }
246 }
247 }
248
249 struct BlockOn<R, D> {
255 registry: R,
256 driver: D,
257 }
258
259 impl<R: AsyncRegistry, D: BlockingDriver + Send + Sync> Registry for BlockOn<R, D> {
260 type Error = R::Error;
261
262 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
263 self.driver.drive(self.registry.claim(dockets, now))
264 }
265
266 fn lease_millis(&self) -> u64 {
267 self.registry.lease_millis()
268 }
269
270 fn apply(
271 &self,
272 retainer: &Retainer,
273 transition: &Transition<'_>,
274 ) -> Result<(), Self::Error> {
275 self.driver.drive(self.registry.apply(retainer, transition))
276 }
277 }
278
279 pub fn run_async_with<R, F, D>(make: F, driver: D)
287 where
288 R: AsyncRegistry,
289 R::Error: core::fmt::Debug,
290 F: Fn(Vec<Pact>, u64) -> R,
291 D: BlockingDriver + Copy + Send + Sync,
292 {
293 crate::run(move |pacts, lease_millis| BlockOn {
294 registry: make(pacts, lease_millis),
295 driver,
296 });
297 }
298
299 pub fn run_async<R, F>(make: F)
307 where
308 R: AsyncRegistry,
309 R::Error: core::fmt::Debug,
310 F: Fn(Vec<Pact>, u64) -> R,
311 {
312 run_async_with(make, SelfProgress);
313 }
314
315 pub fn run_async_contention<R, F>(make: F)
329 where
330 R: AsyncRegistry + 'static,
331 R::Error: core::fmt::Debug + Send,
332 F: Fn(Vec<Pact>, u64) -> R,
333 {
334 use std::sync::Arc;
335
336 for _ in 0..crate::CONTENTION_ROUNDS {
338 let reg = Arc::new(make(vec![crate::a_pact()], crate::LEASE_MILLIS));
339 let retainer = block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
340 .expect("claim should not error")
341 .expect("a pact should be claimable")
342 .retainer;
343
344 let a = {
345 let reg = Arc::clone(®);
346 let retainer = retainer.clone();
347 std::thread::spawn(move || block_on(reg.fulfill(&retainer)))
348 };
349 let b = {
350 let reg = Arc::clone(®);
351 let retainer = retainer.clone();
352 std::thread::spawn(move || block_on(reg.fulfill(&retainer)))
353 };
354 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
355
356 let winners = [ra.is_ok(), rb.is_ok()]
357 .into_iter()
358 .filter(|&ok| ok)
359 .count();
360 assert_eq!(
361 winners, 1,
362 "settlement must apply exactly once: a={ra:?} b={rb:?}"
363 );
364 assert!(
365 block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
366 .expect("claim should not error")
367 .is_none(),
368 "a settled pact must not be claimable again"
369 );
370 }
371
372 for _ in 0..crate::CONTENTION_ROUNDS {
374 let reg = Arc::new(make(vec![crate::a_pact()], crate::LEASE_MILLIS));
375 let a = {
376 let reg = Arc::clone(®);
377 std::thread::spawn(move || {
378 block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
379 .expect("claim should not error")
380 .map(|claim| claim.retainer.id())
381 })
382 };
383 let b = {
384 let reg = Arc::clone(®);
385 std::thread::spawn(move || {
386 block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
387 .expect("claim should not error")
388 .map(|claim| claim.retainer.id())
389 })
390 };
391 let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
392
393 let claims = [ra, rb].into_iter().flatten().collect::<Vec<_>>();
394 assert_eq!(
395 claims.len(),
396 1,
397 "exactly one worker must claim the single available pact: {claims:?}"
398 );
399 }
400 }
401
402 #[cfg(test)]
409 mod reactor_fixture {
410 use super::*;
411 use core::time::Duration;
412 use pacta_contract::lifecycle::{self, State};
413 use std::sync::Mutex;
414 use uuid::Uuid;
415
416 #[derive(Debug, PartialEq, Eq)]
417 struct NotHeld;
418 impl core::fmt::Display for NotHeld {
419 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
420 f.write_str("not held")
421 }
422 }
423 impl std::error::Error for NotHeld {}
424 impl From<lifecycle::NotCurrentHolder> for NotHeld {
425 fn from(_: lifecycle::NotCurrentHolder) -> Self {
426 NotHeld
427 }
428 }
429
430 struct ReactorBacked {
434 records: Mutex<Vec<(Pact, State)>>,
435 lease_millis: u64,
436 }
437
438 impl ReactorBacked {
439 fn seeded(pacts: Vec<Pact>, lease_millis: u64) -> Self {
440 Self {
441 records: Mutex::new(pacts.into_iter().map(|p| (p, State::Available)).collect()),
442 lease_millis,
443 }
444 }
445
446 async fn park() {
447 tokio::time::sleep(Duration::from_millis(1)).await;
450 }
451 }
452
453 impl AsyncRegistry for ReactorBacked {
454 type Error = NotHeld;
455
456 async fn claim(
457 &self,
458 dockets: &[&str],
459 now: Timestamp,
460 ) -> Result<Option<Claim>, NotHeld> {
461 Self::park().await;
462 let mut records = self.records.lock().unwrap();
463 let Some(index) = records.iter().position(|(pact, state)| {
464 dockets.contains(&pact.docket.as_str()) && lifecycle::is_claimable(state, now)
465 }) else {
466 return Ok(None);
467 };
468 let retainer = Retainer::new(Uuid::new_v4());
469 records[index].1 = lifecycle::on_claim(&retainer, now, self.lease_millis);
470 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
471 Ok(Some(Claim::new(records[index].0.clone(), retainer, expiry)))
472 }
473
474 fn lease_millis(&self) -> u64 {
475 self.lease_millis
476 }
477
478 async fn apply(
479 &self,
480 retainer: &Retainer,
481 transition: &Transition<'_>,
482 ) -> Result<(), NotHeld> {
483 Self::park().await;
484 let mut records = self.records.lock().unwrap();
485 let (_, state) = records
486 .iter_mut()
487 .find(|(_, state)| {
488 matches!(state, State::Held { retainer: held, .. } if held == retainer)
489 })
490 .ok_or(NotHeld)?;
491 *state = transition(state)?;
492 Ok(())
493 }
494 }
495
496 #[derive(Clone, Copy)]
497 struct TokioDriver<'a>(&'a tokio::runtime::Runtime);
498 impl BlockingDriver for TokioDriver<'_> {
499 fn drive<F: Future>(&self, future: F) -> F::Output {
500 self.0.block_on(future)
501 }
502 }
503
504 #[test]
505 fn reactor_backed_backend_runs_the_suite_via_run_async_with() {
506 let runtime = tokio::runtime::Builder::new_current_thread()
507 .enable_time()
508 .build()
509 .expect("current-thread runtime with a timer");
510 run_async_with(ReactorBacked::seeded, TokioDriver(&runtime));
513 }
514 }
515}
516
517#[cfg(feature = "async")]
518pub use async_runner::{
519 BlockingDriver, SelfProgress, run_async, run_async_contention, run_async_with,
520};
521
522fn no_available_pact_returns_none<R, F>(make: &F)
523where
524 R: Registry,
525 R::Error: Debug,
526 F: Fn(Vec<Pact>, u64) -> R,
527{
528 let registry = make(Vec::new(), LEASE_MILLIS);
529 assert!(
530 registry
531 .claim(&[DOCKET], at(0))
532 .expect("claim should not error")
533 .is_none(),
534 "an empty registry must yield no claim"
535 );
536}
537
538fn unrequested_docket_is_not_claimed<R, F>(make: &F)
539where
540 R: Registry,
541 R::Error: Debug,
542 F: Fn(Vec<Pact>, u64) -> R,
543{
544 let registry = make(vec![a_pact_on("other")], LEASE_MILLIS);
545 assert!(
546 registry
547 .claim(&[DOCKET], at(0))
548 .expect("claim should not error")
549 .is_none(),
550 "a pact on an unrequested docket must not be claimed"
551 );
552 assert!(
553 registry
554 .claim(&["other"], at(0))
555 .expect("claim should not error")
556 .is_some(),
557 "the same pact must be claimable from its own docket"
558 );
559}
560
561fn claim_returns_claim_with_lease<R, F>(make: &F)
562where
563 R: Registry,
564 R::Error: Debug,
565 F: Fn(Vec<Pact>, u64) -> R,
566{
567 let registry = make(vec![a_pact()], LEASE_MILLIS);
568 let claim = registry
569 .claim(&[DOCKET], at(100))
570 .expect("claim should not error")
571 .expect("a pact should be claimable");
572 assert_eq!(
573 claim.lease_expiry,
574 at(100 + LEASE_MILLIS),
575 "lease expiry must be now plus the lease duration"
576 );
577}
578
579fn held_pact_not_reclaimable_before_expiry<R, F>(make: &F)
580where
581 R: Registry,
582 R::Error: Debug,
583 F: Fn(Vec<Pact>, u64) -> R,
584{
585 let registry = make(vec![a_pact()], LEASE_MILLIS);
586 let _first = registry
587 .claim(&[DOCKET], at(0))
588 .expect("claim should not error")
589 .expect("a pact should be claimable");
590 assert!(
591 registry
592 .claim(&[DOCKET], at(500))
593 .expect("claim should not error")
594 .is_none(),
595 "a held pact must not be reclaimable before its lease expires"
596 );
597}
598
599fn expired_lease_lapses_and_reclaims_with_rotated_retainer<R, F>(make: &F)
600where
601 R: Registry,
602 R::Error: Debug,
603 F: Fn(Vec<Pact>, u64) -> R,
604{
605 let registry = make(vec![a_pact()], LEASE_MILLIS);
606 let first = registry
607 .claim(&[DOCKET], at(0))
608 .expect("claim should not error")
609 .expect("a pact should be claimable");
610 let second = registry
611 .claim(&[DOCKET], at(1500))
612 .expect("claim should not error")
613 .expect("an expired pact should be reclaimable through the claim path");
614 assert_ne!(
615 first.retainer.id(),
616 second.retainer.id(),
617 "reclaiming a lapsed pact must rotate the retainer"
618 );
619 assert_eq!(
620 second.lease_expiry,
621 at(1500 + LEASE_MILLIS),
622 "the reclaim must set a fresh lease"
623 );
624}
625
626fn stale_retainer_settle_rejected_after_reclaim<R, F>(make: &F)
627where
628 R: Registry,
629 R::Error: Debug,
630 F: Fn(Vec<Pact>, u64) -> R,
631{
632 let registry = make(vec![a_pact()], LEASE_MILLIS);
633 let first = registry
634 .claim(&[DOCKET], at(0))
635 .expect("claim should not error")
636 .expect("a pact should be claimable");
637 let _second = registry
638 .claim(&[DOCKET], at(1500))
639 .expect("claim should not error")
640 .expect("an expired pact should be reclaimable");
641 assert!(
642 registry.fulfill(&first.retainer).is_err(),
643 "the prior holder must not settle after a reclaim (at-least-once safety)"
644 );
645}
646
647fn late_fulfill_before_reclaim_succeeds<R, F>(make: &F)
648where
649 R: Registry,
650 R::Error: Debug,
651 F: Fn(Vec<Pact>, u64) -> R,
652{
653 let registry = make(vec![a_pact()], LEASE_MILLIS);
654 let claim = registry
655 .claim(&[DOCKET], at(0))
656 .expect("claim should not error")
657 .expect("a pact should be claimable");
658 assert!(
661 registry.fulfill(&claim.retainer).is_ok(),
662 "a late fulfill before any reclaim must settle"
663 );
664 assert!(
665 registry
666 .claim(&[DOCKET], at(9999))
667 .expect("claim should not error")
668 .is_none(),
669 "a settled pact must not be claimable"
670 );
671}
672
673fn fulfill_settles_and_pact_not_claimable<R, F>(make: &F)
674where
675 R: Registry,
676 R::Error: Debug,
677 F: Fn(Vec<Pact>, u64) -> R,
678{
679 let registry = make(vec![a_pact()], LEASE_MILLIS);
680 let claim = registry
681 .claim(&[DOCKET], at(0))
682 .expect("claim should not error")
683 .expect("a pact should be claimable");
684 registry
685 .fulfill(&claim.retainer)
686 .expect("fulfill should settle");
687 assert!(
688 registry
689 .claim(&[DOCKET], at(0))
690 .expect("claim should not error")
691 .is_none(),
692 "a fulfilled pact must not be claimable"
693 );
694}
695
696fn breach_settles_terminally<R, F>(make: &F)
697where
698 R: Registry,
699 R::Error: Debug,
700 F: Fn(Vec<Pact>, u64) -> R,
701{
702 let registry = make(vec![a_pact()], LEASE_MILLIS);
703 let claim = registry
704 .claim(&[DOCKET], at(0))
705 .expect("claim should not error")
706 .expect("a pact should be claimable");
707 registry
708 .breach(&claim.retainer)
709 .expect("breach should settle");
710 assert!(
711 registry
712 .claim(&[DOCKET], at(5000))
713 .expect("claim should not error")
714 .is_none(),
715 "a breached pact must not be claimable, even after its lease would have expired"
716 );
717}
718
719fn released_pact_withheld_until_reclaimable<R, F>(make: &F)
720where
721 R: Registry,
722 R::Error: Debug,
723 F: Fn(Vec<Pact>, u64) -> R,
724{
725 let registry = make(vec![a_pact()], LEASE_MILLIS);
726 let claim = registry
727 .claim(&[DOCKET], at(0))
728 .expect("claim should not error")
729 .expect("a pact should be claimable");
730 registry
731 .release(&claim.retainer, at(5000))
732 .expect("release should succeed for the current holder");
733 assert!(
736 registry
737 .claim(&[DOCKET], at(3000))
738 .expect("claim should not error")
739 .is_none(),
740 "a released pact must not be claimable before its reclaimable instant"
741 );
742}
743
744fn released_pact_reclaimable_at_its_instant<R, F>(make: &F)
745where
746 R: Registry,
747 R::Error: Debug,
748 F: Fn(Vec<Pact>, u64) -> R,
749{
750 let registry = make(vec![a_pact()], LEASE_MILLIS);
751 let first = registry
752 .claim(&[DOCKET], at(0))
753 .expect("claim should not error")
754 .expect("a pact should be claimable");
755 registry
756 .release(&first.retainer, at(5000))
757 .expect("release should succeed");
758 let second = registry
759 .claim(&[DOCKET], at(5000))
760 .expect("claim should not error")
761 .expect("a released pact must be claimable at its reclaimable instant");
762 assert_ne!(
763 first.retainer.id(),
764 second.retainer.id(),
765 "reclaiming a released pact must rotate the retainer"
766 );
767}
768
769fn immediate_reclaim_reclaims_like_lapse<R, F>(make: &F)
770where
771 R: Registry,
772 R::Error: Debug,
773 F: Fn(Vec<Pact>, u64) -> R,
774{
775 let registry = make(vec![a_pact()], LEASE_MILLIS);
776 let claim = registry
777 .claim(&[DOCKET], at(0))
778 .expect("claim should not error")
779 .expect("a pact should be claimable");
780 registry
781 .release(&claim.retainer, at(0))
782 .expect("release with an immediate reclaim should succeed");
783 assert!(
784 registry
785 .claim(&[DOCKET], at(0))
786 .expect("claim should not error")
787 .is_some(),
788 "an immediate reclaim must make the pact claimable at once, as a voluntary lapse"
789 );
790}
791
792fn release_rotates_authority_from_prior_holder<R, F>(make: &F)
793where
794 R: Registry,
795 R::Error: Debug,
796 F: Fn(Vec<Pact>, u64) -> R,
797{
798 let registry = make(vec![a_pact()], LEASE_MILLIS);
799 let claim = registry
800 .claim(&[DOCKET], at(0))
801 .expect("claim should not error")
802 .expect("a pact should be claimable");
803 registry
804 .release(&claim.retainer, at(0))
805 .expect("release should succeed");
806 assert!(
807 registry.fulfill(&claim.retainer).is_err(),
808 "the prior holder must not settle after releasing (release rotates authority)"
809 );
810}
811
812fn heartbeat_extends_lease_preventing_lapse<R, F>(make: &F)
813where
814 R: Registry,
815 R::Error: Debug,
816 F: Fn(Vec<Pact>, u64) -> R,
817{
818 let registry = make(vec![a_pact()], LEASE_MILLIS);
819 let claim = registry
820 .claim(&[DOCKET], at(0))
821 .expect("claim should not error")
822 .expect("a pact should be claimable");
823 registry
824 .heartbeat(&claim.retainer, at(800))
825 .expect("an in-window heartbeat should extend the lease");
826 assert!(
829 registry
830 .claim(&[DOCKET], at(1500))
831 .expect("claim should not error")
832 .is_none(),
833 "a heartbeat within the window must prevent a lapse"
834 );
835}
836
837fn heartbeat_at_expiry_boundary_succeeds<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 registry
852 .heartbeat(&claim.retainer, at(LEASE_MILLIS))
853 .expect("a heartbeat at now == expiry must extend the lease, not be rejected");
854 assert!(
856 registry
857 .claim(&[DOCKET], at(LEASE_MILLIS + LEASE_MILLIS / 2))
858 .expect("claim should not error")
859 .is_none(),
860 "a heartbeat at the expiry boundary must extend the lease and prevent a lapse"
861 );
862}
863
864fn heartbeat_on_lapsed_lease_rejected<R, F>(make: &F)
865where
866 R: Registry,
867 R::Error: Debug,
868 F: Fn(Vec<Pact>, u64) -> R,
869{
870 let registry = make(vec![a_pact()], LEASE_MILLIS);
871 let claim = registry
872 .claim(&[DOCKET], at(0))
873 .expect("claim should not error")
874 .expect("a pact should be claimable");
875 assert!(
876 registry.heartbeat(&claim.retainer, at(1200)).is_err(),
877 "a heartbeat after the lease expired must be rejected, forcing a re-claim"
878 );
879}
880
881fn heartbeat_unknown_retainer_rejected<R, F>(make: &F)
882where
883 R: Registry,
884 R::Error: Debug,
885 F: Fn(Vec<Pact>, u64) -> R,
886{
887 let registry = make(vec![a_pact()], LEASE_MILLIS);
888 let _claim = registry
889 .claim(&[DOCKET], at(0))
890 .expect("claim should not error")
891 .expect("a pact should be claimable");
892 let unknown = Retainer::new(Uuid::new_v4());
893 assert!(
894 registry.heartbeat(&unknown, at(100)).is_err(),
895 "a heartbeat with an unissued retainer must be rejected"
896 );
897}
898
899#[cfg(test)]
900mod contention_guard {
901 use super::{Registry, claim_contention, run_contention, settle_contention};
905 use pacta_contract::lifecycle::{self, State};
906 use pacta_contract::{Claim, Pact, Retainer, Timestamp, Transition};
907 use std::sync::{Barrier, Mutex};
908 use uuid::Uuid;
909
910 #[derive(Debug, PartialEq, Eq)]
911 struct NotHeld;
912 impl std::fmt::Display for NotHeld {
913 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
914 f.write_str("not held")
915 }
916 }
917 impl std::error::Error for NotHeld {}
918 impl From<lifecycle::NotCurrentHolder> for NotHeld {
919 fn from(_: lifecycle::NotCurrentHolder) -> Self {
920 NotHeld
921 }
922 }
923
924 #[derive(Clone, Copy, PartialEq, Eq)]
928 enum Mode {
929 Atomic,
930 BrokenApply,
931 BrokenClaim,
932 }
933
934 struct TestBackend {
940 records: Mutex<Vec<(Pact, State)>>,
941 lease_millis: u64,
942 mode: Mode,
943 barrier: Option<Barrier>,
944 }
945
946 impl TestBackend {
947 fn build(pacts: Vec<Pact>, lease_millis: u64, mode: Mode) -> Self {
948 Self {
949 records: Mutex::new(pacts.into_iter().map(|p| (p, State::Available)).collect()),
950 lease_millis,
951 mode,
952 barrier: (mode != Mode::Atomic).then(|| Barrier::new(2)),
953 }
954 }
955
956 fn atomic(pacts: Vec<Pact>, lease_millis: u64) -> Self {
957 Self::build(pacts, lease_millis, Mode::Atomic)
958 }
959
960 fn broken_apply(pacts: Vec<Pact>, lease_millis: u64) -> Self {
961 Self::build(pacts, lease_millis, Mode::BrokenApply)
962 }
963
964 fn broken_claim(pacts: Vec<Pact>, lease_millis: u64) -> Self {
965 Self::build(pacts, lease_millis, Mode::BrokenClaim)
966 }
967
968 fn held_index(records: &[(Pact, State)], retainer: &Retainer) -> Option<usize> {
969 records
970 .iter()
971 .position(|(_, state)| matches!(state, State::Held { retainer: held, .. } if held == retainer))
972 }
973
974 fn available_index(
975 records: &[(Pact, State)],
976 dockets: &[&str],
977 now: Timestamp,
978 ) -> Option<usize> {
979 records.iter().position(|(pact, state)| {
980 dockets.contains(&pact.docket.as_str()) && lifecycle::is_claimable(state, now)
981 })
982 }
983 }
984
985 impl Registry for TestBackend {
986 type Error = NotHeld;
987
988 fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, NotHeld> {
989 let mint = |records: &mut Vec<(Pact, State)>, index: usize| {
990 let retainer = Retainer::new(Uuid::new_v4());
991 records[index].1 = lifecycle::on_claim(&retainer, now, self.lease_millis);
992 let expiry = lifecycle::lease_expiry(now, self.lease_millis);
993 Claim::new(records[index].0.clone(), retainer, expiry)
994 };
995 match &self.barrier {
996 Some(barrier) if self.mode == Mode::BrokenClaim => {
999 let index = {
1000 let records = self.records.lock().unwrap();
1001 Self::available_index(&records, dockets, now)
1002 };
1003 let Some(index) = index else { return Ok(None) };
1004 barrier.wait();
1005 let mut records = self.records.lock().unwrap();
1006 Ok(Some(mint(&mut records, index)))
1007 }
1008 _ => {
1010 let mut records = self.records.lock().unwrap();
1011 let Some(index) = Self::available_index(&records, dockets, now) else {
1012 return Ok(None);
1013 };
1014 Ok(Some(mint(&mut records, index)))
1015 }
1016 }
1017 }
1018
1019 fn lease_millis(&self) -> u64 {
1020 self.lease_millis
1021 }
1022
1023 fn apply(&self, retainer: &Retainer, transition: &Transition<'_>) -> Result<(), NotHeld> {
1024 match &self.barrier {
1025 Some(barrier) if self.mode == Mode::BrokenApply => {
1028 let (index, state) = {
1029 let records = self.records.lock().unwrap();
1030 let index = Self::held_index(&records, retainer).ok_or(NotHeld)?;
1031 (index, records[index].1.clone())
1032 };
1033 barrier.wait();
1034 let next = transition(&state)?;
1035 let mut records = self.records.lock().unwrap();
1036 records[index].1 = next;
1037 Ok(())
1038 }
1039 _ => {
1041 let mut records = self.records.lock().unwrap();
1042 let index = Self::held_index(&records, retainer).ok_or(NotHeld)?;
1043 records[index].1 = transition(&records[index].1)?;
1044 Ok(())
1045 }
1046 }
1047 }
1048 }
1049
1050 fn panicked(body: impl FnOnce() + std::panic::UnwindSafe) -> bool {
1053 let previous = std::panic::take_hook();
1054 std::panic::set_hook(Box::new(|_| {}));
1055 let result = std::panic::catch_unwind(body);
1056 std::panic::set_hook(previous);
1057 result.is_err()
1058 }
1059
1060 #[test]
1061 fn harness_catches_a_non_atomic_apply() {
1062 assert!(
1063 panicked(|| settle_contention(&TestBackend::broken_apply)),
1064 "the settlement-contention check must fail against a non-atomic apply"
1065 );
1066 }
1067
1068 #[test]
1069 fn harness_catches_a_non_atomic_claim() {
1070 assert!(
1071 panicked(|| claim_contention(&TestBackend::broken_claim)),
1072 "the claim-contention check must fail against a non-atomic claim (double-issue)"
1073 );
1074 }
1075
1076 #[test]
1077 fn harness_passes_a_fully_atomic_backend() {
1078 run_contention(TestBackend::atomic);
1081 }
1082}