1use creusot_std::prelude::ensures;
4#[cfg(creusot)]
5use creusot_std::prelude::{DeepModel, Int, invariant};
6
7#[krabka_macros::model_type(runtime(Debug, Clone, Copy, PartialEq, Eq))]
9pub enum ShareOffsetMutationDecision {
10 NotCoordinator,
11 NonEmptyGroup,
12 Unrequested,
13 FencedLeaderEpoch,
14 StateEpochOverflow,
15 ExactRetry,
16 Apply { next_state_epoch: i32 },
17}
18
19#[krabka_macros::model_type(runtime(Debug, Clone, Copy, PartialEq, Eq))]
22pub enum ShareOffsetMutationGate {
23 NotCoordinator,
24 NonEmptyGroup,
25 Unrequested,
26 Admissible { exact_retry: bool },
27}
28
29#[must_use]
36#[ensures(match result {
37 ShareOffsetMutationDecision::Apply { next_state_epoch } => {
38 gate == ShareOffsetMutationGate::Admissible { exact_retry: false }
39 && observed_leader_epoch@ == current_leader_epoch@
40 && state_epoch@ < i32::MAX@
41 && next_state_epoch@ == state_epoch@ + 1
42 }
43 ShareOffsetMutationDecision::NotCoordinator => {
44 gate == ShareOffsetMutationGate::NotCoordinator
45 }
46 ShareOffsetMutationDecision::NonEmptyGroup => {
47 gate == ShareOffsetMutationGate::NonEmptyGroup
48 }
49 ShareOffsetMutationDecision::Unrequested => gate == ShareOffsetMutationGate::Unrequested,
50 ShareOffsetMutationDecision::FencedLeaderEpoch => {
51 (exists<retry: bool> gate == ShareOffsetMutationGate::Admissible { exact_retry: retry })
52 && observed_leader_epoch@ != current_leader_epoch@
53 }
54 ShareOffsetMutationDecision::StateEpochOverflow => {
55 gate == ShareOffsetMutationGate::Admissible { exact_retry: false }
56 && observed_leader_epoch@ == current_leader_epoch@
57 && state_epoch@ == i32::MAX@
58 }
59 ShareOffsetMutationDecision::ExactRetry => {
60 gate == ShareOffsetMutationGate::Admissible { exact_retry: true }
61 && observed_leader_epoch@ == current_leader_epoch@
62 }
63})]
64pub const fn share_offset_mutation_decision(
65 gate: ShareOffsetMutationGate,
66 observed_leader_epoch: i32,
67 current_leader_epoch: i32,
68 state_epoch: i32,
69) -> ShareOffsetMutationDecision {
70 let exact_retry = match gate {
71 ShareOffsetMutationGate::NotCoordinator => {
72 return ShareOffsetMutationDecision::NotCoordinator;
73 }
74 ShareOffsetMutationGate::NonEmptyGroup => {
75 return ShareOffsetMutationDecision::NonEmptyGroup;
76 }
77 ShareOffsetMutationGate::Unrequested => {
78 return ShareOffsetMutationDecision::Unrequested;
79 }
80 ShareOffsetMutationGate::Admissible { exact_retry } => exact_retry,
81 };
82 if observed_leader_epoch != current_leader_epoch {
83 return ShareOffsetMutationDecision::FencedLeaderEpoch;
84 }
85 if exact_retry {
86 return ShareOffsetMutationDecision::ExactRetry;
87 }
88 match state_epoch.checked_add(1) {
89 Some(next_state_epoch) => ShareOffsetMutationDecision::Apply { next_state_epoch },
90 None => ShareOffsetMutationDecision::StateEpochOverflow,
91 }
92}
93
94#[ensures(match result {
96 Some(frontier) => [email protected]() > 0
97 && (forall<i: Int> 0 <= i && i < [email protected]()
98 ==> frontier@ <= offsets@[i]@)
99 && (exists<i: Int> 0 <= i && i < [email protected]()
100 && frontier@ == offsets@[i]@),
101 None => [email protected]() == 0,
102})]
103#[must_use]
104pub fn share_prune_frontier(offsets: &[i64]) -> Option<i64> {
105 let &first = offsets.first()?;
106 let mut frontier = first;
107 let mut i = 1usize;
108 #[invariant(1 <= i@ && i@ <= [email protected]())]
109 #[invariant(forall<k: Int> 0 <= k && k < i@ ==> frontier@ <= offsets@[k]@)]
110 #[invariant(exists<k: Int> 0 <= k && k < i@ && frontier@ == offsets@[k]@)]
111 #[variant([email protected]() - i@)]
112 while i < offsets.len() {
113 if let Some(&offset) = offsets.get(i)
114 && offset < frontier
115 {
116 frontier = offset;
117 }
118 i += 1;
119 }
120 Some(frontier)
121}
122
123#[cfg(test)]
124mod tests {
125 use super::{
126 ShareOffsetMutationDecision, ShareOffsetMutationGate, share_offset_mutation_decision,
127 share_prune_frontier,
128 };
129
130 #[yare::parameterized(
131 not_coordinator = {
132 ShareOffsetMutationGate::NotCoordinator,
133 3,
134 3,
135 7,
136 ShareOffsetMutationDecision::NotCoordinator,
137 },
138 nonempty_group = {
139 ShareOffsetMutationGate::NonEmptyGroup,
140 3,
141 3,
142 7,
143 ShareOffsetMutationDecision::NonEmptyGroup,
144 },
145 unrequested = {
146 ShareOffsetMutationGate::Unrequested,
147 3,
148 3,
149 7,
150 ShareOffsetMutationDecision::Unrequested,
151 },
152 fenced_epoch = {
153 ShareOffsetMutationGate::Admissible { exact_retry: false },
154 2,
155 3,
156 7,
157 ShareOffsetMutationDecision::FencedLeaderEpoch,
158 },
159 state_epoch_overflow = {
160 ShareOffsetMutationGate::Admissible { exact_retry: false },
161 3,
162 3,
163 i32::MAX,
164 ShareOffsetMutationDecision::StateEpochOverflow,
165 },
166 exact_retry = {
167 ShareOffsetMutationGate::Admissible { exact_retry: true },
168 3,
169 3,
170 7,
171 ShareOffsetMutationDecision::ExactRetry,
172 },
173 apply = {
174 ShareOffsetMutationGate::Admissible { exact_retry: false },
175 3,
176 3,
177 7,
178 ShareOffsetMutationDecision::Apply { next_state_epoch: 8 },
179 },
180 )]
181 fn offset_mutation_fails_closed_and_advances_exactly_once(
182 gate: ShareOffsetMutationGate,
183 observed: i32,
184 current: i32,
185 state: i32,
186 expected: ShareOffsetMutationDecision,
187 ) {
188 assert2::check!(share_offset_mutation_decision(gate, observed, current, state) == expected);
189 }
190
191 #[yare::parameterized(
192 empty = { &[][..], None },
193 single = { &[42][..], Some(42) },
194 duplicate_minimum = { &[100, 30, 75, 30, 200][..], Some(30) },
195 minimum_at_start = { &[0, 5, 9][..], Some(0) },
196 minimum_integer = { &[i64::MAX, i64::MIN, 0][..], Some(i64::MIN) },
197 )]
198 fn prune_frontier_covers_empty_single_ties_and_missing_snapshots(
199 offsets: &[i64],
200 expected: Option<i64>,
201 ) {
202 assert2::check!(share_prune_frontier(offsets) == expected);
203 }
204}