mito2/compaction/scheduler/
coordinator.rs1#[cfg(test)]
16#[path = "coordinator_test.rs"]
17mod tests;
18
19use std::collections::{BTreeSet, HashMap, VecDeque};
20use std::future::Future;
21use std::pin::Pin;
22use std::sync::{Arc, Mutex, MutexGuard, Weak};
23use std::task::{Context, Poll};
24use std::time::Instant;
25
26use common_telemetry::{debug, error};
27use futures::FutureExt;
28use store_api::storage::RegionId;
29use tokio::sync::Notify;
30
31use crate::error::{InvalidSchedulerStateSnafu, Result};
32use crate::metrics::{
33 COMPACTION_ADMISSION_WAIT, COMPACTION_ADMITTED_UNITS, COMPACTION_READY_UNITS,
34};
35use crate::schedule::CancellableTaskState;
36use crate::schedule::scheduler::{Job, LocalScheduler, SchedulerRef};
37
38pub(crate) struct ReadyUnit {
42 queued_at: Instant,
44 state: CancellableTaskState,
46 input_bytes: u64,
48 job: Job,
50 completion: Job,
54}
55
56impl ReadyUnit {
57 pub(crate) fn new(
63 state: CancellableTaskState,
64 input_bytes: u64,
65 job: Job,
66 completion: Job,
67 ) -> Self {
68 Self {
69 queued_at: Instant::now(),
70 state,
71 input_bytes,
72 job,
73 completion,
74 }
75 }
76
77 fn release(self) {
78 drop(self.job);
79 common_runtime::spawn_global(self.completion);
80 }
81}
82
83pub(crate) struct CompactionCoordinator {
86 executor: SchedulerRef,
89 capacity: usize,
92 state: Mutex<CoordinatorState>,
96 drained: Notify,
98}
99
100#[derive(Default)]
102struct CoordinatorState {
103 regions: HashMap<RegionId, RegionQueue>,
106 ready_regions: BTreeSet<RegionId>,
109 running: usize,
112 closed: bool,
114 dispatching: bool,
116 virtual_time: u128,
119 sequence: u64,
122}
123
124struct RegionQueue {
126 ready: VecDeque<ReadyUnit>,
128 running: usize,
130 service: u128,
133 last_served: u64,
136 retired: bool,
139}
140
141struct AdmittedUnit {
144 job: Option<Job>,
145 completion: Option<Job>,
146 state: CancellableTaskState,
147 coordinator: Weak<CompactionCoordinator>,
148 region_id: RegionId,
149 charged_cost: u128,
151 started: bool,
152}
153
154impl std::fmt::Debug for CompactionCoordinator {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 let state = self.lock();
157 f.debug_struct("CompactionCoordinator")
158 .field("capacity", &self.capacity)
159 .field("running", &state.running)
160 .field("regions", &state.regions.len())
161 .finish()
162 }
163}
164
165impl CompactionCoordinator {
166 pub(crate) fn new(executor: SchedulerRef, capacity: usize) -> Self {
172 Self {
173 executor,
174 capacity: capacity.max(1),
175 state: Mutex::new(CoordinatorState::default()),
176 drained: Notify::new(),
177 }
178 }
179
180 pub(crate) fn new_local(concurrency: usize) -> Self {
185 let concurrency = concurrency.max(1);
186 Self::new(Arc::new(LocalScheduler::new(concurrency)), concurrency)
187 }
188
189 fn lock(&self) -> MutexGuard<'_, CoordinatorState> {
190 self.state
191 .lock()
192 .unwrap_or_else(std::sync::PoisonError::into_inner)
193 }
194
195 pub(crate) fn schedule(self: &Arc<Self>, region_id: RegionId, unit: ReadyUnit) -> Result<()> {
201 {
202 let mut state = self.lock();
203 if state.closed {
204 return InvalidSchedulerStateSnafu.fail();
205 }
206 if unit.state.is_cancelled() {
209 drop(state);
210 unit.release();
211 return Ok(());
212 }
213 let service = state.virtual_time;
214 let queue = state
215 .regions
216 .entry(region_id)
217 .or_insert_with(|| RegionQueue {
218 ready: VecDeque::new(),
219 running: 0,
220 service,
221 last_served: 0,
222 retired: false,
223 });
224 queue.retired = false;
225 if queue.ready.is_empty() {
227 queue.service = queue.service.max(service);
228 }
229 queue.ready.push_back(unit);
230 state.ready_regions.insert(region_id);
231 COMPACTION_READY_UNITS.inc();
232 }
233 self.dispatch();
234 Ok(())
235 }
236
237 pub(crate) fn cancel_pending(self: &Arc<Self>, region_id: RegionId) {
241 let cancelled = {
242 let mut state = self.lock();
243 let Some(queue) = state.regions.get_mut(®ion_id) else {
244 return;
245 };
246 let mut cancelled = Vec::new();
247 let mut ready = VecDeque::new();
248 for unit in queue.ready.drain(..) {
249 if unit.state.is_cancelled() {
250 cancelled.push(unit);
251 } else {
252 ready.push_back(unit);
253 }
254 }
255 queue.ready = ready;
256 if queue.ready.is_empty() {
257 state.ready_regions.remove(®ion_id);
258 }
259 state.prune(region_id);
260 cancelled
261 };
262 for unit in cancelled {
263 Self::withdraw(unit);
264 }
265 self.drained.notify_one();
266 }
267
268 pub(crate) fn forget_region(&self, region_id: RegionId) {
271 let mut state = self.lock();
272 if let Some(queue) = state.regions.get_mut(®ion_id) {
273 queue.retired = true;
274 }
275 state.prune(region_id);
276 }
277
278 pub(crate) async fn stop(&self) -> Result<()> {
281 self.lock().closed = true;
282 loop {
283 let notified = self.drained.notified();
284 let drained = {
285 let state = self.lock();
286 state.running == 0 && state.ready_regions.is_empty()
287 };
288 if drained {
289 break;
290 }
291 notified.await;
292 }
293 self.executor.stop(true).await
294 }
295
296 fn dispatch(self: &Arc<Self>) {
297 {
298 let mut state = self.lock();
299 if state.dispatching {
300 return;
301 }
302 state.dispatching = true;
303 }
304 loop {
305 let next = {
306 let mut state = self.lock();
307 let next = (state.running < self.capacity)
308 .then(|| state.admit())
309 .flatten();
310 if next.is_none() {
311 state.dispatching = false;
312 }
313 next
314 };
315 let Some((region_id, unit, charged_cost)) = next else {
316 break;
317 };
318 let admitted = AdmittedUnit::new(self, region_id, unit, charged_cost);
319 if let Err(err) = self.executor.schedule(Box::pin(admitted)) {
322 error!(err; "Failed to submit admitted compaction for region {}", region_id);
323 }
324 }
325 }
326
327 fn release(self: &Arc<Self>, region_id: RegionId, refund: u128) {
328 {
329 let mut state = self.lock();
330 state.running -= 1;
331 if let Some(queue) = state.regions.get_mut(®ion_id) {
332 queue.running -= 1;
333 queue.service = queue.service.saturating_sub(refund);
334 }
335 state.prune(region_id);
336 }
337 self.drained.notify_one();
338 self.dispatch();
341 }
342
343 fn withdraw(unit: ReadyUnit) {
344 COMPACTION_READY_UNITS.dec();
345 unit.release();
346 }
347}
348
349impl Drop for CompactionCoordinator {
350 fn drop(&mut self) {
351 let state = self
352 .state
353 .get_mut()
354 .unwrap_or_else(std::sync::PoisonError::into_inner);
355 for queue in state.regions.values_mut() {
356 for unit in queue.ready.drain(..) {
357 Self::withdraw(unit);
358 }
359 }
360 }
361}
362
363impl CoordinatorState {
364 fn admit(&mut self) -> Option<(RegionId, ReadyUnit, u128)> {
369 let region_id = self
370 .ready_regions
371 .iter()
372 .filter_map(|id| self.regions.get(id).map(|queue| (id, queue)))
373 .min_by_key(|(id, q)| (q.service, q.last_served, **id))
374 .map(|(id, _)| *id)?;
375 let queue = self.regions.get_mut(®ion_id)?;
376 let weight = if queue.ready.len() > 1 { 2 } else { 1 };
377 let unit = queue.ready.pop_front()?;
378 if queue.ready.is_empty() {
379 self.ready_regions.remove(®ion_id);
380 }
381 COMPACTION_READY_UNITS.dec();
382 COMPACTION_ADMITTED_UNITS.inc();
383 self.virtual_time = self.virtual_time.max(queue.service);
384 let charged_cost = charge(unit.input_bytes, weight);
385 queue.service = queue.service.saturating_add(charged_cost);
386 self.sequence = self.sequence.wrapping_add(1);
387 queue.last_served = self.sequence;
388 queue.running += 1;
389 self.running += 1;
390 debug!(
391 "Admit compaction unit: region={}, input_bytes={}, weight={}, service={}, ready={}, admitted={}",
392 region_id,
393 unit.input_bytes,
394 weight,
395 queue.service,
396 queue.ready.len(),
397 queue.running,
398 );
399 Some((region_id, unit, charged_cost))
400 }
401
402 fn prune(&mut self, region_id: RegionId) {
403 if self
404 .regions
405 .get(®ion_id)
406 .is_some_and(|q| q.retired && q.running == 0 && q.ready.is_empty())
407 {
408 self.regions.remove(®ion_id);
409 }
410 }
411}
412
413fn charge(bytes: u64, weight: u128) -> u128 {
414 u128::from(bytes.max(1)) * 2 / weight
416}
417
418impl AdmittedUnit {
419 fn new(
420 coordinator: &Arc<CompactionCoordinator>,
421 region_id: RegionId,
422 unit: ReadyUnit,
423 charged_cost: u128,
424 ) -> Self {
425 COMPACTION_ADMISSION_WAIT.observe(unit.queued_at.elapsed().as_secs_f64());
426 let job = Box::pin(async move {
427 if let Err(panic) = std::panic::AssertUnwindSafe(unit.job).catch_unwind().await {
428 error!(
429 "Compaction executor panicked: {}",
430 crate::compaction::format_panic_reason(panic.as_ref())
431 );
432 }
433 });
434 Self {
435 job: Some(job),
436 completion: Some(unit.completion),
437 state: unit.state,
438 coordinator: Arc::downgrade(coordinator),
439 region_id,
440 charged_cost,
441 started: false,
442 }
443 }
444}
445
446impl Future for AdmittedUnit {
447 type Output = ();
448
449 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
450 if !self.started && self.state.is_cancelled() {
451 return Poll::Ready(());
452 }
453 self.started = true;
454 let Some(job) = self.job.as_mut() else {
455 return Poll::Ready(());
456 };
457 let poll = job.as_mut().poll(cx);
458 if poll.is_ready() {
459 drop(self.job.take());
461 }
462 poll
463 }
464}
465
466impl Drop for AdmittedUnit {
467 fn drop(&mut self) {
468 drop(self.job.take());
471 COMPACTION_ADMITTED_UNITS.dec();
472 if let Some(coordinator) = self.coordinator.upgrade() {
473 let refund = if self.started { 0 } else { self.charged_cost };
474 coordinator.release(self.region_id, refund);
475 }
476 if let Some(completion) = self.completion.take() {
477 common_runtime::spawn_global(completion);
478 }
479 }
480}