laminar_core/checkpoint/committed_checkpoint/
view_frontiers.rs1use super::{
4 validate_name, BTreeMap, CheckpointManifest, CheckpointScope, CheckpointWatermark,
5 CommittedCheckpointIndex, SOURCE_WATERMARK_INDEX_VERSION,
6};
7
8#[must_use]
13pub fn fold_view_frontiers<'a>(
14 predecessor: &BTreeMap<String, i64>,
15 manifests: impl IntoIterator<Item = &'a CheckpointManifest>,
16) -> BTreeMap<String, i64> {
17 let mut reported = BTreeMap::<&str, CheckpointWatermark>::new();
18 for log in manifests
19 .into_iter()
20 .flat_map(|manifest| &manifest.view_logs)
21 {
22 let participant = match (log.idle, log.frontier) {
23 (true, _) => CheckpointWatermark::Idle,
24 (false, None) => CheckpointWatermark::Uninitialized,
25 (false, Some(frontier)) => CheckpointWatermark::Active(frontier),
26 };
27 reported
28 .entry(log.view.as_str())
29 .and_modify(|cut| *cut = cut.cluster_min(participant))
30 .or_insert(participant);
31 }
32 reported
33 .into_iter()
34 .filter_map(|(view, cut)| {
35 let held = predecessor.get(view).copied();
36 let frontier = match cut.active_value() {
37 Some(minimum) => Some(held.map_or(minimum, |held| held.max(minimum))),
38 None => held,
39 };
40 frontier.map(|frontier| (view.to_owned(), frontier))
41 })
42 .collect()
43}
44
45impl CommittedCheckpointIndex {
46 pub(super) fn validate_view_frontiers(&self) -> Result<(), String> {
47 if self.view_frontiers.is_empty() {
48 return Ok(());
49 }
50 if self.version <= SOURCE_WATERMARK_INDEX_VERSION || self.scope != CheckpointScope::Cluster
51 {
52 return Err("only a version 5 cluster checkpoint carries view frontiers".into());
53 }
54 for (view, frontier) in &self.view_frontiers {
55 validate_name("view frontier", view)?;
56 if *frontier == i64::MIN {
57 return Err(format!(
58 "view frontier '{view}' uses the reserved uninitialized value"
59 ));
60 }
61 }
62 Ok(())
63 }
64
65 pub(crate) fn validate_view_frontier_continuity(
67 &self,
68 predecessor: &CommittedCheckpointIndex,
69 ) -> Result<(), String> {
70 for (view, frontier) in &self.view_frontiers {
71 if predecessor
72 .view_frontiers
73 .get(view)
74 .is_some_and(|predecessor| predecessor > frontier)
75 {
76 return Err(format!(
77 "committed frontier of view '{view}' regresses across its predecessor"
78 ));
79 }
80 }
81 Ok(())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::checkpoint::ViewLog;
89
90 fn manifest(logs: &[(&str, Option<i64>, bool)]) -> CheckpointManifest {
91 let mut manifest = CheckpointManifest::new(1, 1);
92 manifest.view_logs = logs
93 .iter()
94 .map(|&(view, frontier, idle)| ViewLog {
95 view: view.into(),
96 next_sequence: 0,
97 frontier,
98 idle,
99 segments: Vec::new(),
100 })
101 .collect();
102 manifest
103 }
104
105 fn frontiers(entries: &[(&str, i64)]) -> BTreeMap<String, i64> {
106 entries
107 .iter()
108 .map(|&(view, frontier)| (view.to_owned(), frontier))
109 .collect()
110 }
111
112 #[test]
113 fn active_participants_set_the_minimum_and_idle_ones_take_no_part() {
114 let cut = [
115 manifest(&[("v", Some(300), false)]),
116 manifest(&[("v", Some(200), false)]),
117 manifest(&[("v", Some(50), true)]),
118 ];
119 assert_eq!(
120 fold_view_frontiers(&frontiers(&[("v", 100)]), &cut),
121 frontiers(&[("v", 200)])
122 );
123 }
124
125 #[test]
126 fn an_uninitialized_participant_holds_the_predecessor_frontier() {
127 let cut = [
128 manifest(&[("v", Some(300), false)]),
129 manifest(&[("v", None, false)]),
130 ];
131 assert_eq!(
132 fold_view_frontiers(&frontiers(&[("v", 100)]), &cut),
133 frontiers(&[("v", 100)])
134 );
135 assert!(fold_view_frontiers(&BTreeMap::new(), &cut).is_empty());
136 }
137
138 #[test]
139 fn the_frontier_never_regresses_and_an_all_idle_view_holds_it() {
140 let lower = [manifest(&[("v", Some(80), false)])];
141 assert_eq!(
142 fold_view_frontiers(&frontiers(&[("v", 100)]), &lower),
143 frontiers(&[("v", 100)])
144 );
145 let idle = [manifest(&[("v", Some(500), true)])];
146 assert_eq!(
147 fold_view_frontiers(&frontiers(&[("v", 100)]), &idle),
148 frontiers(&[("v", 100)])
149 );
150 }
151
152 #[test]
153 fn views_no_participant_logs_are_dropped() {
154 let cut = [manifest(&[("kept", Some(10), false)])];
155 assert_eq!(
156 fold_view_frontiers(&frontiers(&[("dropped", 5), ("kept", 1)]), &cut),
157 frontiers(&[("kept", 10)])
158 );
159 }
160}