1#[cfg(test)]
8use super::UNDEFINED_EPOCH;
9use {
10 super::{LeaderEpochCheckpoint, UNDEFINED_OFFSET, epoch_and_offset_for_entries},
11 krabka_ids::{LeaderEpoch, Offset},
12};
13
14impl LeaderEpochCheckpoint {
15 #[must_use]
24 pub fn end_offset_for_epoch(&self, epoch: LeaderEpoch, log_end_offset: Offset) -> Offset {
25 if !self.entries.iter().any(|e| e.epoch == epoch) {
26 return UNDEFINED_OFFSET;
27 }
28 self.entries
34 .iter()
35 .filter(|e| e.epoch > epoch)
36 .map(|e| e.start_offset)
37 .min()
38 .unwrap_or(log_end_offset)
39 }
40
41 #[must_use]
55 pub fn epoch_for_offset(&self, offset: Offset) -> Option<LeaderEpoch> {
56 self.entries
57 .iter()
58 .filter(|e| e.start_offset <= offset)
59 .max_by_key(|e| e.start_offset)
60 .map(|e| e.epoch)
61 }
62
63 #[must_use]
83 pub fn epoch_and_offset_for(
84 &self,
85 requested_epoch: LeaderEpoch,
86 log_end_offset: Offset,
87 ) -> (LeaderEpoch, Offset) {
88 epoch_and_offset_for_entries(&self.entries, requested_epoch, log_end_offset)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use {
95 super::*,
96 crate::{
97 leader_epoch_checkpoint::{
98 EpochEntry,
99 test_support::{TempCheckpoint, checkpoint, entry, fresh},
100 },
101 test_support::TempPath,
102 },
103 assert2::check,
104 };
105
106 #[test]
107 fn end_offset_for_current_epoch_returns_log_end_offset() {
108 let TempCheckpoint {
109 dir: _d,
110 checkpoint: c,
111 } = checkpoint(&[entry(0, 0), entry(1, 50)]);
112 assert2::assert!(c.end_offset_for_epoch(LeaderEpoch(1), Offset(100)) == 100);
113 }
114
115 #[test]
116 fn end_offset_for_older_epoch_returns_next_start() {
117 let TempCheckpoint {
118 dir: _d,
119 checkpoint: c,
120 } = checkpoint(&[entry(0, 0), entry(1, 50), entry(2, 100)]);
121 assert2::assert!(c.end_offset_for_epoch(LeaderEpoch(0), Offset(200)) == Offset(50));
122 assert2::assert!(c.end_offset_for_epoch(LeaderEpoch(1), Offset(200)) == Offset(100));
123 }
124
125 #[test]
126 fn end_offset_for_unknown_epoch_returns_undefined() {
127 let TempCheckpoint {
128 dir: _d,
129 checkpoint: c,
130 } = checkpoint(&[entry(0, 0)]);
131 assert2::assert!(c.end_offset_for_epoch(LeaderEpoch(7), Offset(200)) == -1);
132 }
133
134 #[yare::parameterized(
137 zero = { "zero", 0 },
138 positive = { "positive", 100 },
139 )]
140 fn epoch_for_offset_empty_returns_none(_name: &str, offset: i64) {
141 let TempPath { dir: _d, path } = fresh();
142 let c = LeaderEpochCheckpoint::open(path).unwrap();
143 assert2::assert!(c.epoch_for_offset(Offset(offset)) == None);
144 }
145
146 #[test]
147 fn epoch_for_offset_before_first_entry_returns_none() {
148 let TempCheckpoint {
150 dir: _d,
151 checkpoint: c,
152 } = checkpoint(&[entry(0, 10), entry(1, 50)]);
153 assert2::assert!(c.epoch_for_offset(Offset(9)) == None);
154 }
155
156 #[yare::parameterized(
157 start_epoch_0 = { 0, Some(LeaderEpoch(0)), "start of epoch 0" },
158 middle_epoch_0 = { 25, Some(LeaderEpoch(0)), "middle of epoch 0" },
159 last_before_epoch_1 = { 49, Some(LeaderEpoch(0)), "last offset before epoch 1" },
160 start_epoch_1 = { 50, Some(LeaderEpoch(1)), "start of epoch 1" },
161 middle_epoch_1 = { 75, Some(LeaderEpoch(1)), "middle of epoch 1" },
162 last_before_epoch_2 = { 99, Some(LeaderEpoch(1)), "last offset before epoch 2" },
163 )]
164 fn epoch_for_offset_within_epoch_range(offset: i64, want: Option<LeaderEpoch>, why: &str) {
165 let TempCheckpoint {
166 dir: _d,
167 checkpoint: c,
168 } = checkpoint(&[entry(0, 0), entry(1, 50), entry(2, 100)]);
169 check!(
170 c.epoch_for_offset(Offset(offset)) == want,
171 "{why} (offset={offset})"
172 );
173 }
174
175 #[yare::parameterized(
178 epoch_boundary = { "epoch 1 start", 50 },
179 past_last = { "past last entry", 100 },
180 far_past_last = { "far past last entry", 999 },
181 )]
182 fn epoch_for_offset_at_or_past_last_entry(_name: &str, offset: i64) {
183 let TempCheckpoint {
184 dir: _d,
185 checkpoint: c,
186 } = checkpoint(&[entry(0, 0), entry(1, 50)]);
187 assert2::assert!(c.epoch_for_offset(Offset(offset)) == Some(LeaderEpoch(1)));
188 }
189
190 #[test]
191 fn epoch_for_offset_single_entry_at_zero() {
192 let TempCheckpoint {
193 dir: _d,
194 checkpoint: c,
195 } = checkpoint(&[entry(0, 0)]);
196 assert2::assert!(c.epoch_for_offset(Offset(0)) == Some(LeaderEpoch(0)));
197 assert2::assert!(c.epoch_for_offset(Offset(1000)) == Some(LeaderEpoch(0)));
198 }
199
200 #[yare::parameterized(
205 empty = { "empty checkpoint", &[][..], 0, 9, (UNDEFINED_EPOCH, UNDEFINED_OFFSET) },
206 undefined = {
207 "undefined requested epoch",
208 &[entry(0, 0), entry(1, 50)][..],
209 -1,
210 100,
211 (UNDEFINED_EPOCH, UNDEFINED_OFFSET),
212 },
213 latest = {
214 "latest recorded epoch keeps the log end",
215 &[entry(0, 0), entry(1, 50)][..],
216 1,
217 100,
218 (LeaderEpoch(1), Offset(100)),
219 },
220 oldest = {
221 "oldest epoch ends at the next start",
222 &[entry(0, 0), entry(1, 50), entry(2, 100)][..],
223 0,
224 200,
225 (LeaderEpoch(0), Offset(50)),
226 },
227 middle = {
228 "middle epoch ends at the next start",
229 &[entry(0, 0), entry(1, 50), entry(2, 100)][..],
230 1,
231 200,
232 (LeaderEpoch(1), Offset(100)),
233 },
234 gap = {
235 "gap epoch resolves to the floor epoch",
236 &[entry(0, 0), entry(5, 100)][..],
237 3,
238 200,
239 (LeaderEpoch(0), Offset(100)),
240 },
241 future = {
242 "future epoch cannot be placed",
243 &[entry(0, 0), entry(1, 50)][..],
244 7,
245 100,
246 (UNDEFINED_EPOCH, UNDEFINED_OFFSET),
247 },
248 below_first = {
249 "below every recorded epoch keeps the requested epoch",
250 &[entry(3, 30), entry(4, 40)][..],
251 1,
252 100,
253 (LeaderEpoch(1), Offset(30)),
254 },
255 )]
256 fn epoch_and_offset_for_follows_kafka_end_offset_for(
257 name: &str,
258 recorded: &[EpochEntry],
259 requested: i32,
260 log_end: i64,
261 expected: (LeaderEpoch, Offset),
262 ) {
263 let TempCheckpoint {
264 dir: _d,
265 checkpoint: c,
266 } = checkpoint(recorded);
267 check!(
268 c.epoch_and_offset_for(LeaderEpoch(requested), Offset(log_end)) == expected,
269 "{name}"
270 );
271 }
272}