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