Skip to main content

krabka_remote_storage_topic/
log.rs

1//! [`MetadataEventLog`]: the publish/subscribe seam between the
2//! [`TopicBasedRemoteLogMetadataManager`](crate::TopicBasedRemoteLogMetadataManager)
3//! and the underlying durable event store.
4//!
5//! The in-process implementation, [`InProcessMetadataEventLog`], is an
6//! in-memory broadcast-channel fixture that unit tests use. It is also a
7//! single-process model for the multi-broker case, because multiple manager
8//! instances that share the same fixture observe each other's writes. The
9//! production Kafka-backed adapter implements the same trait.
10//!
11//! [`MetadataEventLog::subscribe`] does not consume every partition from
12//! offset 0. It takes an explicit [`PartitionStart`] assignment, which is a
13//! subset of partitions, each with its own start offset. It returns an
14//! [`AssignmentHandle`] that can mutate the live assignment at runtime.
15
16use {
17    crate::error::MetadataLogError,
18    async_trait::async_trait,
19    bytes::Bytes,
20    futures_util::stream::Stream,
21    std::{pin::Pin, sync::Arc},
22};
23
24mod in_process;
25
26pub use self::in_process::InProcessMetadataEventLog;
27
28/// One event read from the metadata log.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct MetadataEventRecord {
31    /// Metadata-topic partition the event came from.
32    pub partition: i32,
33    /// Offset within that partition.
34    pub offset: i64,
35    /// Record key. Compacted metadata topics use it to identify superseded
36    /// state; ordinary append-only metadata events leave it unset.
37    pub key: Option<Bytes>,
38    /// Encoded event payload. See [`crate::serde`].
39    pub payload: Bytes,
40    /// Whether Kafka carried a null value for this key.
41    pub tombstone: bool,
42}
43
44/// One keyed record of a [`MetadataEventLog::publish_keyed_group`] call.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct KeyedEvent {
47    /// Metadata-topic partition to append to.
48    pub partition: i32,
49    /// Compaction key.
50    pub key: Bytes,
51    /// Value, or `None` for a tombstone.
52    pub event: Option<Bytes>,
53}
54
55/// Boxed event stream the [`MetadataEventLog`] hands to subscribers.
56pub type MetadataEventStream = Pin<Box<dyn Stream<Item = MetadataEventRecord> + Send + 'static>>;
57
58/// Receives each record of [`MetadataEventLog::visit_range`]. An error stops
59/// the read, and the read returns it.
60pub type RangeVisitor<'a> =
61    dyn FnMut(MetadataEventRecord) -> Result<(), MetadataLogError> + Send + 'a;
62
63/// One partition to consume and the offset to begin at (inclusive).
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct PartitionStart {
66    /// Metadata-topic partition to consume.
67    pub partition: i32,
68    /// First offset to deliver (inclusive). `0` replays from the start.
69    pub start_offset: i64,
70}
71
72impl PartitionStart {
73    /// Consume one partition beginning at the inclusive offset.
74    #[must_use]
75    pub const fn new(partition: i32, start_offset: i64) -> Self {
76        Self {
77            partition,
78            start_offset,
79        }
80    }
81}
82
83/// Runtime control over a live [`MetadataEventLog`] subscription's assigned
84/// partition set. [`MetadataEventLog::subscribe`] returns it together with
85/// the stream.
86pub trait AssignmentHandle: Send + Sync {
87    /// Begin to consume `start.partition` from `start.start_offset`. This
88    /// method does nothing if the partition is already assigned. A
89    /// newly-added partition emits its backlog from `start_offset` into the
90    /// existing stream, and then live records.
91    fn add(&self, start: PartitionStart);
92    /// Stop the consumption of `partition` and stop the emission of its
93    /// events. This method does nothing if the partition is not currently
94    /// assigned.
95    fn remove(&self, partition: i32);
96    /// Current assigned partition set (unordered).
97    fn assigned(&self) -> Vec<i32>;
98}
99
100/// Publish/subscribe transport that backs the `__remote_log_metadata`
101/// topic.
102///
103/// Implementations must guarantee:
104///
105/// - `publish(p, _)` resolves to a monotonically-increasing offset
106///   within partition `p`, and the assigned offset is also what every
107///   subscriber observes for that record.
108/// - The stream returned by `subscribe` replays each assigned
109///   partition's backlog from its `start_offset` and then forwards new
110///   records as they are published for currently-assigned partitions.
111///   Subscribers attached after some records were already published
112///   still see the history at/after their start offset.
113/// - Records are delivered in publish order on a per-partition basis.
114#[async_trait]
115pub trait MetadataEventLog: Send + Sync {
116    /// Number of partitions the log holds. It is stable for the lifetime of
117    /// the log. The manager hashes user partitions into
118    /// `[0, partition_count())`.
119    fn partition_count(&self) -> i32;
120
121    /// Append `event` to `partition`. Resolves to the assigned offset.
122    ///
123    /// # Errors
124    ///
125    /// Returns [`MetadataLogError`] if the transport refused the write. That
126    /// happens when the partition is out of range, or when the log has been
127    /// closed.
128    async fn publish(&self, partition: i32, event: Bytes) -> Result<i64, MetadataLogError>;
129
130    /// Append a keyed value or tombstone to a compacted metadata topic.
131    ///
132    /// The default rejects keyed publication. A transport used for compaction
133    /// must override this method.
134    async fn publish_keyed(
135        &self,
136        _partition: i32,
137        _key: Bytes,
138        _event: Option<Bytes>,
139    ) -> Result<i64, MetadataLogError> {
140        Err(MetadataLogError::Other(
141            "metadata log does not support keyed publication".into(),
142        ))
143    }
144
145    /// Append a group of keyed records and wait until every one of them is
146    /// acknowledged. Resolves to their offsets, in the order given.
147    ///
148    /// Records for the same partition are appended in the order given. A
149    /// transport that pipelines writes submits the whole group before it
150    /// waits, so the group costs one acknowledgment round trip rather than
151    /// one per record. It still waits for every submitted record before it
152    /// returns, so none is left in flight after an error.
153    ///
154    /// The default publishes the records one at a time through
155    /// [`Self::publish_keyed`] and stops at the first failure.
156    ///
157    /// # Errors
158    ///
159    /// Returns the first [`MetadataLogError`] that any record met. Any subset
160    /// of the group may then have been appended.
161    async fn publish_keyed_group(
162        &self,
163        events: Vec<KeyedEvent>,
164    ) -> Result<Vec<i64>, MetadataLogError> {
165        let mut offsets = Vec::with_capacity(events.len());
166        for KeyedEvent {
167            partition,
168            key,
169            event,
170        } in events
171        {
172            offsets.push(self.publish_keyed(partition, key, event).await?);
173        }
174        Ok(offsets)
175    }
176
177    /// Start to consume the given partitions, each from its start offset,
178    /// which is inclusive. Returns the event stream and a handle to mutate
179    /// the live assignment.
180    ///
181    /// The stream replays each assigned partition's backlog from its
182    /// `start_offset`, then forwards live appends for the currently
183    /// assigned partitions. Records are delivered in publish order on
184    /// a per-partition basis.
185    fn subscribe(
186        &self,
187        assignment: Vec<PartitionStart>,
188    ) -> (MetadataEventStream, Arc<dyn AssignmentHandle>);
189
190    /// Earliest retained offset for each partition, indexed by partition.
191    ///
192    /// Append-only implementations retain offset zero. Compacted transports
193    /// override this so a replay does not wait forever on deleted offsets.
194    async fn low_water_marks(&self) -> Result<Vec<i64>, MetadataLogError> {
195        // A negative count yields no partitions, as an empty range would.
196        Ok(vec![
197            0;
198            usize::try_from(self.partition_count()).unwrap_or(0)
199        ])
200    }
201
202    /// One past the highest written offset for each partition,
203    /// indexed by partition.
204    ///
205    /// # Errors
206    ///
207    /// Returns [`MetadataLogError`] only on an underlying store failure. An
208    /// empty partition is `0`, not an error.
209    async fn high_water_marks(&self) -> Result<Vec<i64>, MetadataLogError>;
210
211    /// Pass every retained record of `partition` whose offset lies in
212    /// `[start, end)` to `visit`, in offset order.
213    ///
214    /// The read neither subscribes nor writes, so a reader that holds only
215    /// `READ` and `DESCRIBE` on the topic can use it. A transport hands
216    /// records over as it fetches them and holds at most one fetched page, so
217    /// the range can be larger than memory. A compacted transport steps over
218    /// the offsets compaction removed and still returns once it reaches `end`.
219    /// An empty range visits nothing.
220    ///
221    /// A networked transport retries transient failures, such as a leader
222    /// change, without a bound of its own. Bound the read with a deadline.
223    ///
224    /// The default reports that the transport does not serve range reads.
225    ///
226    /// # Errors
227    ///
228    /// Returns [`MetadataLogError::PartitionOutOfRange`] for a partition
229    /// outside the log, the first error that `visit` returns, and
230    /// [`MetadataLogError`] when the store fails or stops short of `end`.
231    async fn visit_range(
232        &self,
233        partition: i32,
234        start: i64,
235        end: i64,
236        visit: &mut RangeVisitor<'_>,
237    ) -> Result<(), MetadataLogError> {
238        let _ = (partition, start, end, visit);
239        Err(MetadataLogError::Other(
240            "metadata log does not serve range reads".into(),
241        ))
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use {
248        super::*,
249        std::sync::atomic::{AtomicUsize, Ordering},
250    };
251
252    struct AppendOnlyLog(AtomicUsize);
253
254    #[async_trait]
255    impl MetadataEventLog for AppendOnlyLog {
256        fn partition_count(&self) -> i32 { 1 }
257
258        async fn publish(&self, _partition: i32, _event: Bytes) -> Result<i64, MetadataLogError> {
259            self.0.fetch_add(1, Ordering::Relaxed);
260            Ok(7)
261        }
262
263        fn subscribe(
264            &self,
265            _assignment: Vec<PartitionStart>,
266        ) -> (MetadataEventStream, Arc<dyn AssignmentHandle>) {
267            unreachable!("not used by these tests")
268        }
269
270        async fn high_water_marks(&self) -> Result<Vec<i64>, MetadataLogError> { Ok(vec![0]) }
271    }
272
273    #[tokio::test]
274    async fn default_keyed_publish_rejects_values_and_tombstones_without_appending() {
275        let log = AppendOnlyLog(AtomicUsize::new(0));
276        assert2::assert!(log.partition_count() == 1);
277        assert2::assert!(log.high_water_marks().await.unwrap() == vec![0]);
278        assert2::assert!(
279            log.publish(0, Bytes::from_static(b"append-only"))
280                .await
281                .unwrap()
282                == 7
283        );
284        let value_error = log
285            .publish_keyed(
286                0,
287                Bytes::from_static(b"key"),
288                Some(Bytes::from_static(b"value")),
289            )
290            .await
291            .unwrap_err();
292        assert2::assert!(
293            value_error
294                .to_string()
295                .contains("does not support keyed publication")
296        );
297
298        let error = log
299            .publish_keyed(0, Bytes::from_static(b"key"), None)
300            .await
301            .unwrap_err();
302        assert2::assert!(
303            error
304                .to_string()
305                .contains("does not support keyed publication")
306        );
307        assert2::assert!(log.0.load(Ordering::Relaxed) == 1);
308    }
309
310    #[tokio::test]
311    async fn default_range_read_refuses_without_visiting() {
312        let mut visits = 0;
313        let error = AppendOnlyLog(AtomicUsize::new(0))
314            .visit_range(0, 0, 1, &mut |_record| {
315                visits += 1;
316                Ok(())
317            })
318            .await
319            .unwrap_err();
320
321        assert2::assert!(
322            error.to_string() == "metadata log error: metadata log does not serve range reads"
323        );
324        assert2::assert!(visits == 0);
325    }
326}