Skip to main content

krabka_remote_storage/
inmemory.rs

1//! [`InmemoryRemoteLogMetadataManager`] is a process-memory reference
2//! [`RemoteLogMetadataManager`]. It mirrors Kafka's test fixture of the same
3//! name. Tiered-storage tests run against this manager.
4
5use {
6    crate::{
7        cache::{RemoteLogMetadataCache, sort_by_start_offset},
8        dump::{PartitionDump, RlmmCacheDump},
9        error::RemoteStorageError,
10        metadata::{
11            RemoteLogSegmentMetadata, RemoteLogSegmentMetadataUpdate,
12            RemotePartitionDeleteMetadata, RemotePartitionDeleteState, TopicIdPartition,
13        },
14        metadata_manager::RemoteLogMetadataManager,
15    },
16    krabka_ids::LeaderEpoch,
17    std::{
18        collections::HashMap,
19        sync::{Mutex, PoisonError},
20    },
21};
22
23/// In-memory [`RemoteLogMetadataManager`]: one `RemoteLogMetadataCache` per
24/// partition behind a single mutex.
25///
26/// It is not durable, and the state is lost on restart. It does enforce the
27/// full lifecycle state machine, so it is a faithful stand-in for the
28/// topic-backed production manager in tests and single-process setups.
29#[derive(Debug, Default)]
30pub struct InmemoryRemoteLogMetadataManager {
31    partitions: Mutex<HashMap<TopicIdPartition, RemoteLogMetadataCache>>,
32}
33
34impl InmemoryRemoteLogMetadataManager {
35    /// Constructs an empty manager.
36    #[must_use]
37    pub fn new() -> Self { Self::default() }
38
39    fn list_segments(
40        &self,
41        topic_id_partition: &TopicIdPartition,
42        query: impl FnOnce(&RemoteLogMetadataCache) -> Vec<RemoteLogSegmentMetadata>,
43    ) -> Vec<RemoteLogSegmentMetadata> {
44        let guard = self
45            .partitions
46            .lock()
47            .unwrap_or_else(PoisonError::into_inner);
48        guard.get(topic_id_partition).map_or_default(query)
49    }
50
51    /// Dumps every partition's segment and partition-delete metadata for a
52    /// snapshot. The result is order-independent. [`Self::import`] re-derives
53    /// the order and the epoch index.
54    #[must_use]
55    pub fn export(&self) -> RlmmCacheDump {
56        let mut partitions: Vec<PartitionDump> = self
57            .partitions
58            .lock()
59            .unwrap_or_else(PoisonError::into_inner)
60            .iter()
61            .map(|(tp, cache)| PartitionDump {
62                topic_id_partition: tp.clone(),
63                segments: cache.dump_segments(),
64                delete_state: cache.delete_state(),
65            })
66            .collect();
67        // Stable order so `export()` is deterministic and comparable.
68        partitions.sort_by(|a, b| {
69            (
70                a.topic_id_partition.topic_id,
71                a.topic_id_partition.partition,
72            )
73                .cmp(&(
74                    b.topic_id_partition.topic_id,
75                    b.topic_id_partition.partition,
76                ))
77        });
78        // Within a partition, sort segments by (start_offset, id) so the
79        // dump is canonical regardless of HashMap iteration order.
80        for p in &mut partitions {
81            sort_by_start_offset(&mut p.segments);
82        }
83        RlmmCacheDump { partitions }
84    }
85
86    /// Seeds the cache from a dump and does no transition validation.
87    ///
88    /// Use it on a freshly-constructed manager during a snapshot restore. It
89    /// overwrites existing partitions.
90    pub fn import(&self, dump: RlmmCacheDump) {
91        let mut guard = self
92            .partitions
93            .lock()
94            .unwrap_or_else(PoisonError::into_inner);
95        for p in dump.partitions {
96            let cache = guard.entry(p.topic_id_partition).or_default();
97            cache.seed(p.segments, p.delete_state);
98        }
99        drop(guard);
100    }
101}
102
103impl RemoteLogMetadataManager for InmemoryRemoteLogMetadataManager {
104    fn add_remote_log_segment_metadata(
105        &self,
106        metadata: RemoteLogSegmentMetadata,
107    ) -> Result<(), RemoteStorageError> {
108        let tp = metadata.remote_log_segment_id().topic_id_partition.clone();
109        let mut guard = self
110            .partitions
111            .lock()
112            .unwrap_or_else(PoisonError::into_inner);
113        guard.entry(tp).or_default().add(metadata)
114    }
115
116    fn update_remote_log_segment_metadata(
117        &self,
118        update: RemoteLogSegmentMetadataUpdate,
119    ) -> Result<(), RemoteStorageError> {
120        let tp = update.remote_log_segment_id.topic_id_partition.clone();
121        let mut guard = self
122            .partitions
123            .lock()
124            .unwrap_or_else(PoisonError::into_inner);
125        match guard.get_mut(&tp) {
126            Some(cache) => cache.update(&update),
127            None => Err(RemoteStorageError::SegmentNotFound(
128                update.remote_log_segment_id,
129            )),
130        }
131    }
132
133    fn remote_log_segment_metadata(
134        &self,
135        topic_id_partition: &TopicIdPartition,
136        leader_epoch: LeaderEpoch,
137        offset: i64,
138    ) -> Result<Option<RemoteLogSegmentMetadata>, RemoteStorageError> {
139        let guard = self
140            .partitions
141            .lock()
142            .unwrap_or_else(PoisonError::into_inner);
143        Ok(guard
144            .get(topic_id_partition)
145            .and_then(|c| c.segment_for(leader_epoch, offset)))
146    }
147
148    fn highest_offset_for_epoch(
149        &self,
150        topic_id_partition: &TopicIdPartition,
151        leader_epoch: LeaderEpoch,
152    ) -> Result<Option<i64>, RemoteStorageError> {
153        let guard = self
154            .partitions
155            .lock()
156            .unwrap_or_else(PoisonError::into_inner);
157        Ok(guard
158            .get(topic_id_partition)
159            .and_then(|c| c.highest_offset_for_epoch(leader_epoch)))
160    }
161
162    fn list_remote_log_segments(
163        &self,
164        topic_id_partition: &TopicIdPartition,
165    ) -> Result<Vec<RemoteLogSegmentMetadata>, RemoteStorageError> {
166        Ok(self.list_segments(topic_id_partition, RemoteLogMetadataCache::list))
167    }
168
169    fn list_remote_log_segments_by_epoch(
170        &self,
171        topic_id_partition: &TopicIdPartition,
172        leader_epoch: LeaderEpoch,
173    ) -> Result<Vec<RemoteLogSegmentMetadata>, RemoteStorageError> {
174        Ok(self.list_segments(topic_id_partition, |c| c.list_by_epoch(leader_epoch)))
175    }
176
177    fn put_remote_partition_delete_metadata(
178        &self,
179        metadata: RemotePartitionDeleteMetadata,
180    ) -> Result<(), RemoteStorageError> {
181        let mut guard = self
182            .partitions
183            .lock()
184            .unwrap_or_else(PoisonError::into_inner);
185        let cache = guard
186            .entry(metadata.topic_id_partition.clone())
187            .or_default();
188        let from = cache.delete_state();
189        if !RemotePartitionDeleteState::is_valid_transition(from, metadata.state) {
190            return Err(RemoteStorageError::InvalidPartitionDeleteTransition {
191                tp: metadata.topic_id_partition,
192                from,
193                to: metadata.state,
194            });
195        }
196        cache.set_delete_state(metadata.state);
197        drop(guard);
198        Ok(())
199    }
200
201    // The cache is the store, so every committed event is already applied.
202    fn caught_up(
203        &self,
204        _topic_id_partition: &TopicIdPartition,
205    ) -> Result<bool, RemoteStorageError> {
206        Ok(true)
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use {
213        super::*,
214        crate::metadata::{RemotePartitionDeleteState, test_topic_partition as tp},
215        assert2::{assert, check},
216        uuid::Uuid,
217    };
218
219    krabka_macros::remote_segment_fixtures!(started, finish, crate, end);
220
221    krabka_macros::remote_metadata_missing!(check_missing_partition, crate);
222
223    krabka_macros::remote_segment_check!(check_finished_segment);
224
225    fn manager_with_finished_segment() -> InmemoryRemoteLogMetadataManager {
226        let manager = InmemoryRemoteLogMetadataManager::new();
227        manager
228            .add_remote_log_segment_metadata(started(10, 0, 99))
229            .unwrap();
230        manager
231            .update_remote_log_segment_metadata(finish(10))
232            .unwrap();
233        manager
234    }
235
236    #[test]
237    fn add_finish_query_round_trip() {
238        let m = manager_with_finished_segment();
239
240        check_finished_segment(&m, &tp());
241    }
242
243    #[test]
244    fn caught_up_is_always_true() {
245        assert!(
246            InmemoryRemoteLogMetadataManager::new()
247                .caught_up(&tp())
248                .unwrap()
249        );
250    }
251
252    #[test]
253    fn query_unknown_partition_is_none_not_error() {
254        let m = InmemoryRemoteLogMetadataManager::new();
255        let other = TopicIdPartition::new(Uuid::from_u128(999), "nope", 0);
256        check_missing_partition(&m, &other);
257    }
258
259    #[test]
260    fn list_by_epoch_returns_added_segment() {
261        let m = manager_with_finished_segment();
262        let listed = m
263            .list_remote_log_segments_by_epoch(&tp(), LeaderEpoch(0))
264            .unwrap();
265        assert!(listed.len() == 1);
266        assert!(listed[0].remote_log_segment_id().id == Uuid::from_u128(10));
267    }
268
269    #[test]
270    fn inmemory_read_outcomes_are_some_none_never_not_ready() {
271        let m = InmemoryRemoteLogMetadataManager::new();
272        m.add_remote_log_segment_metadata(started(10, 0, 99))
273            .unwrap();
274        m.update_remote_log_segment_metadata(finish(10)).unwrap();
275
276        // Found.
277        assert!(matches!(
278            m.remote_log_segment_metadata(&tp(), LeaderEpoch(0), 42),
279            Ok(Some(_))
280        ));
281        // Caught up, no covering segment → genuine miss.
282        assert!(matches!(
283            m.remote_log_segment_metadata(&tp(), LeaderEpoch(0), 10_000),
284            Ok(None)
285        ));
286        // Unknown partition → genuine miss, NOT NotReady.
287        let other = TopicIdPartition::new(Uuid::from_u128(999), "nope", 0);
288        let got = m.remote_log_segment_metadata(&other, LeaderEpoch(0), 0);
289        assert!(matches!(got, Ok(None)));
290        assert!(
291            !matches!(got, Err(RemoteStorageError::NotReady { .. })),
292            "in-memory manager has no consumer lag; never NotReady"
293        );
294    }
295
296    #[test]
297    fn update_before_add_errors() {
298        let m = InmemoryRemoteLogMetadataManager::new();
299        let err = m
300            .update_remote_log_segment_metadata(finish(10))
301            .unwrap_err();
302        assert!(matches!(err, RemoteStorageError::SegmentNotFound(_)));
303    }
304
305    #[test]
306    fn list_returns_all_states_ordered() {
307        let m = InmemoryRemoteLogMetadataManager::new();
308        m.add_remote_log_segment_metadata(started(11, 100, 199))
309            .unwrap();
310        m.add_remote_log_segment_metadata(started(10, 0, 99))
311            .unwrap();
312        m.update_remote_log_segment_metadata(finish(10)).unwrap();
313        let listed = m.list_remote_log_segments(&tp()).unwrap();
314        let start_offsets: Vec<i64> = listed
315            .iter()
316            .map(RemoteLogSegmentMetadata::start_offset)
317            .collect();
318        assert!(start_offsets == [0, 100]);
319    }
320
321    #[test]
322    fn partition_delete_lifecycle() {
323        let m = InmemoryRemoteLogMetadataManager::new();
324        for state in [
325            RemotePartitionDeleteState::DeletePartitionMarked,
326            RemotePartitionDeleteState::DeletePartitionStarted,
327            RemotePartitionDeleteState::DeletePartitionFinished,
328        ] {
329            m.put_remote_partition_delete_metadata(RemotePartitionDeleteMetadata {
330                topic_id_partition: tp(),
331                state,
332                event_timestamp_ms: 500,
333                broker_id: 1,
334            })
335            .unwrap();
336        }
337    }
338
339    #[test]
340    fn export_then_import_reproduces_cache() {
341        let m = InmemoryRemoteLogMetadataManager::new();
342        m.add_remote_log_segment_metadata(started(10, 0, 99))
343            .unwrap();
344        m.add_remote_log_segment_metadata(started(11, 100, 199))
345            .unwrap();
346        m.update_remote_log_segment_metadata(finish(10)).unwrap();
347        m.put_remote_partition_delete_metadata(RemotePartitionDeleteMetadata {
348            topic_id_partition: tp(),
349            state: RemotePartitionDeleteState::DeletePartitionMarked,
350            event_timestamp_ms: 500,
351            broker_id: 1,
352        })
353        .unwrap();
354
355        let dump = m.export();
356        let restored = InmemoryRemoteLogMetadataManager::new();
357        restored.import(dump);
358
359        // list_remote_log_segments matches across the partition.
360        let before = m.list_remote_log_segments(&tp()).unwrap();
361        let after = restored.list_remote_log_segments(&tp()).unwrap();
362        check!(before == after);
363        // Finished segment still queryable post-import.
364        check!(
365            restored
366                .highest_offset_for_epoch(&tp(), LeaderEpoch(0))
367                .unwrap()
368                == Some(99)
369        );
370        // Re-exporting yields the same dump (idempotent round trip).
371        check!(m.export() == restored.export());
372    }
373
374    #[test]
375    fn partition_delete_rejects_out_of_order() {
376        let m = InmemoryRemoteLogMetadataManager::new();
377        let err = m
378            .put_remote_partition_delete_metadata(RemotePartitionDeleteMetadata {
379                topic_id_partition: tp(),
380                state: RemotePartitionDeleteState::DeletePartitionFinished,
381                event_timestamp_ms: 500,
382                broker_id: 1,
383            })
384            .unwrap_err();
385        assert!(matches!(
386            err,
387            RemoteStorageError::InvalidPartitionDeleteTransition { .. }
388        ));
389    }
390}