1use {
10 creusot_std::prelude::*,
11 krabka_ids::{LeaderEpoch, Offset},
12};
13
14const UNDEFINED_EPOCH: LeaderEpoch = LeaderEpoch(-1);
16const UNDEFINED_EPOCH_OFFSET: Offset = Offset(-1);
18
19#[cfg_attr(not(creusot), derive(Debug, Clone, Copy, PartialEq, Eq, Hash))]
21pub struct EpochEntry {
22 pub epoch: LeaderEpoch,
23 pub start_offset: Offset,
24}
25
26#[cfg(creusot)]
28#[cfg_attr(test, mutants::skip)]
29#[logic(open)]
30pub fn higher_entry_at(entries: Seq<EpochEntry>, requested: Int, h: Int) -> bool {
35 pearlite! {
36 0 <= h && h < entries.len()
37 && entries[h].epoch.0@ > requested
38 && forall<j: Int> 0 <= j && j < h ==> entries[j].epoch.0@ <= requested
39 }
40}
41
42#[cfg(creusot)]
44#[cfg_attr(test, mutants::skip)]
45#[logic(open)]
46pub fn no_higher_entry(entries: Seq<EpochEntry>, requested: Int) -> bool {
49 pearlite! {
50 forall<i: Int> 0 <= i && i < entries.len() ==> entries[i].epoch.0@ <= requested
51 }
52}
53
54#[cfg(creusot)]
56#[cfg_attr(test, mutants::skip)]
57#[logic(open)]
58pub fn kafka_end_offset_for(
73 entries: Seq<EpochEntry>,
74 requested: Int,
75 log_end: Int,
76 found: Int,
77 end: Int,
78) -> bool {
79 pearlite! {
80 if requested == -1 {
81 found == -1 && end == -1
82 } else if entries.len() > 0 && entries[entries.len() - 1].epoch.0@ == requested {
83 found == requested && end == log_end
84 } else if no_higher_entry(entries, requested) {
85 found == -1 && end == -1
86 } else {
87 exists<h: Int> higher_entry_at(entries, requested, h)
88 && end == entries[h].start_offset.0@
89 && found == if h == 0 { requested } else { entries[h - 1].epoch.0@ }
90 }
91 }
92}
93
94#[requires(forall<i: Int, j: Int> 0 <= i && i < j && j < [email protected]()
106 ==> entries@[i].epoch.0@ < entries@[j].epoch.0@
107 && entries@[i].start_offset.0@ < entries@[j].start_offset.0@)]
108#[ensures(kafka_end_offset_for(
109 entries@, requested_epoch.0@, log_end_offset.0@, result.0.0@, result.1.0@))]
110#[must_use]
111pub fn epoch_and_offset_for_entries(
112 entries: &[EpochEntry],
113 requested_epoch: LeaderEpoch,
114 log_end_offset: Offset,
115) -> (LeaderEpoch, Offset) {
116 if requested_epoch.0 == UNDEFINED_EPOCH.0 {
117 return (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET);
118 }
119
120 let mut i = 0usize;
122 #[invariant(i@ <= [email protected]())]
123 #[invariant(forall<j: Int> 0 <= j && j < i@
124 ==> entries@[j].epoch.0@ <= requested_epoch.0@)]
125 #[variant([email protected]() - i@)]
126 while i < entries.len()
127 && matches!(entries.get(i), Some(entry) if entry.epoch.0 <= requested_epoch.0)
128 {
129 i += 1;
130 }
131
132 if i == entries.len() {
133 if i > 0 && matches!(entries.get(i - 1), Some(last) if last.epoch.0 == requested_epoch.0) {
134 return (requested_epoch, log_end_offset);
135 }
136 return (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET);
137 }
138 proof_assert!(higher_entry_at(entries@, requested_epoch.0@, i@));
139 proof_assert!(entries@[[email protected]() - 1].epoch.0@ >= entries@[i@].epoch.0@);
140 let Some(higher) = entries.get(i) else {
141 return (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET);
142 };
143 if i == 0 {
144 return (requested_epoch, higher.start_offset);
145 }
146 let Some(previous) = entries.get(i - 1) else {
147 return (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET);
148 };
149 (previous.epoch, higher.start_offset)
150}
151
152#[cfg(test)]
153mod tests {
154 use {
155 super::*,
156 proptest::{collection, prelude::*},
157 std::collections::BTreeMap,
158 };
159
160 fn tree_map_oracle(
163 entries: &[EpochEntry],
164 requested_epoch: LeaderEpoch,
165 log_end_offset: Offset,
166 ) -> (LeaderEpoch, Offset) {
167 let epochs: BTreeMap<i32, EpochEntry> = entries.iter().map(|e| (e.epoch.0, *e)).collect();
168 if requested_epoch == UNDEFINED_EPOCH {
169 return (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET);
170 }
171 if epochs.last_key_value().map(|(epoch, _)| *epoch) == Some(requested_epoch.0) {
172 return (requested_epoch, log_end_offset);
173 }
174 let higher = epochs
175 .range(requested_epoch.0.saturating_add(1)..)
176 .next()
177 .map(|(_, entry)| *entry);
178 let floor = epochs
179 .range(..=requested_epoch.0)
180 .next_back()
181 .map(|(_, entry)| *entry);
182 match (higher, floor) {
183 (None, _) => (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET),
184 (Some(higher), None) => (requested_epoch, higher.start_offset),
185 (Some(higher), Some(floor)) => (floor.epoch, higher.start_offset),
186 }
187 }
188
189 const fn entry(epoch: i32, start_offset: i64) -> EpochEntry {
190 EpochEntry {
191 epoch: LeaderEpoch(epoch),
192 start_offset: Offset(start_offset),
193 }
194 }
195
196 #[yare::parameterized(
198 empty_cache = { "empty cache", &[][..], 0, 9, (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET) },
199 undefined_requested_empty = {
200 "undefined requested epoch, empty cache",
201 &[][..],
202 -1,
203 9,
204 (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET),
205 },
206 undefined_requested = {
207 "undefined requested epoch",
208 &[entry(0, 0), entry(2, 50), entry(5, 100)][..],
209 -1,
210 200,
211 (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET),
212 },
213 latest_epoch = {
214 "latest epoch answers the log end offset",
215 &[entry(0, 0), entry(2, 50), entry(5, 100)][..],
216 5,
217 200,
218 (LeaderEpoch(5), Offset(200)),
219 },
220 above_latest = {
221 "requested epoch above the latest",
222 &[entry(0, 0), entry(2, 50), entry(5, 100)][..],
223 7,
224 200,
225 (UNDEFINED_EPOCH, UNDEFINED_EPOCH_OFFSET),
226 },
227 below_first = {
228 "below the first epoch: requested epoch, first start",
229 &[entry(3, 30), entry(4, 40)][..],
230 1,
231 200,
232 (LeaderEpoch(1), Offset(30)),
233 },
234 exact_older = {
235 "exact older epoch: that epoch, next start",
236 &[entry(0, 0), entry(2, 50), entry(5, 100)][..],
237 2,
238 200,
239 (LeaderEpoch(2), Offset(100)),
240 },
241 gap_epoch = {
242 "gap epoch: floor epoch, next start",
243 &[entry(0, 0), entry(2, 50), entry(5, 100)][..],
244 3,
245 200,
246 (LeaderEpoch(2), Offset(100)),
247 },
248 gap_below_latest = {
249 "gap epoch below the latest: floor epoch 2, start of 5",
250 &[entry(0, 0), entry(2, 50), entry(5, 100)][..],
251 4,
252 200,
253 (LeaderEpoch(2), Offset(100)),
254 },
255 gap_above_first = {
256 "gap epoch just above the first: floor epoch 0, start of 2",
257 &[entry(0, 0), entry(2, 50), entry(5, 100)][..],
258 1,
259 200,
260 (LeaderEpoch(0), Offset(50)),
261 },
262 )]
263 fn lookup_matches_kafka_end_offset_for_rows(
264 name: &str,
265 entries: &[EpochEntry],
266 requested: i32,
267 log_end: i64,
268 expected: (LeaderEpoch, Offset),
269 ) {
270 assert2::check!(
271 epoch_and_offset_for_entries(entries, LeaderEpoch(requested), Offset(log_end))
272 == expected,
273 "case {name}"
274 );
275 }
276
277 proptest! {
278 #[test]
279 fn lookup_matches_tree_map_oracle(
280 epochs in collection::btree_set(0i32..100, 0..32),
281 requested in -3i32..110,
282 log_end in 0i64..10_000,
283 ) {
284 let entries = epochs
285 .into_iter()
286 .enumerate()
287 .map(|(i, epoch)| {
288 entry(epoch, i64::try_from(i).expect("epoch set length is bounded to 32") * 10)
289 })
290 .collect::<Vec<_>>();
291 prop_assert_eq!(
292 epoch_and_offset_for_entries(
293 &entries,
294 LeaderEpoch(requested),
295 Offset(log_end),
296 ),
297 tree_map_oracle(&entries, LeaderEpoch(requested), Offset(log_end))
298 );
299 }
300 }
301}