1#[derive(Clone, Copy, Debug)]
5#[cfg_attr(kani, derive(kani::Arbitrary))]
6pub struct Policy {
7 pub limit: u64,
9 pub delay: u64,
11 pub maximum_delay: u64,
13}
14#[derive(Clone, Copy, Debug, Default)]
16#[cfg_attr(kani, derive(kani::Arbitrary))]
17pub struct State {
18 pub attempts: u64,
20 pub next_attempt: u64,
22}
23impl State {
24 pub fn allowed(&self, policy: Policy, now: u64) -> bool {
26 self.attempts < policy.limit && now >= self.next_attempt
27 }
28 pub fn reserve(&mut self, policy: Policy, now: u64) {
31 self.attempts = self.attempts.saturating_add(1);
32 let multiplier = 1u64 << self.attempts.min(63);
33 self.next_attempt = now.saturating_add(
34 policy
35 .delay
36 .saturating_mul(multiplier)
37 .min(policy.maximum_delay),
38 );
39 }
40 pub fn exhaust(&mut self, policy: Policy) {
42 self.attempts = policy.limit;
43 }
44 pub fn next(&self, policy: Policy) -> Option<u64> {
46 (self.attempts < policy.limit).then_some(self.next_attempt)
47 }
48}
49
50#[cfg(kani)]
53mod proofs {
54 use super::*;
55
56 #[kani::proof]
59 fn a_reservation_waits_no_longer_than_the_maximum_delay() {
60 let policy: Policy = kani::any();
61 let mut state: State = kani::any();
62 let now: u64 = kani::any();
63 let attempts = state.attempts;
64 state.reserve(policy, now);
65 assert!(state.attempts == attempts.saturating_add(1));
66 assert!(now <= state.next_attempt);
67 assert!(state.next_attempt <= now.saturating_add(policy.maximum_delay));
68 }
69
70 #[kani::proof]
78 fn backoff_never_shrinks_as_attempts_grow() {
79 let policy: Policy = kani::any();
80 kani::assume(policy.delay <= u64::from(u16::MAX));
81 let now: u64 = kani::any();
82 let mut earlier = State {
83 attempts: kani::any(),
84 next_attempt: 0,
85 };
86 let mut later = State {
87 attempts: kani::any(),
88 next_attempt: 0,
89 };
90 kani::assume(earlier.attempts <= later.attempts);
91 earlier.reserve(policy, now);
92 later.reserve(policy, now);
93 assert!(earlier.next_attempt <= later.next_attempt);
94 let floor = policy.delay.min(policy.maximum_delay);
95 assert!(earlier.next_attempt >= now.saturating_add(floor));
96 }
97
98 #[kani::proof]
101 fn a_campaign_never_exceeds_its_attempt_limit() {
102 let policy: Policy = kani::any();
103 let mut state: State = kani::any();
104 let now: u64 = kani::any();
105 let allowed = state.allowed(policy, now);
107 assert!(allowed == state.next(policy).is_some_and(|at| now >= at));
108 if allowed {
109 state.reserve(policy, now);
110 assert!(state.attempts <= policy.limit);
111 }
112 state.exhaust(policy);
113 assert!(!state.allowed(policy, kani::any()));
114 assert!(state.next(policy).is_none());
115 }
116}