krabka_log/log/stamp.rs
1//! The internal stamp coordinate: source injection, the `.stampindex`
2//! sidecars, and the stamp lookups.
3//!
4//! A partition stamps nothing until a [`StampSource`] is injected, and the
5//! stamp never reaches the wire, so this module is the whole surface of a
6//! feature that leaves the Kafka-visible bytes untouched.
7
8use {
9 super::{
10 Log,
11 control::{COMMIT_CONTROL_TYPE, batch_control_marker_type},
12 },
13 crate::{
14 error::LogError,
15 stamp_index::{OffsetRange, StampEntry, StampIndex},
16 stamp_source::StampSource,
17 },
18 krabka_ids::Offset,
19 krabka_protocol::records::RecordBatch,
20 std::{collections::BTreeMap, path::PathBuf, sync::Arc},
21};
22
23impl Log {
24 /// Inject the [`StampSource`] that folds the additional internal stamp
25 /// coordinate into this partition.
26 ///
27 /// This method opens or recovers every local segment's `.stampindex`.
28 /// From this point each durable non-transactional batch is stamped at
29 /// append, while transactional batches are stamped together when their
30 /// COMMIT marker lands. Aborted and still-open transactions stay
31 /// unstamped.
32 ///
33 /// This is the only switch that enables the feature. With no source
34 /// injected the log stamps nothing and its bytes are identical to those
35 /// of an unstamped log. It changes no wire-facing state: not the `.log`
36 /// bytes, not offset assignment, not the LSO, and not the
37 /// high-watermark.
38 ///
39 /// # Errors
40 /// Returns an error when opening the `.stampindex` sidecar fails.
41 /// # Panics
42 /// Panics only if there is no active segment, which cannot happen after
43 /// [`Log::open`].
44 pub fn set_stamp_source(&mut self, source: Arc<dyn StampSource>) -> Result<(), LogError> {
45 let mut indexes = self
46 .segments
47 .iter()
48 .chain(self.active.iter())
49 .map(|segment| {
50 let mut index = StampIndex::open(segment.stamp_index_path())?;
51 index.set_io(Arc::clone(&self.io));
52 Ok((segment.base_offset(), index))
53 })
54 .collect::<Result<BTreeMap<_, _>, LogError>>()?;
55 let pending_ranges: Vec<OffsetRange> = self
56 .pending_stamp_ranges
57 .values()
58 .flatten()
59 .copied()
60 .collect();
61 for index in indexes.values_mut() {
62 index.remove_ranges(&pending_ranges)?;
63 }
64 if let Some(horizon) = indexes
65 .values()
66 .flat_map(StampIndex::entries)
67 .map(|entry| entry.stamp)
68 .max()
69 {
70 source.observe(horizon);
71 }
72 self.stamp_indexes = indexes;
73 self.stamp_source = Some(source);
74 Ok(())
75 }
76
77 /// Clone the currently installed internal stamp source.
78 ///
79 /// Log-directory moves use this to preserve stamping when they close and
80 /// reopen a partition around the final filesystem swap.
81 #[must_use]
82 pub fn stamp_source(&self) -> Option<Arc<dyn StampSource>> {
83 self.stamp_source.as_ref().map(Arc::clone)
84 }
85
86 /// The internal stamp that covers `offset`, or `None` when there is no
87 /// such stamp.
88 ///
89 /// The result is `None` when the partition is unstamped, that is, when no
90 /// source is injected. It is also `None` when no stamped range covers
91 /// `offset`. This method searches all local segment indexes because a
92 /// transaction can commit after the segment holding its data has rolled.
93 /// This is an internal, server-side query. No produce or fetch handler
94 /// calls it, so the stamp can never reach a client-facing response.
95 #[must_use]
96 pub fn stamp_for_offset(&self, offset: Offset) -> Option<u64> {
97 self.stamp_indexes
98 .values()
99 .find_map(|index| index.stamp_for_offset(offset))
100 }
101
102 /// Record a stamped `[base, last]` range for a durably-appended data
103 /// batch.
104 ///
105 /// This method does nothing when no [`StampSource`] is injected.
106 pub(super) fn record_stamp(&mut self, base: Offset, last: Offset) -> Result<(), LogError> {
107 let Some(source) = self.stamp_source.as_ref() else {
108 return Ok(());
109 };
110 self.record_stamp_value(base, last, source.next_stamp())
111 }
112
113 /// Record a supplied commit stamp for one transactional data range.
114 pub(super) fn record_stamp_value(
115 &mut self,
116 base: Offset,
117 last: Offset,
118 stamp: u64,
119 ) -> Result<(), LogError> {
120 if self.stamp_source.is_none() {
121 return Ok(());
122 }
123 // Retention can remove an old transactional batch before its marker
124 // arrives. There is no local data left to stamp in that case, and the
125 // marker must still be allowed to close the transaction.
126 if last < self.local_log_start_offset() {
127 return Ok(());
128 }
129 let (_, index) = self
130 .stamp_indexes
131 .range_mut(..=base)
132 .next_back()
133 .ok_or_else(|| {
134 LogError::Corrupt(format!(
135 "no stamp index segment covers transactional range {base}..={last}"
136 ))
137 })?;
138 index.upsert(StampEntry {
139 base_offset: base,
140 last_offset: last,
141 stamp,
142 })
143 }
144
145 /// Reopen the active `.stampindex` for a new active segment.
146 ///
147 /// This mirrors the `active_txn_index` reopen on roll, truncate, and
148 /// reset. When no source is injected, this method clears the index, which
149 /// is already absent, and does no I/O.
150 ///
151 /// # Errors
152 /// Returns an error when opening the `.stampindex` sidecar fails.
153 pub(super) fn reopen_active_stamp_index(
154 &mut self,
155 base_offset: Offset,
156 path: PathBuf,
157 ) -> Result<(), LogError> {
158 if self.stamp_source.is_some() {
159 let mut index = StampIndex::open(path)?;
160 index.set_io(Arc::clone(&self.io));
161 self.stamp_indexes.insert(base_offset, index);
162 }
163 Ok(())
164 }
165
166 pub(super) fn validate_commit_stamp_batch(&self, batch: &RecordBatch) -> Result<(), LogError> {
167 let is_commit = batch.attributes.is_control_batch()
168 && batch_control_marker_type(batch)? == Some(COMMIT_CONTROL_TYPE);
169 if !is_commit {
170 return Err(LogError::InvalidArgument(
171 "an explicit transaction stamp requires a COMMIT control batch".into(),
172 ));
173 }
174 if self.stamp_source.is_none() {
175 return Err(LogError::InvalidArgument(
176 "an explicit transaction stamp requires an installed stamp source".into(),
177 ));
178 }
179 Ok(())
180 }
181
182 pub(super) fn observe_stamp(&self, stamp: u64) {
183 if let Some(source) = &self.stamp_source {
184 source.observe(stamp);
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests;