Skip to main content

krabka_verified/broker/
fetch.rs

1use {
2    super::{
3        FetchVisibility, FetchWatermarks, PreferredLeaderChange, ReplicaFetchFacts,
4        ReplicaFetchMutation,
5    },
6    creusot_std::prelude::*,
7};
8
9// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
10#[cfg(creusot)]
11#[cfg_attr(test, mutants::skip)]
12#[logic(open)]
13/// A row answers a request this follower no longer stands behind: the epoch
14/// moved while it was in flight, the replication target changed, or the leader
15/// names a different target than the one this follower fetched from.
16pub fn replica_fetch_fenced(facts: ReplicaFetchFacts) -> bool {
17    pearlite! {
18        facts.request_leader_epoch@ != facts.current_leader_epoch@
19            || !facts.target_matches
20            || !facts.reported_target_matches
21    }
22}
23
24/// Fence one follower Fetch response row against its in-flight request epoch,
25/// its current metadata target and any reported leader identity; then select
26/// one exclusive response action.
27///
28/// A follower's request covers every partition it follows on one leader, so
29/// the caller applies this once per row of the answer, with that row's own
30/// partition configuration and the epoch it was asked under. The guarantee is
31/// therefore per partition and does not weaken as a response carries more of
32/// them: a fenced row mutates nothing, whatever the rows beside it say. An
33/// unfenced row is retried on an error, truncates on a KIP-320 divergence
34/// (which Kafka sends with no records), and appends otherwise.
35#[ensures(result == if replica_fetch_fenced(facts) {
36    ReplicaFetchMutation::Reject
37} else if facts.error_code@ != 0 {
38    ReplicaFetchMutation::Retry
39} else if facts.diverging_epoch@ >= 0 {
40    ReplicaFetchMutation::Truncate
41} else {
42    ReplicaFetchMutation::Append
43})]
44#[must_use]
45pub fn replica_fetch_mutation(facts: ReplicaFetchFacts) -> ReplicaFetchMutation {
46    if facts.request_leader_epoch != facts.current_leader_epoch
47        || !facts.target_matches
48        || !facts.reported_target_matches
49    {
50        ReplicaFetchMutation::Reject
51    } else if facts.error_code != 0 {
52        ReplicaFetchMutation::Retry
53    } else if facts.diverging_epoch >= 0 {
54        ReplicaFetchMutation::Truncate
55    } else {
56        ReplicaFetchMutation::Append
57    }
58}
59
60// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
61#[cfg(creusot)]
62#[cfg_attr(test, mutants::skip)]
63#[logic(open)]
64/// KIP-460's preferred election: leadership moves to the first assigned
65/// replica, and only when that replica is alive, in the ISR, and able to lead.
66pub fn preferred_change_eligible(change: PreferredLeaderChange) -> bool {
67    pearlite! {
68        change.preferred_replica == Some(change.new_leader)
69            && change.leader_in_isr
70            && change.leader_alive
71            && !change.leader_is_witness
72    }
73}
74
75/// Admit one preferred-leader rebalance batch only when it is nonempty, within
76/// the per-tick election cap, and every change in it is a KIP-460 preferred
77/// election.
78///
79/// There is no imbalance-ratio threshold among the facts. That gate is the
80/// `ZooKeeper` controller's `leader.imbalance.per.broker.percentage`; the
81/// `KRaft` controller krabka follows restores every partition whose preferred
82/// replica is eligible, and bounds the work by a per-tick election count
83/// (`QuorumController.MAX_ELECTIONS_PER_IMBALANCE`), which is `max_changes`.
84#[ensures(result == (
85    0 < [email protected]()
86        && [email protected]() <= max_changes@
87        && forall<i: Int> 0 <= i && i < [email protected]()
88            ==> preferred_change_eligible(changes@[i])
89))]
90#[must_use]
91pub fn preferred_rebalance_admission(
92    changes: &[PreferredLeaderChange],
93    max_changes: usize,
94) -> bool {
95    // `len` rather than `is_empty`: Creusot specifies `<[T]>::len` only.
96    let count = changes.len();
97    if count == 0 || count > max_changes {
98        return false;
99    }
100    let mut index = 0_usize;
101    #[invariant(index@ <= [email protected]())]
102    #[invariant(forall<k: Int> 0 <= k && k < index@ ==> preferred_change_eligible(changes@[k]))]
103    #[variant([email protected]() - index@)]
104    while index < changes.len() {
105        let Some(&change) = changes.get(index) else {
106            return false;
107        };
108        let installs_preferred =
109            matches!(change.preferred_replica, Some(preferred) if preferred == change.new_leader);
110        if !installs_preferred
111            || !change.leader_in_isr
112            || !change.leader_alive
113            || change.leader_is_witness
114        {
115            return false;
116        }
117        index += 1;
118    }
119    true
120}
121
122// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
123#[cfg(creusot)]
124#[cfg_attr(test, mutants::skip)]
125#[logic(open)]
126/// The lower of two offsets.
127pub fn offset_min(a: Int, b: Int) -> Int {
128    pearlite! { if a <= b { a } else { b } }
129}
130
131// cargo-mutants: #[cfg(creusot)] spec function; not compiled outside Creusot, so no test can tell.
132#[cfg(creusot)]
133#[cfg_attr(test, mutants::skip)]
134#[logic(open)]
135/// The exclusive upper offset one fetch may read.
136///
137/// A follower reads to the leader's log end (Kafka's `FetchIsolation.LOG_END`).
138/// A consumer reads below the high watermark (`HIGH_WATERMARK`), below the
139/// last stable offset as well under `read_committed` (`TXN_COMMITTED`), and,
140/// on a scheduled topic, below KFC-1's delivery watermark too.
141pub fn fetch_limit_model(is_follower: bool, read_committed: bool, w: FetchWatermarks) -> Int {
142    pearlite! {
143        if is_follower {
144            w.log_end@
145        } else if read_committed {
146            offset_min(offset_min(w.hw@, w.deliverable@), w.lso@)
147        } else {
148            offset_min(w.hw@, w.deliverable@)
149        }
150    }
151}
152
153/// Compute Kafka's consumer/follower Fetch visibility window.
154///
155/// The reported watermarks are the partition's own, for every fetcher:
156/// `response_hw` is the high watermark and `response_lso` is the last stable
157/// offset, capped at the high watermark as Kafka's `UnifiedLog.lastStableOffset`
158/// caps it. Kafka's `Partition.readRecords` reads both before the fetch and
159/// puts them in the `LogReadInfo` whatever the isolation level and whoever is
160/// fetching, so a follower learns the leader's committed bound rather than its
161/// log end. That is what keeps a KIP-392 follower from exposing an offset the
162/// leader may still truncate.
163///
164/// [`FetchWatermarks::deliverable`] is KFC-1's delivery watermark: the first
165/// offset a consumer may not see yet, because the batch that starts there has
166/// not reached its activation time. It caps a consumer, and it caps nothing
167/// else:
168///
169/// - A follower is never gated. Replication carries a scheduled record to the
170///   ISR, and it counts toward the high watermark, long before any consumer can
171///   read it.
172/// - `response_hw` and `response_lso` do not move with it, so consumer lag
173///   stays honest and KIP-227 watermark monotonicity is untouched.
174///
175/// Offsets below the log start or above the log end are out of range.
176/// An offset equal to the log end remains a valid empty fetch.
177///
178/// There is no precondition. The consumer bound is proved outright: whatever
179/// the caller passes, a consumer never reads at or past the high watermark, the
180/// delivery watermark, or, under `read_committed`, the last stable offset.
181#[ensures(result.out_of_range == (fetch_offset@ < w.log_start@ || fetch_offset@ > w.log_end@))]
182#[ensures(result.empty == (!result.out_of_range
183    && fetch_offset@ >= if is_follower { w.log_end@ } else { offset_min(w.hw@, w.deliverable@) }))]
184#[ensures(result.limit_offset@ == fetch_limit_model(is_follower, read_committed, w))]
185#[ensures(!is_follower ==> result.limit_offset@ <= w.hw@
186    && result.limit_offset@ <= w.deliverable@
187    && (read_committed ==> result.limit_offset@ <= w.lso@))]
188#[ensures(result.read_committed_aborts == (read_committed && !is_follower))]
189#[ensures(result.effective_lso@ == if result.read_committed_aborts {
190    offset_min(w.lso@, w.hw@)
191} else {
192    w.lso@
193})]
194#[ensures(result.response_hw == w.hw)]
195#[ensures(result.response_lso@ == offset_min(w.lso@, w.hw@))]
196#[must_use]
197pub fn fetch_visibility(
198    is_follower: bool,
199    read_committed: bool,
200    w: FetchWatermarks,
201    fetch_offset: i64,
202) -> FetchVisibility {
203    // The delivery watermark caps a consumer and never a follower.
204    let visible = if w.deliverable < w.hw {
205        w.deliverable
206    } else {
207        w.hw
208    };
209    let stable = if w.lso < w.hw { w.lso } else { w.hw };
210    let read_committed_aborts = read_committed && !is_follower;
211    let limit_offset = if is_follower {
212        w.log_end
213    } else if read_committed && stable < visible {
214        stable
215    } else {
216        visible
217    };
218    let upper_bound = if is_follower { w.log_end } else { visible };
219    let out_of_range = fetch_offset < w.log_start || fetch_offset > w.log_end;
220    FetchVisibility {
221        out_of_range,
222        empty: !out_of_range && fetch_offset >= upper_bound,
223        limit_offset,
224        effective_lso: if read_committed_aborts { stable } else { w.lso },
225        read_committed_aborts,
226        response_hw: w.hw,
227        response_lso: stable,
228    }
229}