mito2/compaction/scheduler/
history.rs1use std::cmp::Reverse;
16use std::collections::{HashMap, HashSet};
17use std::time::{Duration, Instant};
18
19use prometheus::IntCounterVec;
20
21use crate::compaction::twcs::{PickClass, WindowCandidate};
22use crate::metrics::TWCS_SELECTED_CANDIDATES;
23
24const PROMOTION_DELAY: Duration = Duration::from_secs(60);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub(crate) struct PickKey {
33 pub(crate) window: i64,
35 pub(crate) class: PickClass,
37}
38
39impl From<&WindowCandidate> for PickKey {
40 fn from(candidate: &WindowCandidate) -> Self {
41 Self {
42 window: candidate.window,
43 class: candidate.pick.class,
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
52pub(crate) struct PickHistory {
53 waiting: HashMap<PickKey, Instant>,
56 selected_metrics: IntCounterVec,
57}
58
59impl Default for PickHistory {
60 fn default() -> Self {
61 Self {
62 waiting: HashMap::new(),
63 selected_metrics: TWCS_SELECTED_CANDIDATES.clone(),
64 }
65 }
66}
67
68#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
70struct PickPriority {
71 not_promoted: bool,
72 since: Option<Instant>,
73 class: PickClass,
74 window: Reverse<i64>,
75}
76
77impl PickHistory {
78 fn observe(&mut self, keys: impl Iterator<Item = PickKey>, now: Instant) {
81 let live: HashSet<_> = keys.collect();
82 self.waiting.retain(|key, _| live.contains(key));
83 for key in live {
84 self.waiting.entry(key).or_insert(now);
85 }
86 }
87
88 fn priority(&self, key: PickKey, now: Instant) -> PickPriority {
92 let overdue = self
93 .waiting
94 .get(&key)
95 .copied()
96 .filter(|since| now.saturating_duration_since(*since) >= PROMOTION_DELAY);
97 PickPriority {
98 not_promoted: overdue.is_none(),
99 since: overdue,
100 class: key.class,
101 window: Reverse(key.window),
102 }
103 }
104
105 fn serve(&mut self, key: PickKey, now: Instant) {
108 self.waiting.insert(key, now);
109 }
110
111 pub(crate) fn rank_candidates(
115 &mut self,
116 mut candidates: Vec<WindowCandidate>,
117 now: Instant,
118 ) -> Vec<WindowCandidate> {
119 self.observe(candidates.iter().map(PickKey::from), now);
120 candidates.sort_by_key(|candidate| self.priority(PickKey::from(candidate), now));
121 let mut windows = HashSet::new();
123 candidates.retain(|candidate| windows.insert(candidate.window));
124 candidates
125 }
126
127 pub(crate) fn acknowledge(&mut self, keys: impl Iterator<Item = PickKey>, now: Instant) {
131 for key in keys {
132 let priority = if self.priority(key, now).since.is_some() {
133 "promoted"
134 } else {
135 "normal"
136 };
137 self.selected_metrics
138 .with_label_values(&[key.class.as_str(), priority])
139 .inc();
140 self.serve(key, now);
141 }
142 }
143}
144
145#[cfg(test)]
146impl PickHistory {
147 pub(crate) fn with_selected_metrics_for_test(selected_metrics: IntCounterVec) -> Self {
149 Self {
150 waiting: HashMap::new(),
151 selected_metrics,
152 }
153 }
154
155 pub(crate) fn observe_for_test(&mut self, keys: impl Iterator<Item = PickKey>, now: Instant) {
157 self.observe(keys, now);
158 }
159
160 pub(crate) fn seed_for_test(&mut self, key: PickKey, now: Instant) {
162 self.serve(key, now);
163 }
164
165 pub(crate) fn promoted_since_for_test(&self, key: PickKey, now: Instant) -> Option<Instant> {
167 self.priority(key, now).since
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn key(window: i64, class: PickClass) -> PickKey {
176 PickKey { window, class }
177 }
178
179 #[test]
180 fn test_aging_survives_repicks_and_other_service() {
181 let start = Instant::now();
182 let l0 = key(100, PickClass::L0Merge);
183 let old = key(0, PickClass::FileReduction);
184 let overlap = key(0, PickClass::OverlapResolution);
185 let mut history = PickHistory::default();
186 history.observe([l0, old, overlap].into_iter(), start);
187 assert!(history.priority(l0, start) < history.priority(old, start));
188 for seconds in 1..=60 {
189 let now = start + Duration::from_secs(seconds);
190 history.observe([l0, old, overlap].into_iter(), now);
191 history.serve(l0, now);
192 }
193 let now = start + PROMOTION_DELAY;
194 assert!(history.priority(old, now) < history.priority(l0, now));
195 history.serve(old, now);
196 assert!(history.priority(overlap, now) < history.priority(old, now));
197 }
198
199 #[test]
200 fn test_old_windows_progress_and_obsolete_work_is_pruned() {
201 let start = Instant::now();
202 let old = key(0, PickClass::L0Merge);
203 let new = key(1, PickClass::L0Merge);
204 let mut history = PickHistory::default();
205 history.observe([old].into_iter(), start);
206 let now = start + PROMOTION_DELAY;
207 history.observe([old, new].into_iter(), now);
208 assert!(history.priority(old, now) < history.priority(new, now));
209 history.observe([new].into_iter(), now);
210 assert_eq!(1, history.waiting.len());
211 assert!(!history.waiting.contains_key(&old));
212 history.observe(std::iter::empty(), now);
213 assert!(history.waiting.is_empty());
214 }
215}