1use std::cmp::Reverse;
16use std::collections::{HashMap, HashSet};
17use std::future::Future;
18use std::sync::Arc;
19use std::time::Duration;
20
21use futures::stream::{BoxStream, FuturesUnordered};
22use futures::{StreamExt, pin_mut};
23use itertools::Itertools;
24use risingwave_hummock_sdk::CompactionGroupId;
25use risingwave_pb::hummock::compact_task::{self, TaskStatus};
26use risingwave_pb::hummock::level_handler::RunningCompactTask;
27use rw_futures_util::select_all;
28use thiserror_ext::AsReport;
29use tokio::sync::oneshot::{Receiver, Sender};
30use tokio::sync::watch;
31use tokio::task::JoinHandle;
32use tokio_stream::wrappers::IntervalStream;
33use tracing::warn;
34
35use crate::backup_restore::BackupManagerRef;
36use crate::hummock::metrics_utils::{trigger_lsm_stat, trigger_mv_stat};
37use crate::hummock::{HummockManager, TASK_NORMAL};
38
39#[derive(Clone, Copy)]
40enum SchedulingEvent {
41 DynamicCompaction,
42 SpaceReclaimCompaction,
43 TtlCompaction,
44 TombstoneCompaction,
45 GroupSplit,
46 GroupMerge,
47}
48
49#[derive(Clone, Copy)]
50enum MaintenanceEvent {
51 CheckDeadTask,
52 FullGc,
53}
54
55fn interval_stream<E>(period: Duration, event: E) -> BoxStream<'static, E>
56where
57 E: Clone + Send + 'static,
58{
59 let mut interval = tokio::time::interval(period);
60 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
61 interval.reset();
62 Box::pin(IntervalStream::new(interval).map(move |_| event.clone()))
63}
64
65fn spawn_periodic_loop<F, Fut>(
70 name: &'static str,
71 period: Duration,
72 shutdown_rx: watch::Receiver<bool>,
73 mut handler: F,
74) -> JoinHandle<()>
75where
76 F: FnMut() -> Fut + Send + 'static,
77 Fut: Future<Output = ()> + Send + 'static,
78{
79 spawn_serial_timer_lane(
80 name,
81 vec![interval_stream(period, ())],
82 shutdown_rx,
83 move |()| handler(),
84 )
85}
86
87fn spawn_serial_timer_lane<E, F, Fut>(
89 name: &'static str,
90 triggers: Vec<BoxStream<'static, E>>,
91 mut shutdown_rx: watch::Receiver<bool>,
92 mut handler: F,
93) -> JoinHandle<()>
94where
95 E: Send + 'static,
96 F: FnMut(E) -> Fut + Send + 'static,
97 Fut: Future<Output = ()> + Send + 'static,
98{
99 tokio::spawn(async move {
100 if triggers.is_empty() {
101 tracing::info!(
102 "Hummock timer handler loop [{}] is disabled (no intervals configured)",
103 name
104 );
105 return;
106 }
107
108 let event_stream = select_all(triggers);
109 pin_mut!(event_stream);
110
111 loop {
112 tokio::select! {
113 maybe_event = event_stream.next() => {
114 let Some(event) = maybe_event else {
115 tracing::warn!(
116 "Hummock timer handler loop [{}] event stream ended unexpectedly",
117 name
118 );
119 break;
120 };
121 handler(event).await;
122 }
123 changed = shutdown_rx.changed() => {
124 if changed.is_err() || *shutdown_rx.borrow() {
125 tracing::info!("Hummock timer handler loop [{}] is stopped", name);
126 break;
127 }
128 }
129 }
130 }
131 })
132}
133
134fn spawn_scheduling_loop(
135 hummock_manager: Arc<HummockManager>,
136 split_interval_sec: u64,
137 merge_interval_sec: u64,
138 shutdown_rx: watch::Receiver<bool>,
139) -> JoinHandle<()> {
140 let mut triggers = vec![
141 interval_stream(
142 Duration::from_secs(hummock_manager.env.opts.periodic_compaction_interval_sec),
143 SchedulingEvent::DynamicCompaction,
144 ),
145 interval_stream(
146 Duration::from_secs(
147 hummock_manager
148 .env
149 .opts
150 .periodic_space_reclaim_compaction_interval_sec,
151 ),
152 SchedulingEvent::SpaceReclaimCompaction,
153 ),
154 interval_stream(
155 Duration::from_secs(
156 hummock_manager
157 .env
158 .opts
159 .periodic_ttl_reclaim_compaction_interval_sec,
160 ),
161 SchedulingEvent::TtlCompaction,
162 ),
163 interval_stream(
164 Duration::from_secs(
165 hummock_manager
166 .env
167 .opts
168 .periodic_tombstone_reclaim_compaction_interval_sec,
169 ),
170 SchedulingEvent::TombstoneCompaction,
171 ),
172 ];
173
174 if split_interval_sec > 0 {
175 triggers.push(interval_stream(
176 Duration::from_secs(split_interval_sec),
177 SchedulingEvent::GroupSplit,
178 ));
179 }
180 if merge_interval_sec > 0 {
181 triggers.push(interval_stream(
182 Duration::from_secs(merge_interval_sec),
183 SchedulingEvent::GroupMerge,
184 ));
185 }
186
187 spawn_serial_timer_lane("scheduling", triggers, shutdown_rx, move |event| {
188 let hummock_manager = hummock_manager.clone();
189 async move {
190 if hummock_manager.env.opts.compaction_deterministic_test {
191 return;
192 }
193
194 match event {
195 SchedulingEvent::DynamicCompaction => {
196 hummock_manager
197 .on_handle_trigger_multi_group(compact_task::TaskType::Dynamic)
198 .await;
199 }
200 SchedulingEvent::SpaceReclaimCompaction => {
201 hummock_manager
202 .on_handle_trigger_multi_group(compact_task::TaskType::SpaceReclaim)
203 .await;
204 hummock_manager
206 .on_handle_trigger_multi_group(compact_task::TaskType::VnodeWatermark)
207 .await;
208 }
209 SchedulingEvent::TtlCompaction => {
210 hummock_manager
211 .on_handle_trigger_multi_group(compact_task::TaskType::Ttl)
212 .await;
213 }
214 SchedulingEvent::TombstoneCompaction => {
215 hummock_manager
216 .on_handle_trigger_multi_group(compact_task::TaskType::Tombstone)
217 .await;
218 }
219 SchedulingEvent::GroupSplit => {
220 hummock_manager.on_handle_schedule_group_split().await;
221 }
222 SchedulingEvent::GroupMerge => {
223 hummock_manager.on_handle_schedule_group_merge().await;
224 }
225 }
226 }
227 })
228}
229
230fn spawn_maintenance_loop(
231 hummock_manager: Arc<HummockManager>,
232 backup_manager: Option<BackupManagerRef>,
233 shutdown_rx: watch::Receiver<bool>,
234) -> JoinHandle<()> {
235 const CHECK_PENDING_TASK_PERIOD_SEC: u64 = 300;
236
237 let triggers = vec![
238 interval_stream(
239 Duration::from_secs(CHECK_PENDING_TASK_PERIOD_SEC),
240 MaintenanceEvent::CheckDeadTask,
241 ),
242 interval_stream(
243 Duration::from_secs(hummock_manager.env.opts.full_gc_interval_sec),
244 MaintenanceEvent::FullGc,
245 ),
246 ];
247
248 spawn_serial_timer_lane("maintenance", triggers, shutdown_rx, move |event| {
249 let hummock_manager = hummock_manager.clone();
250 let backup_manager = backup_manager.clone();
251 async move {
252 match event {
253 MaintenanceEvent::CheckDeadTask => {
254 if hummock_manager.env.opts.compaction_deterministic_test {
255 return;
256 }
257 hummock_manager.check_dead_task().await;
258 }
259 MaintenanceEvent::FullGc => {
260 let retention_sec = hummock_manager.env.opts.min_sst_retention_time_sec;
261 tokio::task::spawn(async move {
262 let _ = hummock_manager
263 .start_full_gc(Duration::from_secs(retention_sec), None, backup_manager)
264 .await
265 .inspect_err(|e| warn!(error = %e.as_report(), "Failed to start GC."));
266 });
267 }
268 }
269 }
270 })
271}
272
273async fn supervise_child_loops(
274 mut shutdown_rx: Receiver<()>,
275 child_shutdown_tx: watch::Sender<bool>,
276 child_handles: Vec<(&'static str, JoinHandle<()>)>,
277) {
278 let mut child_handles = child_handles
279 .into_iter()
280 .map(|(name, handle)| async move { (name, handle.await) })
281 .collect::<FuturesUnordered<_>>();
282
283 tokio::select! {
286 _ = &mut shutdown_rx => {
287 let _ = child_shutdown_tx.send(true);
288 while let Some((name, result)) = child_handles.next().await {
289 if let Err(e) = result {
290 warn!(
291 handler = name,
292 error = %e.as_report(),
293 "Hummock timer handler loop failed during shutdown"
294 );
295 }
296 }
297 }
298 Some((name, result)) = child_handles.next() => {
299 match result {
300 Ok(()) => {
301 panic!("Hummock timer handler loop [{name}] exited unexpectedly");
302 }
303 Err(e) if e.is_cancelled() => {
306 let _ = child_shutdown_tx.send(true);
307 while let Some((name, result)) = child_handles.next().await {
308 if let Err(e) = result {
309 warn!(
310 handler = name,
311 error = %e.as_report(),
312 "Hummock timer handler loop failed during shutdown"
313 );
314 }
315 }
316 }
317 Err(e) => {
318 panic!("Hummock timer handler loop [{name}] failed: {}", e.as_report());
319 }
320 }
321 }
322 }
323}
324
325impl HummockManager {
326 pub fn hummock_timer_task(
327 hummock_manager: Arc<Self>,
328 backup_manager: Option<BackupManagerRef>,
329 ) -> (JoinHandle<()>, Sender<()>) {
330 let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
331 let join_handle = tokio::spawn(async move {
332 const STAT_REPORT_PERIOD_SEC: u64 = 20;
333 const COMPACTION_HEARTBEAT_PERIOD_SEC: u64 = 1;
334
335 let split_interval_sec = hummock_manager
336 .env
337 .opts
338 .periodic_scheduling_compaction_group_split_interval_sec;
339 let merge_interval_sec = hummock_manager
340 .env
341 .opts
342 .periodic_scheduling_compaction_group_merge_interval_sec;
343
344 tracing::info!(
345 split_interval_sec,
346 merge_interval_sec,
347 report_interval_sec = STAT_REPORT_PERIOD_SEC,
348 heartbeat_interval_sec = COMPACTION_HEARTBEAT_PERIOD_SEC,
349 "Starting Hummock timer task"
350 );
351
352 let (child_shutdown_tx, child_shutdown_rx) = watch::channel(false);
353 let mut child_handles: Vec<(&'static str, JoinHandle<()>)> = Vec::new();
354
355 {
358 let hummock_manager = hummock_manager.clone();
359 child_handles.push((
360 "heartbeat",
361 spawn_periodic_loop(
362 "heartbeat",
363 Duration::from_secs(COMPACTION_HEARTBEAT_PERIOD_SEC),
364 child_shutdown_rx.clone(),
365 move || {
366 let hummock_manager = hummock_manager.clone();
367 async move {
368 hummock_manager
369 .handle_timer_compaction_heartbeat_expired_check()
370 .await;
371 }
372 },
373 ),
374 ));
375 }
376
377 {
379 let hummock_manager = hummock_manager.clone();
380 child_handles.push((
381 "report",
382 spawn_periodic_loop(
383 "report",
384 Duration::from_secs(STAT_REPORT_PERIOD_SEC),
385 child_shutdown_rx.clone(),
386 move || {
387 let hummock_manager = hummock_manager.clone();
388 async move {
389 hummock_manager.handle_timer_report().await;
390 }
391 },
392 ),
393 ));
394 }
395
396 child_handles.push((
400 "scheduling",
401 spawn_scheduling_loop(
402 hummock_manager.clone(),
403 split_interval_sec,
404 merge_interval_sec,
405 child_shutdown_rx.clone(),
406 ),
407 ));
408
409 child_handles.push((
412 "maintenance",
413 spawn_maintenance_loop(
414 hummock_manager.clone(),
415 backup_manager,
416 child_shutdown_rx.clone(),
417 ),
418 ));
419
420 supervise_child_loops(shutdown_rx, child_shutdown_tx, child_handles).await;
421
422 tracing::info!("Hummock timer loop is stopped");
423 });
424 (join_handle, shutdown_tx)
425 }
426
427 async fn handle_timer_report(&self) {
428 let mv_id_to_all_table_ids = self
429 .metadata_manager
430 .get_job_id_to_internal_table_ids_mapping()
431 .await;
432 let table_write_throughput_statistic_manager =
433 self.table_write_throughput_statistic_manager.read().clone();
434 let id_to_config = self.get_compaction_group_map().await;
435
436 let (current_version, version_stats) = {
440 let versioning_guard = self.versioning.read().await;
441 (
442 versioning_guard.current_version.clone(),
443 versioning_guard.version_stats.clone(),
444 )
445 };
446
447 if let Some(mv_id_to_all_table_ids) = mv_id_to_all_table_ids {
448 trigger_mv_stat(&self.metrics, &version_stats, mv_id_to_all_table_ids);
449 }
450
451 let compaction_group_count = current_version.levels.len();
452 self.metrics
453 .compaction_group_count
454 .set(compaction_group_count as i64);
455
456 let max_statistic_expired_time = std::cmp::max(
457 self.env.opts.table_stat_throuput_window_seconds_for_split,
458 self.env.opts.table_stat_throuput_window_seconds_for_merge,
459 );
460
461 for (group_id, group_levels) in ¤t_version.levels {
462 let Some(compaction_group_config) = id_to_config.get(group_id) else {
463 warn!(
464 compaction_group_id = %group_id,
465 "Missing compaction group config when reporting metrics. Skip."
466 );
467 continue;
468 };
469
470 trigger_lsm_stat(
471 &self.metrics,
472 compaction_group_config.compaction_config(),
473 group_levels,
474 *group_id,
475 );
476
477 let member_table_ids = current_version
478 .state_table_info
479 .compaction_group_member_table_ids(*group_id);
480 let group_size = member_table_ids
481 .iter()
482 .map(|table_id| {
483 version_stats
484 .table_stats
485 .get(table_id)
486 .map(|stats| stats.total_key_size + stats.total_value_size)
487 .unwrap_or(0)
488 .max(0) as u64
489 })
490 .sum::<u64>();
491 self.metrics
492 .compaction_group_size
493 .with_label_values(&[&group_id.to_string()])
494 .set(group_size as _);
495
496 let mut avg_throughput = 0;
497 for table_id in member_table_ids {
498 avg_throughput += table_write_throughput_statistic_manager
499 .avg_write_throughput(*table_id, max_statistic_expired_time as i64)
500 as u64;
501 }
502
503 self.metrics
504 .compaction_group_throughput
505 .with_label_values(&[&group_id.to_string()])
506 .set(avg_throughput as _);
507
508 let file_count = group_levels.count_ssts();
509 self.metrics
510 .compaction_group_file_count
511 .with_label_values(&[&group_id.to_string()])
512 .set(file_count as _);
513 }
514 }
515
516 async fn handle_timer_compaction_heartbeat_expired_check(&self) {
517 let expired_tasks: Vec<u64> = self
524 .compactor_manager
525 .get_heartbeat_expired_tasks()
526 .into_iter()
527 .map(|task| task.task_id)
528 .collect();
529 if !expired_tasks.is_empty() {
530 tracing::info!(
531 expired_tasks = ?expired_tasks,
532 "Heartbeat expired compaction tasks detected. Attempting to cancel tasks.",
533 );
534 if let Err(e) = self
535 .cancel_compact_tasks(expired_tasks.clone(), TaskStatus::HeartbeatCanceled)
536 .await
537 {
538 tracing::error!(
539 expired_tasks = ?expired_tasks,
540 error = %e.as_report(),
541 "Attempt to remove compaction task due to elapsed heartbeat failed. We will continue to track its heartbeat
542 until we can successfully report its status",
543 );
544 }
545 }
546 }
547}
548
549impl HummockManager {
550 async fn maybe_normalize_compaction_groups_before_merge(&self) {
551 if !self.env.opts.enable_compaction_group_normalize {
552 return;
553 }
554
555 match self
556 .normalize_overlapping_compaction_groups_with_limit(
557 self.env
558 .opts
559 .max_normalize_splits_per_round
560 .try_into()
561 .unwrap_or(usize::MAX),
562 )
563 .await
564 {
565 Ok(split_count) => {
566 if split_count > 0 {
567 tracing::info!(
568 "normalize compaction groups finished with {} split(s) before merge scheduling",
569 split_count
570 );
571 }
572 }
573 Err(e) => {
574 tracing::warn!(
575 error = %e.as_report(),
576 "failed to normalize compaction groups before merge scheduling"
577 );
578 }
579 }
580 }
581
582 async fn check_dead_task(&self) {
583 const MAX_COMPACTION_L0_MULTIPLIER: u64 = 32;
584 const MAX_COMPACTION_DURATION_SEC: u64 = 20 * 60;
585 let slowdown_groups = {
586 let versioning_guard = self.versioning.read().await;
587 let compaction_group_manager = self.compaction_group_manager.read().await;
588 let mut slowdown_groups: HashMap<CompactionGroupId, u64> = HashMap::default();
589
590 for (group_id, group_levels) in &versioning_guard.current_version.levels {
591 let compaction_group_config = compaction_group_manager
592 .try_get_compaction_group_config(*group_id)
593 .expect(
594 "compaction group config should exist for every group in current version",
595 );
596 let l0_file_size = group_levels
597 .l0
598 .sub_levels
599 .iter()
600 .map(|level| level.total_file_size)
601 .sum::<u64>();
602 if l0_file_size
603 > MAX_COMPACTION_L0_MULTIPLIER
604 * compaction_group_config
605 .compaction_config
606 .max_bytes_for_level_base
607 {
608 slowdown_groups.insert(*group_id, l0_file_size);
609 }
610 }
611
612 slowdown_groups
613 };
614 if slowdown_groups.is_empty() {
615 return;
616 }
617 let mut pending_tasks: HashMap<u64, (CompactionGroupId, usize, RunningCompactTask)> =
618 HashMap::default();
619 {
620 let compaction_guard = self.compaction.read().await;
621 for group_id in slowdown_groups.keys() {
622 if let Some(status) = compaction_guard.compaction_statuses.get(group_id) {
623 for (idx, level_handler) in status.level_handlers.iter().enumerate() {
624 let tasks = level_handler.pending_tasks().to_vec();
625 if tasks.is_empty() {
626 continue;
627 }
628 for task in tasks {
629 pending_tasks.insert(task.task_id, (*group_id, idx, task));
630 }
631 }
632 }
633 }
634 }
635 let task_ids = pending_tasks.keys().cloned().collect_vec();
636 let task_infos = self
637 .compactor_manager
638 .check_tasks_status(&task_ids, Duration::from_secs(MAX_COMPACTION_DURATION_SEC));
639 for (task_id, (compact_time, status)) in task_infos {
640 if status == TASK_NORMAL {
641 continue;
642 }
643 if let Some((group_id, level_id, task)) = pending_tasks.get(&task_id) {
644 let group_size = *slowdown_groups.get(group_id).unwrap();
645 warn!(
646 "COMPACTION SLOW: the task-{} of group-{}(size: {}MB) level-{} has not finished after {:?}, {}, it may cause pending sstable files({:?}) blocking other task.",
647 task_id,
648 group_id,
649 group_size / 1024 / 1024,
650 *level_id,
651 compact_time,
652 status,
653 task.ssts
654 );
655 }
656 }
657 }
658
659 async fn on_handle_schedule_group_split(&self) {
664 let table_write_throughput = self.table_write_throughput_statistic_manager.read().clone();
665
666 let mut group_infos = self.calculate_compaction_group_statistic().await;
667 group_infos.sort_by_key(|group| Reverse(group.group_size));
668
669 for group in group_infos {
670 if group.table_statistic.len() == 1 {
671 continue;
673 }
674
675 self.try_split_compaction_group(&table_write_throughput, group)
676 .await;
677 }
678 }
679
680 #[cfg(test)]
681 pub async fn schedule_group_split_for_test(&self) {
682 self.on_handle_schedule_group_split().await;
683 }
684
685 #[cfg(test)]
686 pub async fn schedule_group_merge_for_test(&self) {
687 self.on_handle_schedule_group_merge().await;
688 }
689
690 async fn on_handle_trigger_multi_group(&self, task_type: compact_task::TaskType) {
691 for cg_id in self.compaction_group_ids().await {
692 self.compaction_state.try_sched_compaction(
693 cg_id,
694 task_type,
695 super::compaction::ScheduleTrigger::Periodic,
696 );
697 }
698 }
699
700 async fn on_handle_schedule_group_merge(&self) {
706 self.maybe_normalize_compaction_groups_before_merge().await;
707
708 let created_tables = match self.metadata_manager.get_created_table_ids().await {
709 Ok(created_tables) => HashSet::from_iter(created_tables),
710 Err(err) => {
711 tracing::warn!(error = %err.as_report(), "failed to fetch created table ids");
712 return;
713 }
714 };
715 let table_write_throughput_statistic_manager =
716 self.table_write_throughput_statistic_manager.read().clone();
717 let mut group_infos = self.calculate_compaction_group_statistic().await;
718 group_infos.sort_by_key(|group| group.table_statistic.keys().next().copied());
720
721 let group_count = group_infos.len();
722 if group_count < 2 {
723 return;
724 }
725
726 let mut base = 0;
727 let mut candidate = 1;
728
729 while candidate < group_count {
730 let group = &group_infos[base];
731 let next_group = &group_infos[candidate];
732 match self
733 .try_merge_compaction_group(
734 &table_write_throughput_statistic_manager,
735 group,
736 next_group,
737 &created_tables,
738 )
739 .await
740 {
741 Ok(_) => candidate += 1,
742 Err(e) => {
743 tracing::debug!(
744 error = %e.as_report(),
745 "Failed to merge compaction group",
746 );
747 base = candidate;
748 candidate = base + 1;
749 }
750 }
751 }
752 }
753}
754
755#[cfg(test)]
756mod tests {
757 mod merge_scheduling {
758 use std::sync::Arc;
759 use std::time::Duration;
760
761 use itertools::Itertools;
762 use risingwave_common::catalog::TableId;
763 use risingwave_hummock_sdk::CompactionGroupId;
764 use risingwave_hummock_sdk::version::HummockVersion;
765 use risingwave_meta_model::WorkerId;
766 use risingwave_pb::common::worker_node::Property;
767 use risingwave_pb::common::{HostAddress, WorkerType};
768
769 use crate::controller::catalog::CatalogController;
770 use crate::controller::cluster::{ClusterController, ClusterControllerRef};
771 use crate::hummock::compaction::compaction_config::CompactionConfigBuilder;
772 use crate::hummock::{CompactorManager, HummockManager, HummockManagerRef};
773 use crate::manager::{MetaOpts, MetaSrvEnv};
774
775 async fn setup_compute_env_with_meta_opts(
776 port: i32,
777 opts: MetaOpts,
778 ) -> (
779 MetaSrvEnv,
780 HummockManagerRef,
781 ClusterControllerRef,
782 WorkerId,
783 ) {
784 let env = MetaSrvEnv::for_test_opts(opts, |_| ()).await;
785 let cluster_ctl = Arc::new(
786 ClusterController::new(env.clone(), Duration::from_secs(1))
787 .await
788 .unwrap(),
789 );
790 let catalog_ctl = Arc::new(CatalogController::new(env.clone()).await.unwrap());
791 let compactor_manager = Arc::new(CompactorManager::for_test());
792 let (compactor_streams_change_tx, _compactor_streams_change_rx) =
793 tokio::sync::mpsc::unbounded_channel();
794 let config = CompactionConfigBuilder::new()
795 .level0_tier_compact_file_number(1)
796 .level0_max_compact_file_number(130)
797 .level0_sub_level_compact_level_count(1)
798 .level0_overlapping_sub_level_compact_level_count(1)
799 .build();
800 let hummock_manager = HummockManager::with_config(
801 env.clone(),
802 cluster_ctl.clone(),
803 catalog_ctl,
804 Arc::new(Default::default()),
805 compactor_manager,
806 config,
807 compactor_streams_change_tx,
808 )
809 .await;
810
811 let worker_id = cluster_ctl
812 .add_worker(
813 WorkerType::ComputeNode,
814 HostAddress {
815 host: "127.0.0.1".to_owned(),
816 port,
817 },
818 Property {
819 is_streaming: true,
820 is_serving: true,
821 parallelism: 4,
822 ..Default::default()
823 },
824 Default::default(),
825 )
826 .await
827 .unwrap();
828 (env, hummock_manager, cluster_ctl, worker_id)
829 }
830
831 async fn get_compaction_group_id_by_table_id(
832 hummock_manager: HummockManagerRef,
833 table_id: u32,
834 ) -> CompactionGroupId {
835 hummock_manager
836 .get_current_version()
837 .await
838 .state_table_info
839 .info()
840 .get(&TableId::new(table_id))
841 .unwrap()
842 .compaction_group_id
843 }
844
845 fn member_table_ids(version: &HummockVersion, group_id: CompactionGroupId) -> Vec<u32> {
846 version
847 .state_table_info
848 .compaction_group_member_table_ids(group_id)
849 .iter()
850 .map(|table_id| table_id.as_raw_id())
851 .collect_vec()
852 }
853
854 fn assert_no_group_overlap(version: &HummockVersion) {
855 let mut ranges = version
856 .levels
857 .keys()
858 .filter_map(|group_id| {
859 let members = member_table_ids(version, *group_id);
860 (!members.is_empty())
861 .then(|| (*members.first().unwrap(), *members.last().unwrap()))
862 })
863 .collect_vec();
864 ranges.sort_by_key(|(min_table_id, _)| *min_table_id);
865 assert!(ranges.windows(2).all(|window| window[0].1 < window[1].0));
866 }
867
868 #[tokio::test]
869 async fn test_merge_scheduling_normalizes_when_split_scheduling_is_disabled() {
870 let mut opts = MetaOpts::test(false);
871 opts.enable_compaction_group_normalize = true;
872 opts.periodic_scheduling_compaction_group_split_interval_sec = 0;
873
874 let (_env, hummock_manager, _, _worker_id) =
875 setup_compute_env_with_meta_opts(80, opts).await;
876 hummock_manager
877 .register_table_ids_for_test(&[(64, 2.into()), (80, 2.into())])
878 .await
879 .unwrap();
880 hummock_manager
881 .register_table_ids_for_test(&[(65, 3.into()), (81, 3.into()), (83, 3.into())])
882 .await
883 .unwrap();
884
885 let cg_64 = get_compaction_group_id_by_table_id(hummock_manager.clone(), 64).await;
886 let cg_65 = get_compaction_group_id_by_table_id(hummock_manager.clone(), 65).await;
887
888 hummock_manager.on_handle_schedule_group_merge().await;
889
890 let version = hummock_manager.get_current_version().await;
891 assert_eq!(member_table_ids(&version, cg_64), vec![64]);
892 assert_eq!(member_table_ids(&version, cg_65), vec![65]);
893 assert_no_group_overlap(&version);
894 }
895 }
896
897 mod periodic_loop {
898 use std::sync::Arc;
899 use std::sync::atomic::{AtomicUsize, Ordering};
900 use std::time::Duration;
901
902 use tokio::sync::watch;
903
904 use super::super::{interval_stream, spawn_serial_timer_lane};
905
906 #[tokio::test(start_paused = true)]
907 async fn test_serial_timer_lane_does_not_run_handlers_concurrently() {
908 #[derive(Clone, Copy)]
909 enum TestEvent {
910 Split,
911 Merge,
912 }
913
914 struct DecrOnDrop(Arc<AtomicUsize>);
915
916 impl Drop for DecrOnDrop {
917 fn drop(&mut self) {
918 self.0.fetch_sub(1, Ordering::SeqCst);
919 }
920 }
921
922 let (shutdown_tx, shutdown_rx) = watch::channel(false);
923 let in_handler = Arc::new(AtomicUsize::new(0));
924 let max_in_handler = Arc::new(AtomicUsize::new(0));
925 let handled = Arc::new(AtomicUsize::new(0));
926
927 let handle = spawn_serial_timer_lane(
928 "test_serial_lane",
929 vec![
930 interval_stream(Duration::from_millis(10), TestEvent::Split),
931 interval_stream(Duration::from_millis(10), TestEvent::Merge),
932 ],
933 shutdown_rx,
934 {
935 let in_handler = in_handler.clone();
936 let max_in_handler = max_in_handler.clone();
937 let handled = handled.clone();
938 move |_event| {
939 let in_handler = in_handler.clone();
940 let max_in_handler = max_in_handler.clone();
941 let handled = handled.clone();
942 async move {
943 handled.fetch_add(1, Ordering::SeqCst);
944 let now = in_handler.fetch_add(1, Ordering::SeqCst) + 1;
945 let _decr = DecrOnDrop(in_handler);
946 max_in_handler.fetch_max(now, Ordering::SeqCst);
947 tokio::time::sleep(Duration::from_millis(15)).await;
948 }
949 }
950 },
951 );
952
953 tokio::time::sleep(Duration::from_millis(120)).await;
954 shutdown_tx.send(true).unwrap();
955 handle.await.unwrap();
956
957 assert!(handled.load(Ordering::SeqCst) > 0);
958 assert_eq!(max_in_handler.load(Ordering::SeqCst), 1);
959 }
960 }
961}