Skip to main content

krabka_verified/producer_snapshot/
snapshot_transaction_valid.rs

1#[cfg(creusot)]
2use super::ProducerSnapshotEntryFacts;
3#[cfg(test)]
4use std::cell::Cell;
5use {super::ProducerReloadRange, creusot_std::prelude::*};
6
7// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
8#[cfg(creusot)]
9#[cfg_attr(test, mutants::skip)]
10#[logic(open)]
11/// Kafka's `truncateAndReload` keeps a snapshot exactly when
12/// `logStartOffset < offset <= logEndOffset`. A snapshot at the log start
13/// describes no record that is still in the log, so Kafka deletes it too.
14pub fn kafka_reload_keeps(offset: Int, log_start: Int, log_end: Int) -> bool {
15    pearlite! { log_start < offset && offset <= log_end }
16}
17
18// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
19#[cfg(creusot)]
20#[cfg_attr(test, mutants::skip)]
21#[logic(open)]
22/// Some local segment starts at `offset`.
23pub fn is_segment_base(bases: Seq<i64>, offset: Int) -> bool {
24    pearlite! { exists<j: Int> 0 <= j && j < bases.len() && bases[j]@ == offset }
25}
26
27// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
28#[cfg(creusot)]
29#[cfg_attr(test, mutants::skip)]
30#[logic(open)]
31/// Kafka's `removeStraySnapshots(segmentBaseOffsets)` deletes a snapshot
32/// exactly when no segment starts at its offset, unless it is the newest
33/// snapshot and lies above every segment base. That one survivor is the
34/// snapshot a clean shutdown writes at the log end.
35///
36/// The method walks the snapshots in offset order, deleting each stray one
37/// as soon as a later stray one turns up, then deletes the last stray one if
38/// it lies below the greatest segment base. What is left is the rule above.
39pub fn kafka_stray_removed(snapshots: Seq<i64>, index: Int, bases: Seq<i64>) -> bool {
40    pearlite! {
41        !is_segment_base(bases, snapshots[index]@)
42            && !((forall<j: Int> 0 <= j && j < snapshots.len()
43                    ==> snapshots[j]@ <= snapshots[index]@)
44                && (forall<j: Int> 0 <= j && j < bases.len()
45                    ==> bases[j]@ < snapshots[index]@))
46    }
47}
48
49// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
50#[cfg(creusot)]
51#[cfg_attr(test, mutants::skip)]
52#[logic(open)]
53/// Kafka's replay cursor: `max(segment.baseOffset, mapEndOffset,
54/// logStartOffset)` for the first local segment the replay reads, where
55/// `mapEndOffset` is the loaded snapshot's offset, or the log start when no
56/// snapshot loads. A loaded snapshot always lies above the log start.
57pub fn kafka_replay_start(range: ProducerReloadRange, snapshot: Option<i64>) -> Int {
58    pearlite! {
59        match snapshot {
60            Some(offset) => if offset@ >= range.local_start@ { offset@ } else { range.local_start@ },
61            None => if range.log_start@ >= range.local_start@ {
62                range.log_start@
63            } else {
64                range.local_start@
65            },
66        }
67    }
68}
69
70/// Pick the log start a producer-state reload runs against, as Kafka's
71/// `LogLoader.load` does.
72///
73/// With remote storage enabled (KIP-405), it is the checkpointed log start,
74/// which may sit below every local segment. Kafka reads a missing checkpoint
75/// as 0, so `established` is `None` answers 0. Without remote storage, it is
76/// the greater of the checkpoint and the oldest local segment's base, which
77/// is `local_log_start`.
78#[ensures(result == if remote_storage_enable {
79    match established {
80        Some(start) => start,
81        None => 0i64,
82    }
83} else {
84    local_log_start
85})]
86#[must_use]
87pub const fn producer_snapshot_reload_log_start(
88    remote_storage_enable: bool,
89    established: Option<i64>,
90    local_log_start: i64,
91) -> i64 {
92    if remote_storage_enable {
93        match established {
94            Some(start) => start,
95            None => 0,
96        }
97    } else {
98        local_log_start
99    }
100}
101
102/// Keep a snapshot through a reload exactly when Kafka's `truncateAndReload`
103/// keeps it: when it lies in `(log_start, log_end]`.
104#[ensures(result == kafka_reload_keeps(offset@, range.log_start@, range.log_end@))]
105#[must_use]
106pub const fn producer_snapshot_reload_keeps(offset: i64, range: ProducerReloadRange) -> bool {
107    range.log_start < offset && offset <= range.log_end
108}
109
110/// Delete the snapshot at `snapshots[index]` exactly when Kafka's
111/// `removeStraySnapshots` deletes it, given every local segment's base
112/// offset. The snapshot offsets must be distinct; their order is irrelevant.
113///
114/// The proof assumes `index < snapshots.len()`. Other inputs are outside
115/// that contract, and an `index` outside `snapshots` answers `false`.
116#[requires(index@ < [email protected]())]
117#[ensures(result == kafka_stray_removed(snapshots@, index@, segment_bases@))]
118#[must_use]
119pub fn producer_snapshot_stray(snapshots: &[i64], index: usize, segment_bases: &[i64]) -> bool {
120    let Some(&offset) = snapshots.get(index) else {
121        return false;
122    };
123    let mut is_base = false;
124    let mut above_every_base = true;
125    let mut i = 0usize;
126    #[invariant(i@ <= [email protected]())]
127    #[invariant(is_base == exists<j: Int> 0 <= j && j < i@ && segment_bases@[j]@ == offset@)]
128    #[invariant(above_every_base == forall<j: Int> 0 <= j && j < i@
129        ==> segment_bases@[j]@ < offset@)]
130    #[variant([email protected]() - i@)]
131    while i < segment_bases.len() {
132        if let Some(&base) = segment_bases.get(i) {
133            if base == offset {
134                is_base = true;
135            }
136            if base >= offset {
137                above_every_base = false;
138            }
139        }
140        i += 1;
141    }
142    let mut newest = true;
143    let mut k = 0usize;
144    #[invariant(k@ <= [email protected]())]
145    #[invariant(newest == forall<j: Int> 0 <= j && j < k@ ==> snapshots@[j]@ <= offset@)]
146    #[variant([email protected]() - k@)]
147    while k < snapshots.len() {
148        if let Some(&snapshot) = snapshots.get(k)
149            && snapshot > offset
150        {
151            newest = false;
152        }
153        k += 1;
154    }
155    !(is_base || (newest && above_every_base))
156}
157
158/// Classify all producer snapshots using their on-disk offset order.
159///
160/// This is the ordered, bulk form of [`producer_snapshot_stray`]. Both input
161/// slices must be sorted in nondecreasing order. The returned value at each index is
162/// `true` exactly when Kafka removes that snapshot. A single cursor through
163/// `segment_bases` is shared by every snapshot, so each input element is
164/// visited at most once; the greatest base is read once for Kafka's
165/// clean-shutdown exception.
166///
167/// Kafka retains the newest snapshot even when it is not a segment base when
168/// it lies above every segment base. Empty segment lists therefore retain only
169/// the newest snapshot.
170#[cfg_attr(
171    creusot,
172    requires(forall<i: Int, j: Int> 0 <= i && i < j && j < [email protected]()
173        ==> snapshots@[i]@ <= snapshots@[j]@)
174)]
175#[cfg_attr(
176    creusot,
177    requires(forall<i: Int, j: Int> 0 <= i && i < j && j < [email protected]()
178        ==> segment_bases@[i]@ <= segment_bases@[j]@)
179)]
180#[ensures([email protected]() == [email protected]())]
181#[ensures(forall<i: Int> 0 <= i && i < [email protected]()
182    ==> result@[i] == kafka_stray_removed(snapshots@, i, segment_bases@))]
183#[must_use]
184pub fn producer_snapshot_strays(snapshots: &[i64], segment_bases: &[i64]) -> Vec<bool> {
185    let mut removed = Vec::with_capacity(snapshots.len());
186    let greatest_base = segment_bases.last().copied();
187    let mut base_index = 0usize;
188    let mut snapshot_index = 0usize;
189    #[invariant(snapshot_index@ <= [email protected]())]
190    #[invariant(base_index@ <= [email protected]())]
191    #[invariant([email protected]() == snapshot_index@)]
192    #[invariant(snapshot_index@ < [email protected]() ==>
193        forall<i: Int> 0 <= i && i < base_index@
194            ==> segment_bases@[i]@ < snapshots@[snapshot_index@]@)]
195    #[invariant(forall<i: Int> 0 <= i && i < snapshot_index@
196        ==> removed@[i] == kafka_stray_removed(snapshots@, i, segment_bases@))]
197    #[variant([email protected]() - snapshot_index@)]
198    while snapshot_index < snapshots.len() {
199        record_classifier_visit();
200        let Some(&offset) = snapshots.get(snapshot_index) else {
201            break;
202        };
203        #[invariant(base_index@ <= [email protected]())]
204        #[invariant([email protected]() == snapshot_index@)]
205        #[invariant(forall<i: Int> 0 <= i && i < snapshot_index@
206            ==> removed@[i] == kafka_stray_removed(snapshots@, i, segment_bases@))]
207        #[invariant(forall<i: Int> 0 <= i && i < base_index@
208            ==> segment_bases@[i]@ < offset@)]
209        #[variant([email protected]() - base_index@)]
210        while base_index < segment_bases.len()
211            && matches!(segment_bases.get(base_index), Some(&base) if base < offset)
212        {
213            record_classifier_visit();
214            base_index += 1;
215        }
216
217        proof_assert!(base_index@ < [email protected]() ==>
218            forall<j: Int> base_index@ <= j && j < [email protected]()
219                ==> segment_bases@[base_index@]@ <= segment_bases@[j]@);
220        let is_base = base_index < segment_bases.len()
221            && matches!(segment_bases.get(base_index), Some(&base) if base == offset);
222        proof_assert!(is_base == is_segment_base(segment_bases@, offset@));
223        let newest = matches!(snapshots.last(), Some(&last) if last == offset);
224        proof_assert!(forall<j: Int> 0 <= j && j < [email protected]()
225            ==> snapshots@[j]@ <= snapshots@[[email protected]() - 1]@);
226        proof_assert!(newest == (forall<j: Int> 0 <= j && j < [email protected]()
227            ==> snapshots@[j]@ <= offset@));
228        let above_every_base = !matches!(greatest_base, Some(base) if offset <= base);
229        proof_assert!(above_every_base == (forall<j: Int> 0 <= j && j < [email protected]()
230            ==> segment_bases@[j]@ < offset@));
231        removed.push(!(is_base || (newest && above_every_base)));
232        proof_assert!(snapshot_index@ + 1 < [email protected]()
233            ==> offset@ <= snapshots@[snapshot_index@ + 1]@);
234        snapshot_index += 1;
235    }
236    removed
237}
238
239#[cfg(test)]
240thread_local! {
241    static CLASSIFIER_VISITS: Cell<usize> = const { Cell::new(0) };
242}
243
244/// Count one classifier visit; only tests read the count.
245fn record_classifier_visit() {
246    #[cfg(test)]
247    CLASSIFIER_VISITS.with(|visits| visits.set(visits.get() + 1));
248}
249
250#[cfg(test)]
251pub fn take_classifier_visits() -> usize { CLASSIFIER_VISITS.with(|visits| visits.replace(0)) }
252
253/// Return the index of the newest snapshot a reload keeps, as Kafka's
254/// `loadFromSnapshot` loads `latestSnapshotFile()` after `truncateAndReload`
255/// deleted every snapshot outside `(log_start, log_end]`.
256///
257/// Input order is irrelevant.
258#[ensures(result == None ==>
259    forall<i: Int> 0 <= i && i < [email protected]()
260        ==> !kafka_reload_keeps(offsets@[i]@, range.log_start@, range.log_end@))]
261#[ensures(forall<selected: usize> result == Some(selected) ==>
262    selected@ < [email protected]()
263        && kafka_reload_keeps(offsets@[selected@]@, range.log_start@, range.log_end@)
264        && (forall<i: Int> 0 <= i && i < [email protected]()
265            && kafka_reload_keeps(offsets@[i]@, range.log_start@, range.log_end@)
266            ==> offsets@[i]@ <= offsets@[selected@]@))]
267#[must_use]
268pub fn producer_snapshot_latest_index(
269    offsets: &[i64],
270    range: ProducerReloadRange,
271) -> Option<usize> {
272    let mut best: Option<usize> = None;
273    let mut i = 0usize;
274    #[invariant(i@ <= [email protected]())]
275    #[invariant(match best {
276        None => forall<j: Int> 0 <= j && j < i@
277            ==> !kafka_reload_keeps(offsets@[j]@, range.log_start@, range.log_end@),
278        Some(selected) => selected@ < i@
279            && kafka_reload_keeps(offsets@[selected@]@, range.log_start@, range.log_end@)
280            && (forall<j: Int> 0 <= j && j < i@
281                && kafka_reload_keeps(offsets@[j]@, range.log_start@, range.log_end@)
282                ==> offsets@[j]@ <= offsets@[selected@]@),
283    })]
284    #[variant([email protected]() - i@)]
285    while i < offsets.len() {
286        if let Some(&offset) = offsets.get(i)
287            && producer_snapshot_reload_keeps(offset, range)
288        {
289            match best {
290                None => best = Some(i),
291                Some(selected) if matches!(offsets.get(selected), Some(&current) if offset > current) =>
292                {
293                    best = Some(i);
294                }
295                Some(_) => {}
296            }
297        }
298        i += 1;
299    }
300    best
301}
302
303// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
304#[cfg(creusot)]
305#[cfg_attr(test, mutants::skip)]
306#[logic(open)]
307/// The entry's last-record fields are either the no-record sentinel -- a
308/// producer that has only written transaction markers, which Kafka writes as
309/// `(-1, -1, 0)` -- or a real batch that ends before the snapshot's offset,
310/// with room below its last offset for the offsets its delta spans.
311pub fn snapshot_last_record_valid(snapshot_offset: Int, entry: ProducerSnapshotEntryFacts) -> bool {
312    pearlite! {
313        (entry.last_offset@ == -1 && entry.last_sequence@ == -1 && entry.offset_delta@ == 0)
314            || (entry.last_offset@ >= 0
315                && entry.last_sequence@ >= 0
316                && entry.offset_delta@ >= 0
317                && entry.last_offset@ >= entry.offset_delta@
318                && entry.last_offset@ < snapshot_offset)
319    }
320}
321
322// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
323#[cfg(creusot)]
324#[cfg_attr(test, mutants::skip)]
325#[logic(open)]
326/// The entry's open transaction is either absent (`-1`), or starts before
327/// the snapshot's offset and no later than the producer's last record.
328pub fn snapshot_transaction_valid(snapshot_offset: Int, entry: ProducerSnapshotEntryFacts) -> bool {
329    pearlite! {
330        entry.current_txn_first_offset@ == -1
331            || (entry.current_txn_first_offset@ >= 0
332                && entry.current_txn_first_offset@ < snapshot_offset
333                && entry.current_txn_first_offset@ <= entry.last_offset@)
334    }
335}