Skip to main content

krabka_verified/remote_read/
remote_time_index_candidate_count.rs

1use {crate::log_index::TimeEntry, creusot_std::prelude::*};
2
3// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
4#[cfg(creusot)]
5#[cfg_attr(test, mutants::skip)]
6#[logic(open)]
7fn finished_selection(
8    starts: Seq<i64>,
9    ends: Seq<i64>,
10    finished: Seq<bool>,
11    best: Option<usize>,
12    count: Int,
13    latest: bool,
14) -> bool {
15    pearlite! {
16        match best {
17            Some(best) => best@ < count && finished[best@]
18                && 0 <= starts[best@]@ && starts[best@]@ <= ends[best@]@
19                && forall<i: Int> 0 <= i && i < count && finished[i]
20                    && 0 <= starts[i]@ && starts[i]@ <= ends[i]@
21                    ==> if latest { ends[i]@ <= ends[best@]@ } else { starts[best@]@ <= starts[i]@ },
22            None => forall<i: Int> 0 <= i && i < count
23                ==> !finished[i] || starts[i]@ < 0 || ends[i]@ < starts[i]@,
24        }
25    }
26}
27
28#[krabka_macros::finished_selection(latest)]
29fn select_finished_index(
30    starts: &[i64],
31    ends: &[i64],
32    finished: &[bool],
33    latest: bool,
34) -> Option<usize> {
35    let mut best: Option<usize> = None;
36    let mut index = 0usize;
37    #[invariant(index@ <= [email protected]())]
38    #[invariant(finished_selection(starts@, ends@, finished@, best, index@, latest))]
39    #[variant([email protected]() - index@)]
40    while index < starts.len() {
41        if let (Some(true), Some(&start), Some(&end)) =
42            (finished.get(index), starts.get(index), ends.get(index))
43            && start >= 0
44            && start <= end
45        {
46            let keep = match best {
47                Some(current) if latest => {
48                    matches!(ends.get(current), Some(&kept_end) if kept_end >= end)
49                }
50                Some(current) => {
51                    matches!(starts.get(current), Some(&kept_start) if kept_start <= start)
52                }
53                None => false,
54            };
55            if !keep {
56                best = Some(index);
57            }
58        }
59        index += 1;
60    }
61    best
62}
63
64#[krabka_macros::finished_selection(false)]
65/// Select the earliest valid finished remote segment.
66///
67/// The three slices are parallel arrays supplied from one metadata listing.
68/// Negative or inverted ranges are not candidates, even when their lifecycle
69/// state says the copy finished.
70#[must_use]
71pub fn tiered_earliest_finished_index(
72    starts: &[i64],
73    ends: &[i64],
74    finished: &[bool],
75) -> Option<usize> {
76    select_finished_index(starts, ends, finished, false)
77}
78
79#[krabka_macros::finished_selection(true)]
80/// Select the finished remote segment with the greatest valid inclusive end.
81#[must_use]
82pub fn tiered_latest_finished_index(
83    starts: &[i64],
84    ends: &[i64],
85    finished: &[bool],
86) -> Option<usize> {
87    select_finished_index(starts, ends, finished, true)
88}
89
90/// Select the valid leader epoch whose start is greatest at or below a
91/// segment's inclusive end.
92#[requires([email protected]() == [email protected]())]
93#[requires(0 <= segment_start@)]
94#[requires(segment_start@ <= segment_end@)]
95#[ensures(owning_epoch_selection(epochs@, starts@, segment_start@, segment_end@, [email protected](), result))]
96#[must_use]
97pub fn tiered_owning_epoch_index(
98    epochs: &[i32],
99    starts: &[i64],
100    segment_start: i64,
101    segment_end: i64,
102) -> Option<usize> {
103    let mut best: Option<usize> = None;
104    let mut index = 0;
105    #[invariant(index@ <= [email protected]())]
106    #[invariant(owning_epoch_selection(epochs@, starts@, segment_start@, segment_end@, index@, best))]
107    #[variant([email protected]() - index@)]
108    while index < starts.len() {
109        if let (Some(&epoch), Some(&start)) = (epochs.get(index), starts.get(index))
110            && epoch >= 0
111            && start >= segment_start
112            && start <= segment_end
113        {
114            match best {
115                Some(current) if matches!(starts.get(current), Some(&kept) if kept >= start) => {}
116                _ => best = Some(index),
117            }
118        }
119        index += 1;
120    }
121    best
122}
123
124/// Return the length of the usable strict-predecessor prefix of a remote time
125/// index.
126///
127/// Kafka preallocates a time index, so its tail is padding. The usable prefix
128/// is where relative offsets strictly increase; the first non-increasing
129/// offset is padding and ends it. The count stops there or at the first entry
130/// whose timestamp is at or above `target_timestamp`, whichever comes first.
131///
132/// Every counted entry is strictly below the target. A time-index entry holds
133/// the largest timestamp at or before its offset, so no record up to that
134/// offset can match the target, and each counted entry, the last one included,
135/// is a safe place to start a scan. The bytes come from the remote tier, so
136/// nothing here assumes their timestamps are sorted: for a Kafka-written index
137/// they are, and the last counted entry is then the latest safe one.
138#[ensures(result@ <= [email protected]())]
139#[ensures(forall<i: Int> 0 <= i && i < result@
140    ==> entries@[i].key@ < target_timestamp@)]
141#[ensures(forall<i: Int> 1 <= i && i < result@
142    ==> entries@[i - 1].coordinate@ < entries@[i].coordinate@)]
143#[ensures(result@ < [email protected]() ==>
144    (result@ > 0 && entries@[result@].coordinate@ <= entries@[result@ - 1].coordinate@)
145        || entries@[result@].key@ >= target_timestamp@)]
146#[must_use]
147pub fn remote_time_index_candidate_count(entries: &[TimeEntry], target_timestamp: i64) -> usize {
148    let mut count = 0usize;
149    #[invariant(count@ <= [email protected]())]
150    #[invariant(forall<i: Int> 0 <= i && i < count@
151        ==> entries@[i].key@ < target_timestamp@)]
152    #[invariant(forall<i: Int> 1 <= i && i < count@
153        ==> entries@[i - 1].coordinate@ < entries@[i].coordinate@)]
154    #[variant([email protected]() - count@)]
155    while count < entries.len() {
156        let Some(entry) = entries.get(count) else {
157            break;
158        };
159        if count > 0
160            && matches!(entries.get(count - 1), Some(previous) if entry.coordinate <= previous.coordinate)
161        {
162            break;
163        }
164        if entry.key >= target_timestamp {
165            break;
166        }
167        count += 1;
168    }
169    count
170}
171
172// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
173#[cfg(creusot)]
174#[cfg_attr(test, mutants::skip)]
175#[logic(open)]
176/// The selected epoch is the greatest valid start in the scanned segment prefix.
177fn owning_epoch_selection(
178    epochs: Seq<i32>,
179    starts: Seq<i64>,
180    first: Int,
181    last: Int,
182    count: Int,
183    selected: Option<usize>,
184) -> bool {
185    pearlite! { match selected {
186        Some(best) => best@ < count && 0 <= epochs[best@]@
187            && first <= starts[best@]@ && starts[best@]@ <= last
188            && forall<i: Int> 0 <= i && i < count && 0 <= epochs[i]@
189                && first <= starts[i]@ && starts[i]@ <= last
190                ==> starts[i]@ <= starts[best@]@,
191        None => forall<i: Int> 0 <= i && i < count
192            ==> epochs[i]@ < 0 || starts[i]@ < first || last < starts[i]@,
193    } }
194}