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 unwrap_state(value: &Value) -> (Option<Value>, u64) {
225 if let Value::Object(map) = value
226 && map.get(EO_MARKER).and_then(Value::as_u64) == Some(1)
227 {
228 let bookmark = match map.get(EO_BOOKMARK) {
229 None | Some(Value::Null) => None,
230 Some(v) => Some(v.clone()),
231 };
232 let seq = map.get(EO_SEQ).and_then(Value::as_u64).unwrap_or(0);
233 return (bookmark, seq);
234 }
235 let bookmark = if value.is_null() {
237 None
238 } else {
239 Some(value.clone())
240 };
241 (bookmark, 0)
242}
243
244pub const COMMIT_TOKEN_TABLE: &str = "_faucet_commit_token";
246pub const COMMIT_TOKEN_SCOPE_COL: &str = "scope";
248pub const COMMIT_TOKEN_TOKEN_COL: &str = "token";
250
251pub const ICEBERG_SCOPE_PROP: &str = "faucet.commit-scope";
253pub const ICEBERG_TOKEN_PROP: &str = "faucet.commit-token";
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use serde_json::json;
259
260 #[test]
261 fn token_round_trips_and_orders_lexicographically() {
262 assert_eq!(format_token(42).len(), TOKEN_WIDTH);
263 assert_eq!(parse_token(&format_token(42)), Some(42));
264 assert_eq!(parse_token(&format_token(0)), Some(0));
265 assert_eq!(parse_token(&format_token(u64::MAX)), Some(u64::MAX));
266 assert!(format_token(9) < format_token(10));
267 assert!(format_token(2) < format_token(1000));
268 }
269
270 #[test]
271 fn parse_token_rejects_garbage() {
272 assert_eq!(parse_token("abc"), None);
273 assert_eq!(parse_token(""), None);
274 }
275
276 #[test]
277 fn wrap_then_unwrap_preserves_bookmark_and_seq() {
278 let bm = json!({"lsn": "0/16B2D58"});
279 let wrapped = wrap_state(Some(&bm), 7);
280 let (got_bm, got_seq) = unwrap_state(&wrapped);
281 assert_eq!(got_bm, Some(bm));
282 assert_eq!(got_seq, 7);
283 }
284
285 #[test]
286 fn wrap_none_bookmark_unwraps_to_none() {
287 let wrapped = wrap_state(None, 3);
288 let (got_bm, got_seq) = unwrap_state(&wrapped);
289 assert_eq!(got_bm, None);
290 assert_eq!(got_seq, 3);
291 }
292
293 #[test]
294 fn legacy_bare_bookmark_unwraps_with_seq_zero() {
295 let (bm, seq) = unwrap_state(&json!("2024-12-01"));
296 assert_eq!(bm, Some(json!("2024-12-01")));
297 assert_eq!(seq, 0);
298 let (bm2, seq2) = unwrap_state(&json!({"updated_at": "2024-12-01"}));
299 assert_eq!(bm2, Some(json!({"updated_at": "2024-12-01"})));
300 assert_eq!(seq2, 0);
301 }
302
303 #[test]
304 fn object_with_non_sentinel_marker_is_treated_as_bare_bookmark() {
305 let v = json!({"__faucet_eo": null, "offset": 500});
308 let (bm, seq) = unwrap_state(&v);
309 assert_eq!(bm, Some(v));
310 assert_eq!(seq, 0);
311 }
312
313 #[test]
314 fn null_value_unwraps_to_none_seq_zero() {
315 let (bm, seq) = unwrap_state(&json!(null));
316 assert_eq!(bm, None);
317 assert_eq!(seq, 0);
318 }
319
320 #[test]
321 fn token_with_bookmark_round_trips() {
322 let bm = json!({"partition_offsets": [{"topic": "t", "partition": 0, "offset": 42}]});
323 let token = format_token_with_bookmark(7, Some(&bm));
324 assert!(token.starts_with(&format_token(7)));
325 assert_eq!(parse_token(&token), Some(7));
326 let (seq, parsed_bm) = parse_token_parts(&token).unwrap();
327 assert_eq!(seq, 7);
328 assert_eq!(parsed_bm, Some(bm));
329 }
330
331 #[test]
332 fn token_with_no_bookmark_is_bare_and_back_compatible() {
333 assert_eq!(format_token_with_bookmark(3, None), format_token(3));
334 let (seq, bm) = parse_token_parts(&format_token(3)).unwrap();
335 assert_eq!((seq, bm), (3, None));
336 }
337
338 #[test]
339 fn token_with_bookmark_orders_lexicographically_on_prefix() {
340 let a = format_token_with_bookmark(9, Some(&json!({"o": 1})));
344 let b = format_token_with_bookmark(10, Some(&json!({"o": 2})));
345 assert!(a < b);
346 }
347
348 #[test]
349 fn parse_token_parts_tolerates_garbage() {
350 assert_eq!(parse_token_parts("abc"), None);
351 assert_eq!(parse_token_parts(""), None);
352 let (seq, bm) = parse_token_parts("00000000000000000005#{not json").unwrap();
354 assert_eq!((seq, bm), (5, None));
355 assert_eq!(parse_token("00000000000000000005#{not json"), Some(5));
357 }
358
359 #[test]
360 fn derive_guarantee_prefers_atomic_then_keyed_then_at_least_once() {
361 use ReplayGuarantee::*;
362 let base = GuaranteeInputs {
363 replay: Deterministic,
364 sink_atomic: true,
365 keyed_upsert_configured: false,
366 durable_state: true,
367 dlq: false,
368 };
369 assert_eq!(
370 derive_delivery_guarantee(&base),
371 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::AtomicWatermark)
372 );
373 let non_det = GuaranteeInputs {
375 replay: NonDeterministic,
376 ..base
377 };
378 assert_eq!(
379 derive_delivery_guarantee(&non_det),
380 DeliveryGuarantee::AtLeastOnce
381 );
382 let keyed = GuaranteeInputs {
384 keyed_upsert_configured: true,
385 ..non_det
386 };
387 assert_eq!(
388 derive_delivery_guarantee(&keyed),
389 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert)
390 );
391 let dlq = GuaranteeInputs {
393 dlq: true,
394 keyed_upsert_configured: true,
395 ..base
396 };
397 assert_eq!(
398 derive_delivery_guarantee(&dlq),
399 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert)
400 );
401 let mem_state = GuaranteeInputs {
402 durable_state: false,
403 ..base
404 };
405 assert_eq!(
406 derive_delivery_guarantee(&mem_state),
407 DeliveryGuarantee::AtLeastOnce
408 );
409 }
410
411 #[test]
412 fn guarantee_display_is_human_readable() {
413 assert_eq!(DeliveryGuarantee::AtLeastOnce.to_string(), "at-least-once");
414 assert_eq!(
415 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::AtomicWatermark)
416 .to_string(),
417 "effectively-once (atomic watermark)"
418 );
419 assert_eq!(
420 DeliveryGuarantee::EffectivelyOnce(EffectivelyOnceMechanism::KeyedUpsert).to_string(),
421 "effectively-once (keyed upsert)"
422 );
423 }
424
425 #[test]
426 fn capability_enums_default_to_weakest() {
427 assert_eq!(
428 ReplayGuarantee::default(),
429 ReplayGuarantee::NonDeterministic
430 );
431 assert_eq!(SinkGuarantee::default(), SinkGuarantee::AtLeastOnce);
432 }
433
434 #[test]
435 fn delivery_mode_serde_is_snake_case_and_defaults_at_least_once() {
436 assert_eq!(DeliveryMode::default(), DeliveryMode::AtLeastOnce);
437 assert_eq!(
438 serde_json::to_string(&DeliveryMode::ExactlyOnce).unwrap(),
439 "\"exactly_once\""
440 );
441 let m: DeliveryMode = serde_json::from_str("\"at_least_once\"").unwrap();
442 assert_eq!(m, DeliveryMode::AtLeastOnce);
443 }
444}