1use 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#[derive(Debug, Default)]
34pub(crate) struct ObjectCatalog {
35 objects: BTreeMap<u64, Vec<FooterEntry>>,
36 regions: BTreeMap<RegionId, BTreeMap<EntryId, (u64, FooterEntry)>>,
41 max_entry_ids: HashMap<RegionId, EntryId>,
44 watermarks: HashMap<RegionId, EntryId>,
46 live_segments: BTreeMap<u64, usize>,
49}
50
51impl ObjectCatalog {
52 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 ensure!(
90 !self.objects.contains_key(&object_seq),
91 CorruptedWalObjectSnafu {
92 reason: format!("object {object_seq} is already indexed"),
93 }
94 );
95
96 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 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 pub(crate) fn contains_object(&self, object_seq: u64) -> bool {
165 self.objects.contains_key(&object_seq)
166 }
167
168 pub(crate) fn obsolete(&mut self, region_id: RegionId, entry_id: EntryId) {
173 let previous = self.watermarks.get(®ion_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(®ion_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 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 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 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(®ion_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 pub(crate) fn region_max_entry_id(&self, region_id: RegionId) -> Option<u64> {
260 self.max_entry_ids.get(®ion_id).copied()
261 }
262
263 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 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 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 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 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 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 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 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 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 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 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 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 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 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 catalog.obsolete(region_one, entry_id(2, 1));
605 assert_eq!(3, catalog.reclaim_boundary(0));
606 catalog.obsolete(region_two, EntryId::MAX);
609 assert_eq!(4, catalog.reclaim_boundary(0));
610 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 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 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 #[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 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 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(®ion_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 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(®ion_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 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 _ => {
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(®ion_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 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}