1use std::{
4 collections::{BTreeMap, HashMap},
5 sync::Arc,
6};
7
8use crate::{
9 barrier::cut::{BARRIER_STATE_TOPIC, BarrierCut, decode_cut_value, decode_key},
10 error::StreamsClientError,
11 runtime::io::{FetchedRec, IsolationLevel, RecordFetcher},
12};
13
14pub struct CutReader {
42 fetcher: Arc<dyn RecordFetcher>,
43 topic: String,
44 partitions: Option<Vec<i32>>,
46 offsets: HashMap<i32, i64>,
48 cuts: HashMap<String, BTreeMap<i64, BarrierCut>>,
50}
51
52impl CutReader {
53 #[must_use]
55 pub fn new(fetcher: Arc<dyn RecordFetcher>) -> Self {
56 Self::with_topic(fetcher, BARRIER_STATE_TOPIC)
57 }
58
59 #[must_use]
61 pub fn with_topic(fetcher: Arc<dyn RecordFetcher>, topic: impl Into<String>) -> Self {
62 Self {
63 fetcher,
64 topic: topic.into(),
65 partitions: None,
66 offsets: HashMap::new(),
67 cuts: HashMap::new(),
68 }
69 }
70
71 pub async fn latest_complete_cut(
81 &mut self,
82 group: &str,
83 ) -> Result<Option<BarrierCut>, StreamsClientError> {
84 self.refresh().await?;
85 Ok(self
86 .cuts
87 .get(group)
88 .and_then(|by_epoch| by_epoch.values().next_back())
89 .cloned())
90 }
91
92 pub async fn complete_cuts_after(
102 &mut self,
103 group: &str,
104 epoch: i64,
105 ) -> Result<Vec<BarrierCut>, StreamsClientError> {
106 self.refresh().await?;
107 Ok(self.cuts.get(group).map_or_else(Vec::new, |by_epoch| {
108 by_epoch
109 .range((epoch + 1)..)
110 .map(|(_epoch, cut)| cut.clone())
111 .collect()
112 }))
113 }
114
115 pub async fn complete_cut_at(
124 &mut self,
125 group: &str,
126 epoch: i64,
127 ) -> Result<Option<BarrierCut>, StreamsClientError> {
128 self.refresh().await?;
129 Ok(self
130 .cuts
131 .get(group)
132 .and_then(|by_epoch| by_epoch.get(&epoch))
133 .cloned())
134 }
135
136 async fn refresh(&mut self) -> Result<(), StreamsClientError> {
141 if self.partitions.is_none() {
142 let mut partitions = self.fetcher.partitions(&self.topic).await?;
143 partitions.sort_unstable();
144 self.partitions = Some(partitions);
145 }
146 let partitions = self.partitions.clone().unwrap_or_default();
147 for partition in partitions {
148 let mut offset = *self.offsets.entry(partition).or_insert(0);
149 loop {
150 let batch = self
151 .fetcher
152 .fetch(
153 &self.topic,
154 partition,
155 offset,
156 IsolationLevel::ReadUncommitted,
157 )
158 .await?;
159 if batch.records.is_empty() {
160 break;
161 }
162 let mut advanced = false;
163 for record in &batch.records {
164 self.accept(record)?;
165 let next = record.offset + 1;
166 if next > offset {
167 offset = next;
168 advanced = true;
169 }
170 }
171 if !advanced {
174 break;
175 }
176 }
177 self.offsets.insert(partition, offset);
178 }
179 Ok(())
180 }
181
182 fn accept(&mut self, record: &FetchedRec) -> Result<(), StreamsClientError> {
187 let Some(key) = record.key.as_deref() else {
188 return Ok(()); };
190 let key = decode_key(key)?;
191 if !key.is_cut() {
192 return Ok(());
193 }
194 let value = record.value.as_deref().filter(|value| !value.is_empty());
195 let Some(value) = value else {
196 self.forget(&key.group, key.epoch);
197 return Ok(());
198 };
199 let cut = decode_cut_value(&key, value)?;
200 if cut.complete() {
201 self.cuts
202 .entry(key.group)
203 .or_default()
204 .insert(key.epoch, cut);
205 } else {
206 self.forget(&key.group, key.epoch);
207 }
208 Ok(())
209 }
210
211 fn forget(&mut self, group: &str, epoch: i64) {
212 if let Some(by_epoch) = self.cuts.get_mut(group) {
213 by_epoch.remove(&epoch);
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use std::sync::Mutex as StdMutex;
221
222 use assert2::{assert, check};
223 use bytes::Bytes;
224
225 use super::*;
226 use crate::{
227 barrier::testing::{CutRecord, cut_key, cut_value, key_of_kind},
228 runtime::io::FetchBatch,
229 };
230
231 #[derive(Default)]
234 struct CutFetcher {
235 partitions: Vec<i32>,
236 batches: StdMutex<HashMap<(i32, i64), FetchBatch>>,
237 }
238
239 impl CutFetcher {
240 fn one_batch(records: Vec<FetchedRec>) -> Self {
242 let mut fetcher = Self {
243 partitions: vec![0],
244 batches: StdMutex::new(HashMap::new()),
245 };
246 fetcher.partitions = vec![0];
247 fetcher.script(0, 0, records);
248 fetcher
249 }
250
251 fn script(&self, partition: i32, offset: i64, records: Vec<FetchedRec>) {
253 self.batches
254 .lock()
255 .unwrap()
256 .insert((partition, offset), FetchBatch { records });
257 }
258 }
259
260 #[async_trait::async_trait]
261 impl RecordFetcher for CutFetcher {
262 async fn fetch(
263 &self,
264 _topic: &str,
265 partition: i32,
266 offset: i64,
267 _isolation: IsolationLevel,
268 ) -> Result<FetchBatch, StreamsClientError> {
269 Ok(self
270 .batches
271 .lock()
272 .unwrap()
273 .remove(&(partition, offset))
274 .unwrap_or_default())
275 }
276
277 async fn partitions(&self, _topic: &str) -> Result<Vec<i32>, StreamsClientError> {
278 Ok(self.partitions.clone())
279 }
280 }
281
282 fn record(offset: i64, key: Bytes, value: Option<Bytes>) -> FetchedRec {
283 FetchedRec {
284 offset,
285 key: Some(key),
286 value,
287 timestamp: -1,
288 }
289 }
290
291 fn cut_record(offset: i64, cut: &CutRecord) -> FetchedRec {
292 record(offset, cut_key(cut), Some(cut_value(cut)))
293 }
294
295 #[tokio::test]
296 async fn reads_complete_cuts_in_epoch_order() {
297 let first = CutRecord::simple("g", 1, &[("in", 0, 10)]);
298 let second = CutRecord::simple("g", 2, &[("in", 0, 20)]);
299 let other_group = CutRecord::simple("other", 3, &[("in", 0, 30)]);
300 let fetcher = Arc::new(CutFetcher::one_batch(vec![
301 cut_record(0, &first),
302 cut_record(1, &second),
303 cut_record(2, &other_group),
304 ]));
305 let mut reader = CutReader::new(fetcher);
306
307 check!(
308 reader.complete_cuts_after("g", -1).await.unwrap()
309 == vec![first.expected(), second.expected()]
310 );
311 check!(reader.complete_cuts_after("g", 1).await.unwrap() == vec![second.expected()]);
312 check!(reader.latest_complete_cut("g").await.unwrap() == Some(second.expected()));
313 check!(reader.complete_cut_at("g", 1).await.unwrap() == Some(first.expected()));
314 check!(reader.complete_cut_at("g", 9).await.unwrap() == None);
315 check!(reader.latest_complete_cut("absent").await.unwrap() == None);
316 }
317
318 #[tokio::test]
319 async fn never_adopts_a_partial_cut() {
320 let partial = CutRecord::simple("g", 1, &[("in", 0, 10)]).partial(&[("in", 1)]);
321 let complete = CutRecord::simple("g", 2, &[("in", 0, 20), ("in", 1, 7)]);
322 let fetcher = Arc::new(CutFetcher::one_batch(vec![
323 cut_record(0, &partial),
324 cut_record(1, &complete),
325 ]));
326 let mut reader = CutReader::new(fetcher);
327
328 check!(reader.complete_cuts_after("g", -1).await.unwrap() == vec![complete.expected()]);
329 check!(reader.complete_cut_at("g", 1).await.unwrap() == None);
330 }
331
332 #[tokio::test]
333 async fn a_republished_partial_cut_drops_the_epoch() {
334 let complete = CutRecord::simple("g", 1, &[("in", 0, 10)]);
335 let partial = CutRecord::simple("g", 1, &[("in", 0, 10)]).partial(&[("in", 1)]);
336 let fetcher = Arc::new(CutFetcher::one_batch(vec![
337 cut_record(0, &complete),
338 cut_record(1, &partial),
339 ]));
340 let mut reader = CutReader::new(fetcher);
341 check!(reader.complete_cuts_after("g", -1).await.unwrap() == vec![]);
342 }
343
344 #[tokio::test]
345 async fn skips_other_kinds_tombstones_and_keyless_records() {
346 let cut = CutRecord::simple("g", 1, &[("in", 0, 10)]);
347 let fetcher = Arc::new(CutFetcher::one_batch(vec![
348 record(
349 0,
350 key_of_kind(0, "g", -1),
351 Some(Bytes::from_static(b"junk")),
352 ),
353 record(1, key_of_kind(1, "g", 1), Some(Bytes::from_static(b"junk"))),
354 FetchedRec {
355 offset: 2,
356 key: None,
357 value: Some(Bytes::from_static(b"junk")),
358 timestamp: -1,
359 },
360 cut_record(3, &cut),
361 record(4, cut_key(&cut), None),
362 ]));
363 let mut reader = CutReader::new(fetcher);
364 check!(reader.complete_cuts_after("g", -1).await.unwrap() == vec![]);
367 }
368
369 #[tokio::test]
370 async fn resumes_at_the_offset_the_previous_read_left() {
371 let first = CutRecord::simple("g", 1, &[("in", 0, 10)]);
372 let second = CutRecord::simple("g", 2, &[("in", 0, 20)]);
373 let fetcher = Arc::new(CutFetcher::one_batch(vec![cut_record(0, &first)]));
374 let mut reader = CutReader::new(Arc::clone(&fetcher) as Arc<dyn RecordFetcher>);
375 check!(reader.complete_cuts_after("g", -1).await.unwrap() == vec![first.expected()]);
376
377 fetcher.script(0, 1, vec![cut_record(1, &second)]);
380 check!(
381 reader.complete_cuts_after("g", -1).await.unwrap()
382 == vec![first.expected(), second.expected()]
383 );
384 }
385
386 #[tokio::test]
387 async fn reads_every_partition_of_the_topic() {
388 let first = CutRecord::simple("g", 1, &[("in", 0, 10)]);
389 let second = CutRecord::simple("g", 2, &[("in", 0, 20)]);
390 let fetcher = Arc::new(CutFetcher {
391 partitions: vec![0, 1],
392 batches: StdMutex::new(HashMap::new()),
393 });
394 fetcher.script(0, 0, vec![cut_record(0, &first)]);
395 fetcher.script(1, 0, vec![cut_record(0, &second)]);
396 let mut reader = CutReader::new(fetcher);
397 check!(
398 reader.complete_cuts_after("g", -1).await.unwrap()
399 == vec![first.expected(), second.expected()]
400 );
401 }
402
403 #[tokio::test]
404 async fn a_malformed_record_fails_the_read() {
405 let fetcher = Arc::new(CutFetcher::one_batch(vec![record(
406 0,
407 Bytes::from_static(b"\x00\x00"),
408 Some(Bytes::from_static(b"x")),
409 )]));
410 let mut reader = CutReader::new(fetcher);
411 assert!(let Err(error) = reader.latest_complete_cut("g").await);
412 check!(error.to_string().contains("truncated barrier state key"));
413 }
414}