krabka_log/stamp_source.rs
1//! The injected stamp source. It is the seam through which a partition's log
2//! gets the additional internal `TimestampSource` coordinate that it records
3//! in the `.stampindex` sidecar.
4//!
5//! The trait is deliberately tiny, and it lives in `krabka-log` rather than in
6//! the broker. The log owns both the append seam that folds stamps in and the
7//! `.stampindex` that they land in. The seam therefore stays here, so tests
8//! can check the log's own stamp behavior with a deterministic in-crate
9//! source, and the log needs no dependency edge onto a real clock. The broker
10//! injects the tenant source when internal stamping is enabled. A log with no
11//! injected source stamps nothing and behaves exactly as before.
12
13use std::fmt::Debug;
14
15/// A source of monotonic internal stamps folded into a partition's
16/// `.stampindex`.
17///
18/// The log calls [`StampSource::next_stamp`] for each
19/// non-transactional batch at append time and for each pure-Kafka transaction
20/// when its commit marker is durable. A cross-domain coordinator supplies its
21/// own commit stamp, which the log folds into this source with
22/// [`StampSource::observe`].
23///
24/// Implementations use interior mutability, because the log holds the source
25/// behind a shared `Arc` and stamps through `&mut self` appends. They must be
26/// cheap. They must never read or alter the wire-exact `.log` bytes, the
27/// offset assignment, the LSO, or the high-watermark. The stamp is an
28/// additional server-side coordinate only.
29pub trait StampSource: Debug + Send + Sync {
30 /// Return the next stamp.
31 fn next_stamp(&self) -> u64;
32
33 /// Fold an externally allocated commit stamp into this source so every
34 /// later allocation is greater than it.
35 ///
36 /// Centralized sources can keep the default no-op because they are the
37 /// sole timestamp authority. Distributed clocks override this with their
38 /// Lamport/HLC receive rule.
39 fn observe(&self, _stamp: u64) {}
40}
41
42#[cfg(any(test, feature = "test-helpers"))]
43mod test_sources {
44 use {
45 super::StampSource,
46 std::sync::atomic::{AtomicU64, Ordering},
47 };
48
49 /// A deterministic monotonic stamp source for tests.
50 ///
51 /// It yields `start`,
52 /// `start + step`, `start + 2*step`, and so on, on successive calls. The
53 /// sequence is fully predictable, so tests can assert the exact stamps
54 /// that a sequence of appends writes into the `.stampindex`.
55 #[derive(Debug)]
56 pub struct MonotonicStampSource {
57 next: AtomicU64,
58 step: u64,
59 }
60
61 impl MonotonicStampSource {
62 /// Create a source that yields `start` first and then advances by
63 /// `step` on each later call.
64 #[must_use]
65 pub fn new(start: u64, step: u64) -> Self {
66 Self {
67 next: AtomicU64::new(start),
68 step,
69 }
70 }
71 }
72
73 impl StampSource for MonotonicStampSource {
74 fn next_stamp(&self) -> u64 { self.next.fetch_add(self.step, Ordering::Relaxed) }
75
76 fn observe(&self, stamp: u64) {
77 let next = stamp.saturating_add(self.step);
78 self.next.fetch_max(next, Ordering::Relaxed);
79 }
80 }
81}
82
83#[cfg(any(test, feature = "test-helpers"))]
84pub use test_sources::MonotonicStampSource;