Skip to main content

log_store/object_store_wal/
catalog.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! In-memory index over the footers of the objects of one WAL prefix.
16
17use std::collections::{BTreeMap, HashMap};
18use std::ops::Bound::{Excluded, Included, Unbounded};
19
20use snafu::{OptionExt, ensure};
21use store_api::logstore::EntryId;
22use store_api::storage::RegionId;
23
24use crate::error::{
25    CorruptedWalObjectSnafu, InvalidWalEntryRangeSnafu, Result, WalObjectSequenceExhaustedSnafu,
26};
27use crate::object_store_wal::batch::{OBJECT_SEQ_LIMIT, sequence_floor};
28use crate::object_store_wal::format::{FooterEntry, RELEASE_REGION_ID};
29
30/// Indexes objects by sequence and, per region, the objects that hold entries
31/// of that region, and counts the live segments of every object under the
32/// obsolete watermarks.
33#[derive(Debug, Default)]
34pub(crate) struct ObjectCatalog {
35    objects: BTreeMap<u64, Vec<FooterEntry>>,
36    /// The objects that hold a segment of the region, as the sequence and
37    /// the footer entry of the segment, keyed by the largest entry id of the
38    /// segment. The entry ranges of a region increase strictly with the
39    /// object sequence, so this is also sequence order.
40    regions: BTreeMap<RegionId, BTreeMap<EntryId, (u64, FooterEntry)>>,
41    /// Largest entry id ever indexed per region, kept after the object that
42    /// held it was removed.
43    max_entry_ids: HashMap<RegionId, EntryId>,
44    /// The obsolete watermark per region, see [`obsolete`](Self::obsolete).
45    watermarks: HashMap<RegionId, EntryId>,
46    /// The number of live segments of every indexed object that holds one,
47    /// see [`reclaim_boundary`](Self::reclaim_boundary).
48    live_segments: BTreeMap<u64, usize>,
49}
50
51impl ObjectCatalog {
52    /// Indexes the footer of the object `object_seq`. Objects may be inserted
53    /// in any order, which lets recovery index them as it discovers them.
54    /// Inserting a sequence that is already indexed is rejected, whether or not
55    /// the footer matches the indexed one. An object without segments holds no
56    /// entries but still takes its sequence.
57    pub(crate) fn insert_object(
58        &mut self,
59        object_seq: u64,
60        mut footer: Vec<FooterEntry>,
61    ) -> Result<()> {
62        footer.sort_unstable_by_key(|entry| entry.region_id);
63        for entries in footer.windows(2) {
64            ensure!(
65                entries[0].region_id != entries[1].region_id,
66                CorruptedWalObjectSnafu {
67                    reason: format!(
68                        "object {object_seq} has duplicate footer entries for region {}",
69                        entries[0].region_id
70                    ),
71                }
72            );
73        }
74        for entry in &footer {
75            ensure!(
76                entry.entry_count > 0 && entry.min_entry_id <= entry.max_entry_id,
77                CorruptedWalObjectSnafu {
78                    reason: format!(
79                        "object {object_seq} has invalid entry range {}..={} with {} entries for region {}",
80                        entry.min_entry_id, entry.max_entry_id, entry.entry_count, entry.region_id
81                    ),
82                }
83            );
84        }
85
86        // Object sequences are unique: recovery indexes every listed key once, and
87        // accepting a repeated insertion would hide a caller that lost track of it.
88        // Retrying an identical write stays an object store concern.
89        ensure!(
90            !self.objects.contains_key(&object_seq),
91            CorruptedWalObjectSnafu {
92                reason: format!("object {object_seq} is already indexed"),
93            }
94        );
95
96        // Validate every region before mutating either index so insertion is atomic.
97        // While the indexed ranges of a region increase with the sequence, a
98        // segment can only conflict with the segments right below and right
99        // above its largest entry id: each must lie on the side of it that
100        // its sequence is on.
101        for entry in &footer {
102            let Some(region_objects) = self.regions.get(&entry.region_id) else {
103                continue;
104            };
105            let below = region_objects.range(..entry.max_entry_id).next_back();
106            let above = region_objects.range(entry.max_entry_id..).next();
107            for (_, (neighbor_seq, neighbor)) in below.into_iter().chain(above) {
108                let ((lower_seq, lower), (upper_seq, upper)) = if *neighbor_seq < object_seq {
109                    ((*neighbor_seq, neighbor), (object_seq, entry))
110                } else {
111                    ((object_seq, entry), (*neighbor_seq, neighbor))
112                };
113                ensure!(
114                    lower.max_entry_id < upper.min_entry_id,
115                    CorruptedWalObjectSnafu {
116                        reason: out_of_order_reason(
117                            entry.region_id,
118                            lower_seq,
119                            lower.max_entry_id,
120                            upper_seq,
121                            upper.min_entry_id
122                        ),
123                    }
124                );
125            }
126        }
127
128        let live = footer.iter().filter(|entry| self.is_live(entry)).count();
129        if live > 0 {
130            self.live_segments.insert(object_seq, live);
131        }
132        for entry in &footer {
133            self.regions
134                .entry(entry.region_id)
135                .or_default()
136                .insert(entry.max_entry_id, (object_seq, entry.clone()));
137            self.max_entry_ids
138                .entry(entry.region_id)
139                .and_modify(|max| *max = (*max).max(entry.max_entry_id))
140                .or_insert(entry.max_entry_id);
141        }
142        self.objects.insert(object_seq, footer);
143        Ok(())
144    }
145
146    /// Removes the object `object_seq` from the index once it was deleted and
147    /// returns the footer it held, or `None` if it was not indexed. The
148    /// largest entry id of every region it held is kept.
149    pub(crate) fn remove_object(&mut self, object_seq: u64) -> Option<Vec<FooterEntry>> {
150        let footer = self.objects.remove(&object_seq)?;
151        self.live_segments.remove(&object_seq);
152        for entry in &footer {
153            if let Some(objects) = self.regions.get_mut(&entry.region_id) {
154                objects.remove(&entry.max_entry_id);
155                if objects.is_empty() {
156                    self.regions.remove(&entry.region_id);
157                }
158            }
159        }
160        Some(footer)
161    }
162
163    /// Returns true while the object `object_seq` is indexed.
164    pub(crate) fn contains_object(&self, object_seq: u64) -> bool {
165        self.objects.contains_key(&object_seq)
166    }
167
168    /// Moves the obsolete watermark of `region_id` up to `entry_id`; it never
169    /// moves down. The segments of the region whose largest entry id the
170    /// watermark reaches stop being live, so a move costs a lookup per
171    /// segment it reaches, however many segments their objects hold.
172    pub(crate) fn obsolete(&mut self, region_id: RegionId, entry_id: EntryId) {
173        let previous = self.watermarks.get(&region_id).copied();
174        if previous.is_some_and(|previous| previous >= entry_id) {
175            return;
176        }
177        self.watermarks.insert(region_id, entry_id);
178        if region_id == RELEASE_REGION_ID {
179            return;
180        }
181        let Some(objects) = self.regions.get(&region_id) else {
182            return;
183        };
184        let reached = (previous.map_or(Unbounded, Excluded), Included(entry_id));
185        for (object_seq, _) in objects.range(reached).map(|(_, object)| object) {
186            if let Some(live) = self.live_segments.get_mut(object_seq) {
187                *live -= 1;
188                if *live == 0 {
189                    self.live_segments.remove(object_seq);
190                }
191            }
192        }
193    }
194
195    /// Returns whether a segment is live: its region is not
196    /// [`RELEASE_REGION_ID`], whose records recovery reads only from the
197    /// objects it indexes, and either has no watermark or a watermark below
198    /// the largest entry id of the segment. A region whose watermark is
199    /// [`EntryId::MAX`] holds no live segment.
200    fn is_live(&self, entry: &FooterEntry) -> bool {
201        entry.region_id != RELEASE_REGION_ID
202            && self
203                .watermarks
204                .get(&entry.region_id)
205                .is_none_or(|watermark| entry.max_entry_id > *watermark)
206    }
207
208    /// Returns the reclaim boundary the indexed objects and the obsolete
209    /// watermarks allow: the first indexed object at or above `from` that
210    /// holds a live segment, see [`is_live`](Self::is_live), or the sequence
211    /// after the last indexed object when none does, and never less than
212    /// `from`. The objects holding a live segment are tracked as objects are
213    /// indexed and watermarks move, so the boundary is a lookup.
214    pub(crate) fn reclaim_boundary(&self, from: u64) -> u64 {
215        match self.live_segments.range(from..).next() {
216            Some((&object_seq, _)) => object_seq,
217            None => self
218                .objects
219                .last_key_value()
220                .map_or(from, |(&object_seq, _)| {
221                    object_seq.saturating_add(1).max(from)
222                }),
223        }
224    }
225
226    /// Returns the objects that hold entries of `region_id` overlapping
227    /// `start_entry_id..=end_entry_id`, ordered by object sequence. The
228    /// segments of a region are ordered by entry id, so the lookup seeks to
229    /// the first segment that ends at or after `start_entry_id` and stops
230    /// before the first that starts after `end_entry_id`, whatever the number
231    /// of objects the region holds.
232    pub(crate) fn objects_for_entry_range(
233        &self,
234        region_id: RegionId,
235        start_entry_id: u64,
236        end_entry_id: u64,
237    ) -> Result<Vec<(u64, &FooterEntry)>> {
238        ensure!(
239            start_entry_id <= end_entry_id,
240            InvalidWalEntryRangeSnafu {
241                region_id,
242                start_entry_id,
243                end_entry_id,
244            }
245        );
246
247        let Some(objects) = self.regions.get(&region_id) else {
248            return Ok(Vec::new());
249        };
250        Ok(objects
251            .range(start_entry_id..)
252            .map(|(_, (object_seq, entry))| (*object_seq, entry))
253            .take_while(|(_, entry)| entry.min_entry_id <= end_entry_id)
254            .collect())
255    }
256
257    /// Returns the largest entry id ever indexed for `region_id`, which is
258    /// kept after the object holding it was removed.
259    pub(crate) fn region_max_entry_id(&self, region_id: RegionId) -> Option<u64> {
260        self.max_entry_ids.get(&region_id).copied()
261    }
262
263    /// Returns the sequence to assign to the next object written after recovery.
264    ///
265    /// An empty catalog starts at zero, so the first object of a prefix always
266    /// takes sequence zero. Otherwise the sequence continues after the largest
267    /// indexed one, which recovery discovers regardless of insertion order, and
268    /// is raised further when the largest entry id of a region lies at or above
269    /// the ids that sequence would assign: ids assigned under the earlier
270    /// contiguous scheme carry no object information, and every new id of a
271    /// region must be greater than every id it already has. A sequence at or
272    /// above [`OBJECT_SEQ_LIMIT`] does not fit an entry id and is rejected.
273    pub(crate) fn next_object_seq(&self) -> Result<u64> {
274        let after_last = match self.objects.last_key_value() {
275            None => 0,
276            Some((&last_object_seq, _)) => last_object_seq
277                .checked_add(1)
278                .context(WalObjectSequenceExhaustedSnafu { last_object_seq })?,
279        };
280        let floor = self
281            .max_entry_ids
282            .values()
283            .copied()
284            .map(sequence_floor)
285            .max()
286            .unwrap_or(0);
287        let next_object_seq = after_last.max(floor);
288        ensure!(
289            next_object_seq < OBJECT_SEQ_LIMIT,
290            WalObjectSequenceExhaustedSnafu {
291                last_object_seq: next_object_seq - 1,
292            }
293        );
294        Ok(next_object_seq)
295    }
296
297    /// Iterates over the indexed objects ordered by object sequence.
298    pub(crate) fn objects_in_order(&self) -> impl Iterator<Item = (u64, &[FooterEntry])> + '_ {
299        self.objects
300            .iter()
301            .map(|(&object_seq, footer)| (object_seq, footer.as_slice()))
302    }
303}
304
305fn out_of_order_reason(
306    region_id: RegionId,
307    lower_object_seq: u64,
308    lower_max_entry_id: u64,
309    upper_object_seq: u64,
310    upper_min_entry_id: u64,
311) -> String {
312    format!(
313        "entry ranges of region {region_id} are not strictly increasing, object {lower_object_seq} ends at {lower_max_entry_id}, object {upper_object_seq} starts at {upper_min_entry_id}"
314    )
315}
316
317#[cfg(test)]
318mod tests {
319    use rand::rngs::StdRng;
320    use rand::seq::SliceRandom;
321    use rand::{Rng, SeedableRng};
322
323    use super::*;
324    use crate::error::Error;
325    use crate::object_store_wal::batch::entry_id;
326
327    #[test]
328    fn test_catalog_indexes_objects_and_queries_ranges() {
329        let region_one = RegionId::new(1, 1);
330        let region_two = RegionId::new(2, 1);
331        let mut catalog = ObjectCatalog::default();
332
333        // Recovery may discover objects out of order.
334        catalog
335            .insert_object(
336                2,
337                vec![
338                    footer_entry(region_one, 4, 6),
339                    footer_entry(region_two, 8, 9),
340                ],
341            )
342            .unwrap();
343        catalog
344            .insert_object(1, vec![footer_entry(region_one, 1, 3)])
345            .unwrap();
346        catalog
347            .insert_object(4, vec![footer_entry(region_one, 10, 12)])
348            .unwrap();
349
350        assert_eq!(Some(12), catalog.region_max_entry_id(region_one));
351        assert_eq!(Some(9), catalog.region_max_entry_id(region_two));
352        assert_eq!(None, catalog.region_max_entry_id(RegionId::new(3, 1)));
353
354        let objects = catalog.objects_for_entry_range(region_one, 3, 10).unwrap();
355        assert_eq!(vec![1, 2, 4], object_seqs(&objects));
356        assert_eq!(
357            vec![1, 2, 4],
358            catalog
359                .objects_in_order()
360                .map(|(object_seq, _)| object_seq)
361                .collect::<Vec<_>>()
362        );
363    }
364
365    #[test]
366    fn test_catalog_rejects_duplicate_object_sequences() {
367        let region_id = RegionId::new(1, 1);
368        let mut catalog = ObjectCatalog::default();
369        let first = footer_entry(region_id, 1, 2);
370        let second = footer_entry(RegionId::new(2, 1), 4, 5);
371
372        catalog
373            .insert_object(1, vec![first.clone(), second.clone()])
374            .unwrap();
375
376        // An identical footer is rejected just like a conflicting one.
377        assert_corrupted(
378            catalog.insert_object(1, vec![second, first.clone()]),
379            "object 1 is already indexed",
380        );
381
382        let mut conflicting = first;
383        conflicting.segment_offset += 1;
384        assert_corrupted(
385            catalog.insert_object(1, vec![conflicting]),
386            "object 1 is already indexed",
387        );
388        assert_eq!(1, catalog.objects_in_order().count());
389    }
390
391    #[test]
392    fn test_catalog_resumes_object_sequence_after_recovery() {
393        let region_id = RegionId::new(1, 1);
394        let mut catalog = ObjectCatalog::default();
395        assert_eq!(0, catalog.next_object_seq().unwrap());
396
397        // Recovery may discover objects out of order.
398        catalog
399            .insert_object(4, vec![footer_entry(region_id, 10, 12)])
400            .unwrap();
401        catalog
402            .insert_object(1, vec![footer_entry(region_id, 1, 3)])
403            .unwrap();
404
405        assert_eq!(5, catalog.next_object_seq().unwrap());
406
407        // An object without segments takes its sequence all the same.
408        catalog.insert_object(7, Vec::new()).unwrap();
409        assert_eq!(8, catalog.next_object_seq().unwrap());
410        assert_eq!(Some(12), catalog.region_max_entry_id(region_id));
411    }
412
413    #[test]
414    fn test_catalog_raises_object_sequence_above_existing_entry_ids() {
415        let region_one = RegionId::new(1, 1);
416        let region_two = RegionId::new(1, 2);
417        let mut catalog = ObjectCatalog::default();
418
419        // Ids that fit below the ids of the next sequence leave it alone.
420        catalog
421            .insert_object(0, vec![footer_entry(region_one, 1, 3)])
422            .unwrap();
423        catalog
424            .insert_object(
425                1,
426                vec![footer_entry(region_two, entry_id(1, 1), entry_id(1, 2))],
427            )
428            .unwrap();
429        assert_eq!(2, catalog.next_object_seq().unwrap());
430
431        // A contiguous id past them names a later object: the sequence
432        // resumes above it, whichever region holds it.
433        catalog
434            .insert_object(2, vec![footer_entry(region_one, 5_000_000, 5_000_000)])
435            .unwrap();
436        assert_eq!(5, catalog.next_object_seq().unwrap());
437        catalog
438            .insert_object(
439                3,
440                vec![footer_entry(region_two, entry_id(7, 4), entry_id(7, 4))],
441            )
442            .unwrap();
443        assert_eq!(8, catalog.next_object_seq().unwrap());
444    }
445
446    #[test]
447    fn test_catalog_rejects_exhausted_object_sequence() {
448        let region_id = RegionId::new(1, 1);
449        let assert_exhausted = |catalog: &ObjectCatalog| {
450            let error = catalog.next_object_seq().unwrap_err();
451            assert!(
452                error.to_string().contains("object sequence is exhausted"),
453                "unexpected error: {error}"
454            );
455        };
456
457        // The last sequence that fits an entry id is indexed.
458        let mut catalog = ObjectCatalog::default();
459        catalog
460            .insert_object(OBJECT_SEQ_LIMIT - 2, vec![footer_entry(region_id, 1, 2)])
461            .unwrap();
462        assert_eq!(OBJECT_SEQ_LIMIT - 1, catalog.next_object_seq().unwrap());
463        catalog
464            .insert_object(OBJECT_SEQ_LIMIT - 1, vec![footer_entry(region_id, 3, 4)])
465            .unwrap();
466        assert_exhausted(&catalog);
467
468        // A sequence that does not fit was written by an earlier scheme.
469        let mut catalog = ObjectCatalog::default();
470        catalog
471            .insert_object(u64::MAX, vec![footer_entry(region_id, 1, 2)])
472            .unwrap();
473        assert_exhausted(&catalog);
474
475        // An entry id that leaves no sequence above it.
476        let mut catalog = ObjectCatalog::default();
477        catalog
478            .insert_object(0, vec![footer_entry(region_id, u64::MAX, u64::MAX)])
479            .unwrap();
480        assert_exhausted(&catalog);
481    }
482
483    #[test]
484    fn test_catalog_rejects_overlapping_or_reversed_region_ranges() {
485        let region_id = RegionId::new(1, 1);
486        let mut catalog = ObjectCatalog::default();
487        catalog
488            .insert_object(2, vec![footer_entry(region_id, 10, 20)])
489            .unwrap();
490
491        assert_corrupted(
492            catalog.insert_object(3, vec![footer_entry(region_id, 20, 30)]),
493            "are not strictly increasing",
494        );
495        assert_corrupted(
496            catalog.insert_object(3, vec![footer_entry(region_id, 5, 9)]),
497            "are not strictly increasing",
498        );
499        assert_corrupted(
500            catalog.insert_object(1, vec![footer_entry(region_id, 15, 19)]),
501            "are not strictly increasing",
502        );
503        assert_eq!(1, catalog.objects_in_order().count());
504
505        // A segment that fits between two others by its entry ids must also
506        // fit between them by its sequence.
507        catalog
508            .insert_object(8, vec![footer_entry(region_id, 50, 60)])
509            .unwrap();
510        assert_corrupted(
511            catalog.insert_object(1, vec![footer_entry(region_id, 30, 40)]),
512            "object 1 ends at 40, object 2 starts at 10",
513        );
514        assert_corrupted(
515            catalog.insert_object(9, vec![footer_entry(region_id, 30, 40)]),
516            "object 8 ends at 60, object 9 starts at 30",
517        );
518        assert_corrupted(
519            catalog.insert_object(5, vec![footer_entry(region_id, 61, 70)]),
520            "object 5 ends at 70, object 8 starts at 50",
521        );
522        catalog
523            .insert_object(5, vec![footer_entry(region_id, 30, 40)])
524            .unwrap();
525        assert_eq!(
526            vec![2, 5, 8],
527            object_seqs(
528                &catalog
529                    .objects_for_entry_range(region_id, 0, EntryId::MAX)
530                    .unwrap()
531            )
532        );
533    }
534
535    #[test]
536    fn test_catalog_rejects_duplicate_region_and_invalid_ranges() {
537        let region_id = RegionId::new(1, 1);
538        let mut catalog = ObjectCatalog::default();
539        assert_corrupted(
540            catalog.insert_object(
541                1,
542                vec![footer_entry(region_id, 1, 1), footer_entry(region_id, 2, 2)],
543            ),
544            "duplicate footer entries",
545        );
546        let mut invalid = footer_entry(region_id, 2, 1);
547        invalid.entry_count = 0;
548        assert_corrupted(
549            catalog.insert_object(1, vec![invalid]),
550            "has invalid entry range 2..=1",
551        );
552
553        let error = catalog
554            .objects_for_entry_range(region_id, 2, 1)
555            .unwrap_err();
556        assert!(
557            matches!(error, Error::InvalidWalEntryRange { start_entry_id, end_entry_id, .. } if start_entry_id == 2 && end_entry_id == 1),
558            "unexpected error: {error:?}"
559        );
560    }
561
562    #[test]
563    fn test_catalog_reclaim_boundary_follows_the_watermarks() {
564        let region_one = RegionId::new(1, 1);
565        let region_two = RegionId::new(2, 1);
566        let region_three = RegionId::new(3, 1);
567        let mut catalog = ObjectCatalog::default();
568        catalog.insert_object(0, Vec::new()).unwrap();
569        catalog
570            .insert_object(
571                1,
572                vec![
573                    footer_entry(region_one, entry_id(1, 1), entry_id(1, 2)),
574                    footer_entry(region_two, entry_id(1, 1), entry_id(1, 1)),
575                ],
576            )
577            .unwrap();
578        catalog
579            .insert_object(
580                2,
581                vec![
582                    footer_entry(region_one, entry_id(2, 1), entry_id(2, 1)),
583                    footer_entry(RELEASE_REGION_ID, entry_id(2, 1), entry_id(2, 1)),
584                ],
585            )
586            .unwrap();
587        catalog
588            .insert_object(
589                3,
590                vec![footer_entry(region_two, entry_id(3, 1), entry_id(3, 1))],
591            )
592            .unwrap();
593
594        // A region without a watermark holds the boundary at its first object.
595        assert_eq!(1, catalog.reclaim_boundary(0));
596        catalog.obsolete(region_two, entry_id(1, 1));
597        assert_eq!(1, catalog.reclaim_boundary(0));
598        // A watermark inside a segment keeps its object.
599        catalog.obsolete(region_one, entry_id(1, 1));
600        assert_eq!(1, catalog.reclaim_boundary(0));
601        catalog.obsolete(region_one, entry_id(1, 2));
602        assert_eq!(2, catalog.reclaim_boundary(0));
603        // The segment of release records never holds the boundary.
604        catalog.obsolete(region_one, entry_id(2, 1));
605        assert_eq!(3, catalog.reclaim_boundary(0));
606        // A region whose watermark is `EntryId::MAX` holds nothing, and when
607        // no region does, the boundary is past the last object.
608        catalog.obsolete(region_two, EntryId::MAX);
609        assert_eq!(4, catalog.reclaim_boundary(0));
610        // A watermark never moves down, and the boundary is never below
611        // `from`.
612        catalog.obsolete(region_one, entry_id(1, 1));
613        assert_eq!(4, catalog.reclaim_boundary(0));
614        assert_eq!(5, catalog.reclaim_boundary(5));
615
616        // A segment indexed at or below the watermark of its region is not
617        // live, one of a region without a watermark is.
618        catalog
619            .insert_object(
620                4,
621                vec![footer_entry(region_two, entry_id(4, 1), entry_id(4, 1))],
622            )
623            .unwrap();
624        assert_eq!(5, catalog.reclaim_boundary(0));
625        catalog
626            .insert_object(
627                5,
628                vec![footer_entry(region_three, entry_id(5, 1), entry_id(5, 1))],
629            )
630            .unwrap();
631        assert_eq!(5, catalog.reclaim_boundary(0));
632        catalog.obsolete(region_three, entry_id(5, 1));
633        assert_eq!(6, catalog.reclaim_boundary(0));
634
635        // Removing objects keeps the largest entry id of every region, and a
636        // region without indexed objects contributes nothing.
637        for object_seq in [0, 1, 2] {
638            assert!(catalog.remove_object(object_seq).is_some());
639        }
640        assert!(catalog.remove_object(2).is_none());
641        assert!(!catalog.contains_object(2));
642        assert!(catalog.contains_object(3));
643        assert_eq!(
644            Some(entry_id(2, 1)),
645            catalog.region_max_entry_id(region_one)
646        );
647        assert!(
648            catalog
649                .objects_for_entry_range(region_one, 0, EntryId::MAX)
650                .unwrap()
651                .is_empty()
652        );
653        assert_eq!(6, catalog.reclaim_boundary(0));
654        assert_eq!(6, catalog.next_object_seq().unwrap());
655    }
656
657    /// Random appends, watermark moves, releases, drops, collections and
658    /// recoveries give the reclaim boundary that a scan of the footers from
659    /// `from` gives, whatever `from` is, and the objects of a region that a
660    /// scan finds for a range of entry ids.
661    #[test]
662    fn test_catalog_matches_a_scan_of_the_footers() {
663        let regions = (1..=6)
664            .map(|number| RegionId::new(1, number))
665            .collect::<Vec<_>>();
666        for seed in 0..256 {
667            let mut rng = StdRng::seed_from_u64(seed);
668            let mut catalog = ObjectCatalog::default();
669            let mut watermarks = HashMap::new();
670            // The largest entry id written per region, which a later segment
671            // of the region is above.
672            let mut written = HashMap::new();
673            let mut next_object_seq = 0;
674            let mut boundary = 0;
675            for step in 0..128 {
676                let mut releases = rng.random_bool(0.2);
677                match rng.random_range(0..8) {
678                    0..=2 => {}
679                    // A watermark move, which may be below the watermark or
680                    // above every id written, of a region or, rarely, of the
681                    // reserved region, which holds nothing live either way.
682                    3 | 4 => {
683                        let region_id = if rng.random_bool(0.1) {
684                            RELEASE_REGION_ID
685                        } else {
686                            regions[rng.random_range(0..regions.len())]
687                        };
688                        let ceiling = written.get(&region_id).copied().unwrap_or(0) + 2;
689                        let entry_id = rng.random_range(0..=ceiling);
690                        obsolete(&mut catalog, &mut watermarks, region_id, entry_id);
691                    }
692                    // A release through the largest id written, or a drop,
693                    // seals an object with a release record.
694                    5 => {
695                        let region_id = regions[rng.random_range(0..regions.len())];
696                        let entry_id = if rng.random_bool(0.5) {
697                            written.get(&region_id).copied().unwrap_or(0)
698                        } else {
699                            EntryId::MAX
700                        };
701                        obsolete(&mut catalog, &mut watermarks, region_id, entry_id);
702                        releases = true;
703                    }
704                    // A collection deletes objects below the boundary, in any
705                    // order and not always all of them. Removing an object
706                    // above it, which may hold live segments, is followed too.
707                    6 => {
708                        let collected = catalog
709                            .objects_in_order()
710                            .map(|(object_seq, _)| object_seq)
711                            .filter(|object_seq| {
712                                (*object_seq < boundary || rng.random_bool(0.1))
713                                    && rng.random_bool(0.7)
714                            })
715                            .collect::<Vec<_>>();
716                        for object_seq in collected {
717                            assert!(catalog.remove_object(object_seq).is_some());
718                        }
719                    }
720                    // A recovery indexes the objects in any order, then the
721                    // watermarks.
722                    _ => {
723                        let mut objects = catalog
724                            .objects_in_order()
725                            .map(|(object_seq, footer)| (object_seq, footer.to_vec()))
726                            .collect::<Vec<_>>();
727                        objects.shuffle(&mut rng);
728                        catalog = ObjectCatalog::default();
729                        for (object_seq, footer) in objects {
730                            catalog.insert_object(object_seq, footer).unwrap();
731                        }
732                        for (region_id, entry_id) in &watermarks {
733                            catalog.obsolete(*region_id, *entry_id);
734                        }
735                    }
736                }
737                if rng.random_bool(0.5) || releases {
738                    let segments = regions
739                        .iter()
740                        .copied()
741                        .filter(|_| rng.random_bool(0.5))
742                        .chain(releases.then_some(RELEASE_REGION_ID))
743                        .collect::<Vec<_>>();
744                    let mut footer = Vec::new();
745                    for region_id in segments {
746                        let min_entry_id = written.get(&region_id).copied().unwrap_or(0)
747                            + 1
748                            + rng.random_range(0..3);
749                        let max_entry_id = min_entry_id + rng.random_range(0..3);
750                        written.insert(region_id, max_entry_id);
751                        footer.push(footer_entry(region_id, min_entry_id, max_entry_id));
752                    }
753                    catalog.insert_object(next_object_seq, footer).unwrap();
754                    next_object_seq += rng.random_range(1..=2);
755                }
756
757                let expected = scanned_reclaim_boundary(&catalog, boundary, &watermarks);
758                assert_eq!(
759                    expected,
760                    catalog.reclaim_boundary(boundary),
761                    "seed {seed}, step {step}"
762                );
763                let from = rng.random_range(0..=next_object_seq + 1);
764                assert_eq!(
765                    scanned_reclaim_boundary(&catalog, from, &watermarks),
766                    catalog.reclaim_boundary(from),
767                    "seed {seed}, step {step}, from {from}"
768                );
769                boundary = expected;
770
771                for region_id in regions.iter().chain([&RELEASE_REGION_ID]) {
772                    let ceiling = written.get(region_id).copied().unwrap_or(0) + 2;
773                    let start = rng.random_range(0..=ceiling);
774                    let end = rng.random_range(start..=ceiling);
775                    let scanned = catalog
776                        .objects_in_order()
777                        .filter(|(_, footer)| {
778                            footer.iter().any(|entry| {
779                                entry.region_id == *region_id
780                                    && entry.max_entry_id >= start
781                                    && entry.min_entry_id <= end
782                            })
783                        })
784                        .map(|(object_seq, _)| object_seq)
785                        .collect::<Vec<_>>();
786                    assert_eq!(
787                        scanned,
788                        object_seqs(
789                            &catalog
790                                .objects_for_entry_range(*region_id, start, end)
791                                .unwrap()
792                        ),
793                        "seed {seed}, step {step}, region {region_id}, {start}..={end}"
794                    );
795                }
796            }
797        }
798    }
799
800    fn obsolete(
801        catalog: &mut ObjectCatalog,
802        watermarks: &mut HashMap<RegionId, EntryId>,
803        region_id: RegionId,
804        entry_id: EntryId,
805    ) {
806        catalog.obsolete(region_id, entry_id);
807        watermarks
808            .entry(region_id)
809            .and_modify(|watermark: &mut EntryId| *watermark = (*watermark).max(entry_id))
810            .or_insert(entry_id);
811    }
812
813    /// The first object at or above `from` holding a segment that is live
814    /// under `watermarks`, found by reading every footer, or the sequence
815    /// after the last object when none does, and never less than `from`.
816    fn scanned_reclaim_boundary(
817        catalog: &ObjectCatalog,
818        from: u64,
819        watermarks: &HashMap<RegionId, EntryId>,
820    ) -> u64 {
821        let live = catalog
822            .objects_in_order()
823            .filter(|(object_seq, _)| *object_seq >= from)
824            .find(|(_, footer)| {
825                footer.iter().any(|entry| {
826                    entry.region_id != RELEASE_REGION_ID
827                        && watermarks
828                            .get(&entry.region_id)
829                            .is_none_or(|watermark| entry.max_entry_id > *watermark)
830                })
831            });
832        match live {
833            Some((object_seq, _)) => object_seq,
834            None => catalog
835                .objects_in_order()
836                .last()
837                .map_or(from, |(object_seq, _)| {
838                    object_seq.saturating_add(1).max(from)
839                }),
840        }
841    }
842
843    fn footer_entry(region_id: RegionId, min_entry_id: u64, max_entry_id: u64) -> FooterEntry {
844        FooterEntry {
845            region_id,
846            min_entry_id,
847            max_entry_id,
848            entry_count: max_entry_id
849                .checked_sub(min_entry_id)
850                .and_then(|count| count.checked_add(1))
851                .unwrap_or(0) as u32,
852            segment_offset: min_entry_id.wrapping_mul(100),
853            segment_len: 100,
854            segment_crc32: min_entry_id as u32,
855        }
856    }
857
858    fn object_seqs(objects: &[(u64, &FooterEntry)]) -> Vec<u64> {
859        objects.iter().map(|(object_seq, _)| *object_seq).collect()
860    }
861
862    fn assert_corrupted(result: Result<()>, expected_reason: &str) {
863        match result {
864            Err(Error::CorruptedWalObject { reason, .. }) => assert!(
865                reason.contains(expected_reason),
866                "expected reason to contain {expected_reason:?}, actual {reason:?}"
867            ),
868            other => panic!("expected a corrupted object error, actual {other:?}"),
869        }
870    }
871}