Skip to main content

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;