1#![allow(clippy::disallowed_types)] use std::collections::HashMap;
6use std::fmt;
7use std::num::NonZeroU64;
8use std::sync::{Arc, OnceLock};
9
10enum OffsetTree {
11 Leaf(Arc<str>),
12 Branch { older: Arc<Self>, newer: Arc<Self> },
13}
14
15impl OffsetTree {
16 fn write_to(&self, output: &mut String, separator: &str, first: &mut bool) {
17 match self {
18 Self::Leaf(value) => {
19 if *first {
20 *first = false;
21 } else {
22 output.push_str(separator);
23 }
24 output.push_str(value);
25 }
26 Self::Branch { older, newer } => {
27 older.write_to(output, separator, first);
28 newer.write_to(output, separator, first);
29 }
30 }
31 }
32}
33
34#[derive(Clone)]
39pub struct PersistentOffset {
40 trees: Arc<Vec<Option<Arc<OffsetTree>>>>,
43 prefix: Arc<str>,
44 separator: Arc<str>,
45 suffix: Arc<str>,
46 fragment_count: usize,
47 fragment_bytes: usize,
48}
49
50impl PersistentOffset {
51 #[must_use]
56 pub fn new(
57 prefix: impl Into<Arc<str>>,
58 separator: impl Into<Arc<str>>,
59 suffix: impl Into<Arc<str>>,
60 ) -> Self {
61 Self {
62 trees: Arc::new(Vec::new()),
63 prefix: prefix.into(),
64 separator: separator.into(),
65 suffix: suffix.into(),
66 fragment_count: 0,
67 fragment_bytes: 0,
68 }
69 }
70
71 pub fn push_fragment(&mut self, fragment: impl Into<Arc<str>>) {
74 let value = fragment.into();
75 self.fragment_bytes = self.fragment_bytes.saturating_add(value.len());
79 self.fragment_count = self.fragment_count.saturating_add(1);
80
81 let trees = Arc::make_mut(&mut self.trees);
82 let mut carry = Arc::new(OffsetTree::Leaf(value));
83 let mut level = 0;
84 loop {
85 if level == trees.len() {
86 trees.push(Some(carry));
87 break;
88 }
89 if let Some(older) = trees[level].take() {
90 carry = Arc::new(OffsetTree::Branch {
91 older,
92 newer: carry,
93 });
94 level += 1;
95 } else {
96 trees[level] = Some(carry);
97 break;
98 }
99 }
100 }
101
102 #[must_use]
104 pub const fn fragment_count(&self) -> usize {
105 self.fragment_count
106 }
107
108 fn materialize(&self) -> String {
109 let separator_bytes = self
110 .separator
111 .len()
112 .saturating_mul(self.fragment_count.saturating_sub(1));
113 let capacity = self
114 .prefix
115 .len()
116 .saturating_add(self.fragment_bytes)
117 .saturating_add(separator_bytes)
118 .saturating_add(self.suffix.len());
119 let mut output = String::with_capacity(capacity);
120 output.push_str(&self.prefix);
121 let mut first = true;
122 for tree in self.trees.iter().rev().flatten() {
123 tree.write_to(&mut output, &self.separator, &mut first);
124 }
125 output.push_str(&self.suffix);
126 output
127 }
128}
129
130impl fmt::Debug for PersistentOffset {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.debug_struct("PersistentOffset")
133 .field("fragment_count", &self.fragment_count)
134 .field("fragment_bytes", &self.fragment_bytes)
135 .finish_non_exhaustive()
136 }
137}
138
139pub struct SourceCheckpoint {
152 offsets: HashMap<String, String>,
154
155 persistent_offsets: HashMap<String, PersistentOffset>,
157
158 materialized_offsets: Arc<OnceLock<HashMap<String, String>>>,
160
161 metadata: HashMap<String, String>,
163
164 assignment_version: Option<NonZeroU64>,
166}
167
168impl Clone for SourceCheckpoint {
169 fn clone(&self) -> Self {
170 Self {
171 offsets: self.offsets.clone(),
172 persistent_offsets: self.persistent_offsets.clone(),
173 materialized_offsets: Arc::clone(&self.materialized_offsets),
174 metadata: self.metadata.clone(),
175 assignment_version: self.assignment_version,
176 }
177 }
178}
179
180impl Default for SourceCheckpoint {
181 fn default() -> Self {
182 Self {
183 offsets: HashMap::new(),
184 persistent_offsets: HashMap::new(),
185 materialized_offsets: Arc::new(OnceLock::new()),
186 metadata: HashMap::new(),
187 assignment_version: None,
188 }
189 }
190}
191
192impl fmt::Debug for SourceCheckpoint {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.debug_struct("SourceCheckpoint")
195 .field("offsets", &self.offsets)
196 .field(
197 "persistent_offset_keys",
198 &self.persistent_offsets.keys().collect::<Vec<_>>(),
199 )
200 .field("metadata", &self.metadata)
201 .field("assignment_version", &self.assignment_version)
202 .finish_non_exhaustive()
203 }
204}
205
206impl PartialEq for SourceCheckpoint {
207 fn eq(&self, other: &Self) -> bool {
208 self.offsets() == other.offsets()
209 && self.metadata == other.metadata
210 && self.assignment_version == other.assignment_version
211 }
212}
213
214impl Eq for SourceCheckpoint {}
215
216impl SourceCheckpoint {
217 #[must_use]
219 pub fn new() -> Self {
220 Self::default()
221 }
222
223 #[must_use]
225 pub fn with_offsets(offsets: HashMap<String, String>) -> Self {
226 Self {
227 offsets,
228 persistent_offsets: HashMap::new(),
229 materialized_offsets: Arc::new(OnceLock::new()),
230 metadata: HashMap::new(),
231 assignment_version: None,
232 }
233 }
234
235 pub fn set_offset(&mut self, key: impl Into<String>, value: impl Into<String>) {
237 let key = key.into();
238 self.persistent_offsets.remove(&key);
239 self.offsets.insert(key, value.into());
240 self.invalidate_materialized_offsets();
241 }
242
243 pub fn set_persistent_offset(&mut self, key: impl Into<String>, value: PersistentOffset) {
249 let key = key.into();
250 self.offsets.remove(&key);
251 self.persistent_offsets.insert(key, value);
252 self.invalidate_materialized_offsets();
253 }
254
255 #[must_use]
257 pub fn get_offset(&self, key: &str) -> Option<&str> {
258 if let Some(value) = self.offsets.get(key) {
259 return Some(value);
260 }
261 if !self.persistent_offsets.contains_key(key) {
262 return None;
263 }
264 self.offsets().get(key).map(String::as_str)
265 }
266
267 #[must_use]
269 pub fn offsets(&self) -> &HashMap<String, String> {
270 if self.persistent_offsets.is_empty() {
271 return &self.offsets;
272 }
273 self.materialized_offsets.get_or_init(|| {
274 let mut offsets = self.offsets.clone();
275 offsets.extend(
276 self.persistent_offsets
277 .iter()
278 .map(|(key, value)| (key.clone(), value.materialize())),
279 );
280 offsets
281 })
282 }
283
284 #[must_use]
291 pub fn durable_offsets(&self) -> HashMap<String, String> {
292 if let Some(offsets) = self.materialized_offsets.get() {
293 return offsets.clone();
294 }
295 let mut offsets = self.offsets.clone();
296 offsets.extend(
297 self.persistent_offsets
298 .iter()
299 .map(|(key, value)| (key.clone(), value.materialize())),
300 );
301 offsets
302 }
303
304 pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
306 self.metadata.insert(key.into(), value.into());
307 }
308
309 #[must_use]
311 pub fn get_metadata(&self, key: &str) -> Option<&str> {
312 self.metadata.get(key).map(String::as_str)
313 }
314
315 #[must_use]
317 pub fn metadata(&self) -> &HashMap<String, String> {
318 &self.metadata
319 }
320
321 pub fn bind_assignment_version(&mut self, assignment_version: NonZeroU64) {
323 self.assignment_version = Some(assignment_version);
324 }
325
326 #[must_use]
328 pub const fn assignment_version(&self) -> Option<NonZeroU64> {
329 self.assignment_version
330 }
331
332 #[must_use]
334 pub fn is_empty(&self) -> bool {
335 self.offsets.is_empty() && self.persistent_offsets.is_empty()
336 }
337
338 fn invalidate_materialized_offsets(&mut self) {
339 self.materialized_offsets = Arc::new(OnceLock::new());
340 }
341
342 #[cfg(test)]
343 fn offsets_are_materialized(&self) -> bool {
344 self.materialized_offsets.get().is_some()
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
353 fn test_source_checkpoint_basic() {
354 let mut cp = SourceCheckpoint::new();
355 cp.set_offset("events:0", "1234");
356 cp.set_offset("events:1", "5678");
357
358 assert_eq!(cp.get_offset("events:0"), Some("1234"));
359 assert_eq!(cp.get_offset("events:1"), Some("5678"));
360 assert_eq!(cp.get_offset("events:2"), None);
361 assert!(!cp.is_empty());
362 }
363
364 #[test]
365 fn test_source_checkpoint_with_offsets() {
366 let mut offsets = HashMap::new();
367 offsets.insert("lsn".to_string(), "0/1234ABCD".to_string());
368
369 let cp = SourceCheckpoint::with_offsets(offsets);
370 assert_eq!(cp.get_offset("lsn"), Some("0/1234ABCD"));
371 }
372
373 #[test]
374 fn test_source_checkpoint_metadata() {
375 let mut cp = SourceCheckpoint::new();
376 cp.set_metadata("connector", "kafka");
377 cp.set_metadata("topic", "events");
378
379 assert_eq!(cp.get_metadata("connector"), Some("kafka"));
380 assert_eq!(cp.get_metadata("topic"), Some("events"));
381 }
382
383 #[test]
384 fn source_checkpoint_assignment_version_is_typed_and_part_of_identity() {
385 let mut checkpoint = SourceCheckpoint::new();
386 assert_eq!(checkpoint.assignment_version(), None);
387
388 let version = NonZeroU64::new(7).unwrap();
389 checkpoint.bind_assignment_version(version);
390 assert_eq!(checkpoint.assignment_version(), Some(version));
391 assert_eq!(checkpoint.clone(), checkpoint);
392 assert!(format!("{checkpoint:?}").contains("assignment_version: Some(7)"));
393
394 let unbound = SourceCheckpoint::new();
395 assert_ne!(checkpoint, unbound);
396 assert_eq!(SourceCheckpoint::default().assignment_version(), None);
397 }
398
399 #[test]
400 fn test_empty_checkpoint() {
401 let cp = SourceCheckpoint::new();
402 assert!(cp.is_empty());
403 }
404
405 #[test]
406 fn persistent_offset_is_lazy_and_durable_conversion_does_not_cache_a_copy() {
407 let mut value = PersistentOffset::new("[", ",", "]");
408 value.push_fragment(r#""first""#);
409 value.push_fragment(r#""second""#);
410 let mut cp = SourceCheckpoint::new();
411 cp.set_offset("small", "7");
412 cp.set_persistent_offset("large", value);
413
414 assert!(!cp.offsets_are_materialized());
415 assert_eq!(cp.get_offset("small"), Some("7"));
416 assert!(!cp.offsets_are_materialized());
417 let durable = cp.durable_offsets();
418 assert_eq!(
419 durable.get("large").map(String::as_str),
420 Some(r#"["first","second"]"#)
421 );
422 assert!(!cp.offsets_are_materialized());
423
424 assert_eq!(cp.get_offset("large"), Some(r#"["first","second"]"#));
425 assert!(cp.offsets_are_materialized());
426 }
427
428 #[test]
429 fn persistent_offset_snapshots_are_immutable() {
430 let mut value = PersistentOffset::new("[", ",", "]");
431 value.push_fragment("1");
432 let snapshot = value.clone();
433 value.push_fragment("2");
434
435 let mut old = SourceCheckpoint::new();
436 old.set_persistent_offset("values", snapshot);
437 let mut new = SourceCheckpoint::new();
438 new.set_persistent_offset("values", value);
439 assert_eq!(old.get_offset("values"), Some("[1]"));
440 assert_eq!(new.get_offset("values"), Some("[1,2]"));
441 }
442
443 #[test]
444 fn persistent_offset_preserves_order_across_tree_carries() {
445 let mut value = PersistentOffset::new("[", ",", "]");
446 for fragment in 0..19 {
447 value.push_fragment(fragment.to_string());
448 }
449 let mut checkpoint = SourceCheckpoint::new();
450 checkpoint.set_persistent_offset("values", value);
451 assert_eq!(
452 checkpoint.get_offset("values"),
453 Some("[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]")
454 );
455 }
456
457 #[test]
458 fn eager_offsets_do_not_allocate_a_compatibility_copy() {
459 let mut checkpoint = SourceCheckpoint::new();
460 checkpoint.set_offset("partition", "42");
461 assert_eq!(
462 checkpoint.offsets().get("partition").map(String::as_str),
463 Some("42")
464 );
465 assert!(!checkpoint.offsets_are_materialized());
466 }
467
468 #[test]
469 fn mutating_a_checkpoint_does_not_change_its_clone() {
470 let mut original = SourceCheckpoint::new();
471 original.set_offset("position", "1");
472 let snapshot = original.clone();
473 original.set_offset("position", "2");
474
475 assert_eq!(snapshot.get_offset("position"), Some("1"));
476 assert_eq!(original.get_offset("position"), Some("2"));
477 }
478}