heddle_pack/store/pack/repack/
scheduler.rs1use std::{
4 collections::{HashMap, HashSet},
5 sync::{Arc, Mutex, atomic::AtomicU64, mpsc},
6 thread,
7 time::{Duration, Instant},
8};
9
10use super::{
11 CancellationToken, LoadMonitor, RepackContext, RepackError, RepackHandle, RepackOperation,
12 RepackPolicy, RepackReason, RepackReport, RepackResourceLimits, RepackSchedule, types::NoLoad,
13};
14
15#[derive(Default)]
16struct SchedulerState {
17 running: HashSet<String>,
18 next_attempt: HashMap<String, Instant>,
19 failure_count: HashMap<String, u32>,
20}
21
22#[derive(Clone)]
24pub struct RepackScheduler {
25 policy: RepackPolicy,
26 limits: RepackResourceLimits,
27 load: Arc<dyn LoadMonitor>,
28 state: Arc<Mutex<SchedulerState>>,
29 success_backoff: Duration,
30 failure_backoff: Duration,
31 maximum_failure_backoff: Duration,
32}
33
34impl RepackScheduler {
35 pub fn new(policy: RepackPolicy, limits: RepackResourceLimits) -> Self {
37 Self {
38 policy,
39 limits,
40 load: Arc::new(NoLoad),
41 state: Arc::new(Mutex::new(SchedulerState::default())),
42 success_backoff: Duration::from_secs(30 * 60),
43 failure_backoff: Duration::from_secs(30),
44 maximum_failure_backoff: Duration::from_secs(30 * 60),
45 }
46 }
47
48 pub fn with_load_monitor(mut self, load: Arc<dyn LoadMonitor>) -> Self {
50 self.load = load;
51 self
52 }
53
54 pub fn with_backoff(
56 mut self,
57 success: Duration,
58 initial_failure: Duration,
59 maximum_failure: Duration,
60 ) -> Self {
61 self.success_backoff = success;
62 self.failure_backoff = initial_failure;
63 self.maximum_failure_backoff = maximum_failure.max(initial_failure);
64 self
65 }
66
67 pub fn schedule_if_needed(
69 &self,
70 operation: Arc<dyn RepackOperation>,
71 ) -> Result<RepackSchedule, RepackError> {
72 let inventory = operation.inspect()?;
73 let Some(reason) = self.policy.evaluate(inventory) else {
74 return Ok(RepackSchedule::NotNeeded(inventory));
75 };
76 self.start(operation, reason, false, CancellationToken::default())
77 }
78
79 pub fn repack_now(
81 &self,
82 operation: Arc<dyn RepackOperation>,
83 ) -> Result<RepackSchedule, RepackError> {
84 self.start(
85 operation,
86 RepackReason::Manual,
87 true,
88 CancellationToken::default(),
89 )
90 }
91
92 pub fn repack_now_with_token(
94 &self,
95 operation: Arc<dyn RepackOperation>,
96 cancellation: CancellationToken,
97 ) -> Result<RepackSchedule, RepackError> {
98 self.start(operation, RepackReason::Manual, true, cancellation)
99 }
100
101 fn start(
102 &self,
103 operation: Arc<dyn RepackOperation>,
104 reason: RepackReason,
105 bypass_backoff: bool,
106 cancellation: CancellationToken,
107 ) -> Result<RepackSchedule, RepackError> {
108 let key = operation.key();
109 {
110 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
111 if state.running.contains(&key)
112 || state.running.len() >= self.limits.max_concurrent_operations.get()
113 {
114 return Ok(RepackSchedule::Busy);
115 }
116 if !bypass_backoff
117 && let Some(next) = state.next_attempt.get(&key)
118 && *next > Instant::now()
119 {
120 return Ok(RepackSchedule::BackingOff {
121 remaining: next.saturating_duration_since(Instant::now()),
122 });
123 }
124 state.running.insert(key.clone());
125 }
126
127 let state = Arc::clone(&self.state);
128 let load = Arc::clone(&self.load);
129 let limits = self.limits;
130 let success_backoff = self.success_backoff;
131 let failure_backoff = self.failure_backoff;
132 let maximum_failure_backoff = self.maximum_failure_backoff;
133 let worker_cancellation = cancellation.clone();
134 let (sender, receiver) = mpsc::channel();
135 let worker_state = Arc::clone(&state);
136 let worker_key = key.clone();
137 let spawn = thread::Builder::new()
138 .name(format!("heddle-repack-{key}"))
139 .spawn(move || {
140 let started = Instant::now();
141 let context = RepackContext {
142 cancellation: worker_cancellation,
143 load,
144 limits,
145 started,
146 accounted_io: AtomicU64::new(0),
147 };
148 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
149 operation.run(&context)
150 }))
151 .map_err(|_| RepackError::WorkerPanicked)
152 .and_then(|result| result)
153 .map(|outcome| RepackReport {
154 reason,
155 objects_repacked: outcome.objects_repacked,
156 bytes_repacked: outcome.bytes_repacked,
157 duration: started.elapsed(),
158 bytes_reclaimed: outcome.bytes_reclaimed,
159 });
160 finish_attempt(
161 &worker_state,
162 &worker_key,
163 &result,
164 success_backoff,
165 failure_backoff,
166 maximum_failure_backoff,
167 );
168 if let Ok(report) = result {
169 tracing::info!(
170 operation = %worker_key,
171 objects_repacked = report.objects_repacked,
172 bytes_repacked = report.bytes_repacked,
173 duration_ms = report.duration.as_millis(),
174 bytes_reclaimed = report.bytes_reclaimed,
175 "background repack completed"
176 );
177 }
178 let _ = sender.send(result);
179 });
180 let worker = match spawn {
181 Ok(worker) => worker,
182 Err(error) => {
183 state
184 .lock()
185 .unwrap_or_else(|poisoned| poisoned.into_inner())
186 .running
187 .remove(&key);
188 return Err(RepackError::operation(error));
189 }
190 };
191
192 Ok(RepackSchedule::Started(RepackHandle {
193 cancellation,
194 result: receiver,
195 worker,
196 }))
197 }
198}
199
200fn finish_attempt(
201 state: &Mutex<SchedulerState>,
202 key: &str,
203 result: &Result<RepackReport, RepackError>,
204 success_backoff: Duration,
205 failure_backoff: Duration,
206 maximum_failure_backoff: Duration,
207) {
208 let mut state = state.lock().unwrap_or_else(|error| error.into_inner());
209 state.running.remove(key);
210 let delay = if result.is_ok() {
211 state.failure_count.remove(key);
212 success_backoff
213 } else {
214 let failures = state.failure_count.entry(key.to_string()).or_default();
215 *failures = failures.saturating_add(1);
216 failure_backoff
217 .checked_mul(2u32.saturating_pow(failures.saturating_sub(1)))
218 .unwrap_or(maximum_failure_backoff)
219 .min(maximum_failure_backoff)
220 };
221 state
222 .next_attempt
223 .insert(key.to_string(), Instant::now() + delay);
224}