1use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13#[derive(
15 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
16)]
17#[serde(rename_all = "snake_case")]
18pub enum DeliveryMode {
19 #[default]
22 AtLeastOnce,
23 ExactlyOnce,
27}
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum ReplayGuarantee {
39 #[default]
43 NonDeterministic,
44 Deterministic,
50}
51
52#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum SinkGuarantee {
56 #[default]
58 AtLeastOnce,
59 KeyedUpsert,
63 AtomicWatermark,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum EffectivelyOnceMechanism {
72 AtomicWatermark,
76 KeyedUpsert,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case", tag = "guarantee", content = "via")]
90pub enum DeliveryGuarantee {
91 AtLeastOnce,
94 EffectivelyOnce(EffectivelyOnceMechanism),
96}
97
98impl std::fmt::Display for DeliveryGuarantee {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 Self::AtLeastOnce => write!(f, "at-least-once"),
102 Self::EffectivelyOnce(EffectivelyOnceMechanism::AtomicWatermark) => {
103 write!(f, "effectively-once (atomic watermark)")
104 }
105 Self::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert) => {
106 write!(f, "effectively-once (keyed upsert)")
107 }
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, Default)]
115pub struct GuaranteeInputs {
116 pub replay: ReplayGuarantee,
118 pub sink_atomic: bool,
121 pub keyed_upsert_configured: bool,
124 pub durable_state: bool,
127 pub dlq: bool,
130}
131
132pub fn derive_delivery_guarantee(i: &GuaranteeInputs) -> DeliveryGuarantee {
140 if i.sink_atomic && i.replay == ReplayGuarantee::Deterministic && i.durable_state && !i.dlq {
141 return DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::AtomicWatermark);
142 }
143 if i.keyed_upsert_configured {
144 return DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert);
145 }
146 DeliveryGuarantee::AtLeastOnce
147}
148
149const EO_MARKER: &str = "__faucet_eo";
151const EO_BOOKMARK: &str = "bookmark";
152const EO_SEQ: &str = "seq";
153
154const TOKEN_WIDTH: usize = 20;
157
158const TOKEN_BOOKMARK_SEP: char = '#';
161
162pub fn format_token(seq: u64) -> String {
164 format!("{seq:0TOKEN_WIDTH$}")
165}
166
167pub fn format_token_with_bookmark(seq: u64, bookmark: Option<&Value>) -> String {
177 match bookmark {
178 Some(bm) => format!("{seq:0TOKEN_WIDTH$}{TOKEN_BOOKMARK_SEP}{bm}"),
179 None => format_token(seq),
180 }
181}
182
183pub fn parse_token(s: &str) -> Option<u64> {
186 let seq = match s.split_once(TOKEN_BOOKMARK_SEP) {
187 Some((prefix, _)) => prefix,
188 None => s,
189 };
190 seq.trim().parse::<u64>().ok()
191}
192
193pub fn parse_token_parts(s: &str) -> Option<(u64, Option<Value>)> {
200 match s.split_once(TOKEN_BOOKMARK_SEP) {
201 Some((prefix, suffix)) => {
202 let seq = prefix.trim().parse::<u64>().ok()?;
203 Some((seq, serde_json::from_str(suffix).ok()))
204 }
205 None => Some((s.trim().parse::<u64>().ok()?, None)),
206 }
207}
208
209pub fn wrap_state(bookmark: Option<&Value>, seq: u64) -> Value {
211 serde_json::json!({
212 EO_MARKER: 1,
213 EO_BOOKMARK: bookmark.cloned().unwrap_or(Value::Null),
214 EO_SEQ: seq,
215 })
216}
217
218pub fn is_eo_envelope(value: &Value) -> bool {
220 value.get(EO_MARKER).and_then(Value::as_u64) == Some(1)
221}
222
223pub fn unwrap_state(value: &Value) -> (Option<Value>, u64) {
231 let peeled = crate::state_version::peel_versioned(value);
232 let value = &peeled;
233 if let Value::Object(map) = value
234 && map.get(EO_MARKER).and_then(Value::as_u64) == Some(1)
235 {
236 let bookmark = match map.get(EO_BOOKMARK) {
237 None | Some(Value::Null) => None,
238 Some(v) => Some(v.clone()),
239 };
240 let seq = map.get(EO_SEQ).and_then(Value::as_u64).unwrap_or(0);
241 return (bookmark, seq);
242 }
243 let bookmark = if value.is_null() {
245 None
246 } else {
247 Some(value.clone())
248 };
249 (bookmark, 0)
250}
251
252pub const COMMIT_TOKEN_TABLE: &str = "_faucet_commit_token";
254pub const COMMIT_TOKEN_SCOPE_COL: &str = "scope";
256pub const COMMIT_TOKEN_TOKEN_COL: &str = "token";
258
259pub const ICEBERG_SCOPE_PROP: &str = "faucet.commit-scope";
261pub const ICEBERG_TOKEN_PROP: &str = "faucet.commit-token";
262
263pub const OVERWRITE_STAGING_SUFFIX: &str = "__faucet_ovw";
272pub const OVERWRITE_STAGING_OLD_SUFFIX: &str = "__faucet_ovw_old";
279
280const SCOPE_DIGEST_LEN: usize = 4 + 64;
283
284pub fn scope_key(scope: &str, max: usize) -> String {
299 if scope.len() <= max {
300 return scope.to_owned();
301 }
302 use sha2::Digest;
303 let digest = sha2::Sha256::digest(scope.as_bytes());
304 let h: String = digest.iter().map(|b| format!("{b:02x}")).collect();
305 let room = max.saturating_sub(SCOPE_DIGEST_LEN);
309 let mut head = 0usize;
310 for (i, _) in scope.char_indices() {
311 if i > room {
312 break;
313 }
314 head = i;
315 }
316 let shortened = format!("{}__h:{h}", &scope[..head]);
317 tracing::debug!(
318 scope_len = scope.len(),
319 max,
320 "exactly-once scope exceeds the sink's key-column width; shortening with a digest"
321 );
322 shortened
323}
324
325pub fn legacy_scope_key(scope: &str, max: usize) -> Option<String> {
329 const LEGACY_DIGEST_LEN: usize = 4 + 16;
330 if scope.len() <= max {
331 return None;
332 }
333 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
334 for b in scope.as_bytes() {
335 h ^= u64::from(*b);
336 h = h.wrapping_mul(0x1000_0000_01b3);
337 }
338 let room = max.saturating_sub(LEGACY_DIGEST_LEN);
339 let mut head = 0usize;
340 for (i, _) in scope.char_indices() {
341 if i > room {
342 break;
343 }
344 head = i;
345 }
346 Some(format!("{}__h:{h:016x}", &scope[..head]))
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use serde_json::json;
353
354 #[test]
355 fn token_round_trips_and_orders_lexicographically() {
356 assert_eq!(format_token(42).len(), TOKEN_WIDTH);
357 assert_eq!(parse_token(&format_token(42)), Some(42));
358 assert_eq!(parse_token(&format_token(0)), Some(0));
359 assert_eq!(parse_token(&format_token(u64::MAX)), Some(u64::MAX));
360 assert!(format_token(9) < format_token(10));
361 assert!(format_token(2) < format_token(1000));
362 }
363
364 #[test]
365 fn parse_token_rejects_garbage() {
366 assert_eq!(parse_token("abc"), None);
367 assert_eq!(parse_token(""), None);
368 }
369
370 #[test]
371 fn wrap_then_unwrap_preserves_bookmark_and_seq() {
372 let bm = json!({"lsn": "0/16B2D58"});
373 let wrapped = wrap_state(Some(&bm), 7);
374 let (got_bm, got_seq) = unwrap_state(&wrapped);
375 assert_eq!(got_bm, Some(bm));
376 assert_eq!(got_seq, 7);
377 }
378
379 #[test]
380 fn wrap_none_bookmark_unwraps_to_none() {
381 let wrapped = wrap_state(None, 3);
382 let (got_bm, got_seq) = unwrap_state(&wrapped);
383 assert_eq!(got_bm, None);
384 assert_eq!(got_seq, 3);
385 }
386
387 #[test]
388 fn legacy_bare_bookmark_unwraps_with_seq_zero() {
389 let (bm, seq) = unwrap_state(&json!("2024-12-01"));
390 assert_eq!(bm, Some(json!("2024-12-01")));
391 assert_eq!(seq, 0);
392 let (bm2, seq2) = unwrap_state(&json!({"updated_at": "2024-12-01"}));
393 assert_eq!(bm2, Some(json!({"updated_at": "2024-12-01"})));
394 assert_eq!(seq2, 0);
395 }
396
397 #[test]
398 fn object_with_non_sentinel_marker_is_treated_as_bare_bookmark() {
399 let v = json!({"__faucet_eo": null, "offset": 500});
402 let (bm, seq) = unwrap_state(&v);
403 assert_eq!(bm, Some(v));
404 assert_eq!(seq, 0);
405 }
406
407 #[test]
408 fn null_value_unwraps_to_none_seq_zero() {
409 let (bm, seq) = unwrap_state(&json!(null));
410 assert_eq!(bm, None);
411 assert_eq!(seq, 0);
412 }
413
414 #[test]
415 fn token_with_bookmark_round_trips() {
416 let bm = json!({"partition_offsets": [{"topic": "t", "partition": 0, "offset": 42}]});
417 let token = format_token_with_bookmark(7, Some(&bm));
418 assert!(token.starts_with(&format_token(7)));
419 assert_eq!(parse_token(&token), Some(7));
420 let (seq, parsed_bm) = parse_token_parts(&token).unwrap();
421 assert_eq!(seq, 7);
422 assert_eq!(parsed_bm, Some(bm));
423 }
424
425 #[test]
426 fn token_with_no_bookmark_is_bare_and_back_compatible() {
427 assert_eq!(format_token_with_bookmark(3, None), format_token(3));
428 let (seq, bm) = parse_token_parts(&format_token(3)).unwrap();
429 assert_eq!((seq, bm), (3, None));
430 }
431
432 #[test]
433 fn token_with_bookmark_orders_lexicographically_on_prefix() {
434 let a = format_token_with_bookmark(9, Some(&json!({"o": 1})));
438 let b = format_token_with_bookmark(10, Some(&json!({"o": 2})));
439 assert!(a < b);
440 }
441
442 #[test]
443 fn parse_token_parts_tolerates_garbage() {
444 assert_eq!(parse_token_parts("abc"), None);
445 assert_eq!(parse_token_parts(""), None);
446 let (seq, bm) = parse_token_parts("00000000000000000005#{not json").unwrap();
448 assert_eq!((seq, bm), (5, None));
449 assert_eq!(parse_token("00000000000000000005#{not json"), Some(5));
451 }
452
453 #[test]
454 fn derive_guarantee_prefers_atomic_then_keyed_then_at_least_once() {
455 use ReplayGuarantee::*;
456 let base = GuaranteeInputs {
457 replay: Deterministic,
458 sink_atomic: true,
459 keyed_upsert_configured: false,
460 durable_state: true,
461 dlq: false,
462 };
463 assert_eq!(
464 derive_delivery_guarantee(&base),
465 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::AtomicWatermark)
466 );
467 let non_det = GuaranteeInputs {
469 replay: NonDeterministic,
470 ..base
471 };
472 assert_eq!(
473 derive_delivery_guarantee(&non_det),
474 DeliveryGuarantee::AtLeastOnce
475 );
476 let keyed = GuaranteeInputs {
478 keyed_upsert_configured: true,
479 ..non_det
480 };
481 assert_eq!(
482 derive_delivery_guarantee(&keyed),
483 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert)
484 );
485 let dlq = GuaranteeInputs {
487 dlq: true,
488 keyed_upsert_configured: true,
489 ..base
490 };
491 assert_eq!(
492 derive_delivery_guarantee(&dlq),
493 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert)
494 );
495 let mem_state = GuaranteeInputs {
496 durable_state: false,
497 ..base
498 };
499 assert_eq!(
500 derive_delivery_guarantee(&mem_state),
501 DeliveryGuarantee::AtLeastOnce
502 );
503 }
504
505 #[test]
506 fn guarantee_display_is_human_readable() {
507 assert_eq!(DeliveryGuarantee::AtLeastOnce.to_string(), "at-least-once");
508 assert_eq!(
509 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::AtomicWatermark)
510 .to_string(),
511 "effectively-once (atomic watermark)"
512 );
513 assert_eq!(
514 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert).to_string(),
515 "effectively-once (keyed upsert)"
516 );
517 }
518
519 #[test]
520 fn capability_enums_default_to_weakest() {
521 assert_eq!(
522 ReplayGuarantee::default(),
523 ReplayGuarantee::NonDeterministic
524 );
525 assert_eq!(SinkGuarantee::default(), SinkGuarantee::AtLeastOnce);
526 }
527
528 #[test]
529 fn delivery_mode_serde_is_snake_case_and_defaults_at_least_once() {
530 assert_eq!(DeliveryMode::default(), DeliveryMode::AtLeastOnce);
531 assert_eq!(
532 serde_json::to_string(&DeliveryMode::ExactlyOnce).unwrap(),
533 "\"exactly_once\""
534 );
535 let m: DeliveryMode = serde_json::from_str("\"at_least_once\"").unwrap();
536 assert_eq!(m, DeliveryMode::AtLeastOnce);
537 }
538
539 #[test]
540 fn overwrite_staging_suffixes_are_pinned() {
541 assert_eq!(OVERWRITE_STAGING_SUFFIX, "__faucet_ovw");
545 assert_eq!(OVERWRITE_STAGING_OLD_SUFFIX, "__faucet_ovw_old");
546 assert_ne!(OVERWRITE_STAGING_SUFFIX, OVERWRITE_STAGING_OLD_SUFFIX);
547 assert!(OVERWRITE_STAGING_OLD_SUFFIX.starts_with(OVERWRITE_STAGING_SUFFIX));
550 }
551}
552
553#[cfg(test)]
554mod scope_key_tests {
555 use super::*;
556
557 #[test]
562 fn scope_key_passes_short_scopes_through_and_shortens_long_ones() {
563 assert_eq!(scope_key("pipe::row", 255), "pipe::row");
566 let exactly = "x".repeat(255);
567 assert_eq!(scope_key(&exactly, 255), exactly);
568
569 let long = format!("pipe::row::{}", "k".repeat(400));
571 let key = scope_key(&long, 255);
572 assert!(key.len() <= 255, "len {}", key.len());
573 assert_ne!(key, long);
574 assert!(key.starts_with("pipe::row::"), "{key}");
576 assert!(key.contains("__h:"), "{key}");
577 }
578
579 #[test]
580 fn scope_key_is_deterministic_and_distinguishes_scopes() {
581 let a = format!("pipe::row::{}", "a".repeat(400));
582 let b = format!("pipe::row::{}", "b".repeat(400));
583 assert_eq!(scope_key(&a, 255), scope_key(&a, 255));
585 assert_ne!(scope_key(&a, 255), scope_key(&b, 255));
588 assert_eq!(&a[..255], &format!("pipe::row::{}", "a".repeat(400))[..255]);
589 }
590
591 #[test]
592 fn scope_key_digest_is_sha256() {
593 let long = format!("pipe::row::{}", "k".repeat(400));
594 let key = scope_key(&long, 255);
595 let (_, digest) = key.rsplit_once("__h:").unwrap();
596 assert_eq!(digest.len(), 64);
597 assert_eq!(
598 &digest[..16],
599 "4c5c3b297497a0a0",
600 "pinned: a changed digest strands every shortened watermark"
601 );
602 }
603
604 #[test]
605 fn legacy_scope_key_reproduces_the_fnv_form() {
606 assert_eq!(legacy_scope_key("short", 64), None);
607 let long = "p::row::".to_string() + &"x".repeat(100);
608 let legacy = legacy_scope_key(&long, 64).unwrap();
609 assert!(legacy.len() <= 64, "{legacy}");
610 assert!(legacy.starts_with("p::row::"), "{legacy}");
611 let digest = legacy.rsplit_once("__h:").unwrap().1;
612 assert_eq!(digest.len(), 16);
613 assert_ne!(legacy, scope_key(&long, 64));
614 assert_eq!(
615 legacy_scope_key("é".repeat(40).as_str(), 30).unwrap().len(),
616 4 + 16 + 10
617 );
618 }
619
620 #[test]
621 fn scope_key_truncates_on_a_char_boundary() {
622 let long = format!("pipé::{}", "é".repeat(400));
624 let key = scope_key(&long, 255);
625 assert!(key.len() <= 255);
626 assert!(key.contains("__h:"), "{key}");
627 }
628}