Skip to main content

risingwave_meta/hummock/
compactor_manager.rs

1// Copyright 2022 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use std::time::{Duration, Instant, SystemTime};
18
19use fail::fail_point;
20use parking_lot::RwLock;
21use risingwave_hummock_sdk::compact::statistics_compact_task;
22use risingwave_hummock_sdk::compact_task::CompactTask;
23use risingwave_hummock_sdk::{HummockCompactionTaskId, HummockContextId};
24use risingwave_pb::hummock::subscribe_compaction_event_response::Event as ResponseEvent;
25use risingwave_pb::hummock::{
26    CancelCompactTask, CompactTaskProgress, SubscribeCompactionEventResponse,
27};
28use risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event as IcebergResponseEvent;
29use risingwave_pb::iceberg_compaction::{
30    CancelCompactTask as IcebergCancelCompactTask, SubscribeIcebergCompactionEventResponse,
31};
32use risingwave_pb::id::IcebergCompactionTaskId;
33use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
34
35use crate::MetaResult;
36use crate::hummock::model::ext::compaction_task_model_to_assignment;
37use crate::manager::MetaSrvEnv;
38use crate::model::MetadataModelError;
39
40pub type CompactorManagerRef = Arc<CompactorManager>;
41pub type IcebergCompactorManagerRef = Arc<IcebergCompactorManager>;
42
43pub const TASK_RUN_TOO_LONG: &str = "running too long";
44pub const TASK_NOT_FOUND: &str = "task not found";
45pub const TASK_NORMAL: &str = "task is normal, please wait some time";
46
47type CompactorSubscribeStreamSender = UnboundedSender<MetaResult<SubscribeCompactionEventResponse>>;
48type CompactorSubscribeStreamReceiver =
49    UnboundedReceiver<MetaResult<SubscribeCompactionEventResponse>>;
50
51type IcebergCompactorSubscribeStreamSender =
52    UnboundedSender<MetaResult<SubscribeIcebergCompactionEventResponse>>;
53type IcebergCompactorSubscribeStreamReceiver =
54    UnboundedReceiver<MetaResult<SubscribeIcebergCompactionEventResponse>>;
55
56type CompactorSubscribeResponseEvent = ResponseEvent;
57
58type IcebergCompactorSubscribeResponseEvent = IcebergResponseEvent;
59
60/// Wraps the stream between meta node and compactor node.
61/// Compactor node will re-establish the stream when the previous one fails.
62pub struct Compactor {
63    context_id: HummockContextId,
64    sender: CompactorSubscribeStreamSender,
65}
66
67pub struct IcebergCompactor {
68    context_id: HummockContextId,
69    sender: IcebergCompactorSubscribeStreamSender,
70}
71
72struct TaskHeartbeat {
73    task: CompactTask,
74    num_ssts_sealed: u32,
75    num_ssts_uploaded: u32,
76    num_progress_key: u64,
77    num_pending_read_io: u64,
78    num_pending_write_io: u64,
79    create_time: Instant,
80    expire_at: u64,
81
82    update_at: u64,
83}
84
85impl Compactor {
86    pub fn new(context_id: HummockContextId, sender: CompactorSubscribeStreamSender) -> Self {
87        Self { context_id, sender }
88    }
89
90    pub fn send_event(&self, event: CompactorSubscribeResponseEvent) -> MetaResult<()> {
91        fail_point!("compaction_send_task_fail", |_| Err(anyhow::anyhow!(
92            "compaction_send_task_fail"
93        )
94        .into()));
95
96        self.sender
97            .send(Ok(SubscribeCompactionEventResponse {
98                create_at: SystemTime::now()
99                    .duration_since(SystemTime::UNIX_EPOCH)
100                    .expect("Clock may have gone backwards")
101                    .as_millis() as u64,
102                event: Some(event),
103            }))
104            .map_err(|e| anyhow::anyhow!(e))?;
105
106        Ok(())
107    }
108
109    pub fn cancel_task(&self, task_id: u64) -> MetaResult<()> {
110        self.send_event(ResponseEvent::CancelCompactTask(CancelCompactTask {
111            context_id: self.context_id,
112            task_id,
113        }))
114    }
115
116    pub fn cancel_tasks(&self, task_ids: &Vec<u64>) -> MetaResult<()> {
117        for task_id in task_ids {
118            self.cancel_task(*task_id)?;
119        }
120        Ok(())
121    }
122
123    pub fn context_id(&self) -> HummockContextId {
124        self.context_id
125    }
126}
127
128pub trait CompactorManagerTrait {
129    fn add_compactor(&self, context_id: HummockContextId) -> CompactorSubscribeStreamReceiver;
130    fn remove_compactor(&self, context_id: HummockContextId);
131    fn get_compactor(&self, context_id: HummockContextId) -> Option<Arc<Compactor>>;
132    fn next_compactor(&self) -> Option<Arc<Compactor>>;
133    fn compactor_num(&self) -> usize;
134}
135
136/// `CompactorManagerInner` maintains compactors which can process compact task.
137/// A compact task is tracked in `HummockManager::Compaction` via both `CompactStatus` and
138/// `CompactTaskAssignment`.
139///
140/// A compact task can be in one of these states:
141/// 1. Success: an assigned task is reported as success via `CompactStatus::report_compact_task`.
142///    It's the final state.
143/// 2. Failed: an Failed task is reported as success via `CompactStatus::report_compact_task`.
144///    It's the final state.
145/// 3. Cancelled: a task is reported as cancelled via `CompactStatus::report_compact_task`. It's
146///    the final state.
147pub struct CompactorManagerInner {
148    pub task_expired_seconds: u64,
149    pub heartbeat_expired_seconds: u64,
150    task_heartbeats: HashMap<HummockCompactionTaskId, TaskHeartbeat>,
151
152    /// The outer lock is a `RwLock`, so we should still be able to modify each compactor
153    pub compactor_map: HashMap<HummockContextId, Arc<Compactor>>,
154}
155
156impl CompactorManagerInner {
157    pub async fn with_meta(env: MetaSrvEnv) -> MetaResult<Self> {
158        use risingwave_meta_model::compaction_task;
159        use sea_orm::EntityTrait;
160        // Retrieve the existing task assignments from metastore.
161        let task_assignment: Vec<_> = compaction_task::Entity::find()
162            .all(&env.meta_store_ref().conn)
163            .await
164            .map_err(MetadataModelError::from)?
165            .into_iter()
166            .map(compaction_task_model_to_assignment)
167            .collect();
168        let mut manager = Self {
169            task_expired_seconds: env.opts.compaction_task_max_progress_interval_secs,
170            heartbeat_expired_seconds: env.opts.compaction_task_max_heartbeat_interval_secs,
171            task_heartbeats: Default::default(),
172            compactor_map: Default::default(),
173        };
174        // Initialize heartbeat for existing tasks.
175        task_assignment.into_iter().for_each(|assignment| {
176            manager.initiate_task_heartbeat(assignment.compact_task);
177        });
178        Ok(manager)
179    }
180
181    /// Only used for unit test.
182    pub fn for_test() -> Self {
183        Self {
184            task_expired_seconds: 1,
185            heartbeat_expired_seconds: 1,
186            task_heartbeats: Default::default(),
187            compactor_map: Default::default(),
188        }
189    }
190
191    pub fn next_compactor(&self) -> Option<Arc<Compactor>> {
192        use rand::Rng;
193        if self.compactor_map.is_empty() {
194            return None;
195        }
196
197        let rand_index = rand::rng().random_range(0..self.compactor_map.len());
198        let compactor = self.compactor_map.values().nth(rand_index).unwrap().clone();
199
200        Some(compactor)
201    }
202
203    /// Retrieve a receiver of tasks for the compactor identified by `context_id`. The sender should
204    /// be obtained by calling one of the compactor getters.
205    ///
206    ///  If `add_compactor` is called with the same `context_id` more than once, the only cause
207    /// would be compactor re-subscription, as `context_id` is a monotonically increasing
208    /// sequence.
209    pub fn add_compactor(
210        &mut self,
211        context_id: HummockContextId,
212    ) -> CompactorSubscribeStreamReceiver {
213        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
214
215        self.compactor_map
216            .insert(context_id, Arc::new(Compactor::new(context_id, tx)));
217
218        tracing::info!(context_id = %context_id, "Added compactor session");
219
220        rx
221    }
222
223    /// Used when meta exiting to support graceful shutdown.
224    pub fn abort_all_compactors(&mut self) {
225        while let Some(compactor) = self.next_compactor() {
226            self.remove_compactor(compactor.context_id);
227        }
228    }
229
230    pub fn remove_compactor(&mut self, context_id: HummockContextId) {
231        if self.compactor_map.remove(&context_id).is_some() {
232            tracing::info!(context_id = %context_id, "Removed compactor session")
233        };
234    }
235
236    pub fn get_compactor(&self, context_id: HummockContextId) -> Option<Arc<Compactor>> {
237        self.compactor_map.get(&context_id).cloned()
238    }
239
240    pub fn check_tasks_status(
241        &self,
242        tasks: &[HummockCompactionTaskId],
243        slow_task_duration: Duration,
244    ) -> HashMap<HummockCompactionTaskId, (Duration, &'static str)> {
245        let tasks_ids: HashSet<u64> = HashSet::from_iter(tasks.to_vec());
246        let mut ret = HashMap::default();
247        for TaskHeartbeat {
248            task, create_time, ..
249        } in self.task_heartbeats.values()
250        {
251            if !tasks_ids.contains(&task.task_id) {
252                continue;
253            }
254            let pending_time = create_time.elapsed();
255            if pending_time > slow_task_duration {
256                ret.insert(task.task_id, (pending_time, TASK_RUN_TOO_LONG));
257            } else {
258                ret.insert(task.task_id, (pending_time, TASK_NORMAL));
259            }
260        }
261
262        for task_id in tasks {
263            if !ret.contains_key(task_id) {
264                ret.insert(*task_id, (Duration::from_secs(0), TASK_NOT_FOUND));
265            }
266        }
267        ret
268    }
269
270    pub fn get_heartbeat_expired_tasks(&self) -> Vec<CompactTask> {
271        let heartbeat_expired_ts: u64 = SystemTime::now()
272            .duration_since(SystemTime::UNIX_EPOCH)
273            .expect("Clock may have gone backwards")
274            .as_secs()
275            - self.heartbeat_expired_seconds;
276        Self::get_heartbeat_expired_tasks_impl(&self.task_heartbeats, heartbeat_expired_ts)
277    }
278
279    fn get_heartbeat_expired_tasks_impl(
280        task_heartbeats: &HashMap<HummockCompactionTaskId, TaskHeartbeat>,
281        heartbeat_expired_ts: u64,
282    ) -> Vec<CompactTask> {
283        let mut cancellable_tasks = vec![];
284        const MAX_TASK_DURATION_SEC: u64 = 2700;
285
286        for TaskHeartbeat {
287            expire_at,
288            task,
289            create_time,
290            num_ssts_sealed,
291            num_ssts_uploaded,
292            num_progress_key,
293            num_pending_read_io,
294            num_pending_write_io,
295            update_at,
296        } in task_heartbeats.values()
297        {
298            if *update_at < heartbeat_expired_ts {
299                cancellable_tasks.push(task.clone());
300            }
301
302            let task_duration_too_long = create_time.elapsed().as_secs() > MAX_TASK_DURATION_SEC;
303            if task_duration_too_long {
304                let compact_task_statistics = statistics_compact_task(task);
305                tracing::info!(
306                    "CompactionGroupId {} Task {} duration too long create_time {:?} expire_at {:?} num_ssts_sealed {} num_ssts_uploaded {} num_progress_key {} \
307                        pending_read_io_count {} pending_write_io_count {} target_level {} \
308                        base_level {} target_sub_level_id {} task_type {} compact_task_statistics {:?}",
309                    task.compaction_group_id,
310                    task.task_id,
311                    create_time,
312                    expire_at,
313                    num_ssts_sealed,
314                    num_ssts_uploaded,
315                    num_progress_key,
316                    num_pending_read_io,
317                    num_pending_write_io,
318                    task.target_level,
319                    task.base_level,
320                    task.target_sub_level_id,
321                    task.task_type.as_str_name(),
322                    compact_task_statistics
323                );
324            }
325        }
326        cancellable_tasks
327    }
328
329    pub fn initiate_task_heartbeat(&mut self, task: CompactTask) {
330        let now = SystemTime::now()
331            .duration_since(SystemTime::UNIX_EPOCH)
332            .expect("Clock may have gone backwards")
333            .as_secs();
334        self.task_heartbeats.insert(
335            task.task_id,
336            TaskHeartbeat {
337                task,
338                num_ssts_sealed: 0,
339                num_ssts_uploaded: 0,
340                num_progress_key: 0,
341                num_pending_read_io: 0,
342                num_pending_write_io: 0,
343                create_time: Instant::now(),
344                expire_at: now + self.task_expired_seconds,
345                update_at: now,
346            },
347        );
348    }
349
350    pub fn remove_task_heartbeat(&mut self, task_id: u64) {
351        self.task_heartbeats.remove(&task_id).unwrap();
352    }
353
354    /// Returns expired tracked task IDs and untracked task IDs to stop on the compactor.
355    pub fn update_task_heartbeats(
356        &mut self,
357        progress_list: &Vec<CompactTaskProgress>,
358    ) -> (Vec<HummockCompactionTaskId>, Vec<HummockCompactionTaskId>) {
359        let now = SystemTime::now()
360            .duration_since(SystemTime::UNIX_EPOCH)
361            .expect("Clock may have gone backwards")
362            .as_secs();
363        let mut cancel_tasks = vec![];
364        let mut untracked_tasks = vec![];
365        for progress in progress_list {
366            if let Some(task_ref) = self.task_heartbeats.get_mut(&progress.task_id) {
367                task_ref.update_at = now;
368
369                if task_ref.num_ssts_sealed < progress.num_ssts_sealed
370                    || task_ref.num_ssts_uploaded < progress.num_ssts_uploaded
371                    || task_ref.num_progress_key < progress.num_progress_key
372                {
373                    // Refresh the expired of the task as it is showing progress.
374                    task_ref.expire_at = now + self.task_expired_seconds;
375                    task_ref.num_ssts_sealed = progress.num_ssts_sealed;
376                    task_ref.num_ssts_uploaded = progress.num_ssts_uploaded;
377                    task_ref.num_progress_key = progress.num_progress_key;
378                }
379                task_ref.num_pending_read_io = progress.num_pending_read_io;
380                task_ref.num_pending_write_io = progress.num_pending_write_io;
381
382                // timeout check
383                if task_ref.expire_at < now {
384                    // cancel
385                    cancel_tasks.push(progress.task_id)
386                }
387            } else {
388                // Meta may have already canceled the task, for example through risectl.
389                // Keep sending cancellation while the compactor still reports it.
390                untracked_tasks.push(progress.task_id);
391            }
392        }
393
394        (cancel_tasks, untracked_tasks)
395    }
396
397    pub fn compactor_num(&self) -> usize {
398        self.compactor_map.len()
399    }
400
401    pub fn get_progress(&self) -> Vec<CompactTaskProgress> {
402        self.task_heartbeats
403            .values()
404            .map(|hb| CompactTaskProgress {
405                task_id: hb.task.task_id,
406                num_ssts_sealed: hb.num_ssts_sealed,
407                num_ssts_uploaded: hb.num_ssts_uploaded,
408                num_progress_key: hb.num_progress_key,
409                num_pending_read_io: hb.num_pending_read_io,
410                num_pending_write_io: hb.num_pending_write_io,
411                compaction_group_id: Some(hb.task.compaction_group_id),
412            })
413            .collect()
414    }
415}
416
417pub struct CompactorManager {
418    inner: Arc<RwLock<CompactorManagerInner>>,
419}
420
421impl CompactorManager {
422    pub async fn with_meta(env: MetaSrvEnv) -> MetaResult<Self> {
423        let inner = CompactorManagerInner::with_meta(env).await?;
424
425        Ok(Self {
426            inner: Arc::new(RwLock::new(inner)),
427        })
428    }
429
430    /// Only used for unit test.
431    pub fn for_test() -> Self {
432        let inner = CompactorManagerInner::for_test();
433        Self {
434            inner: Arc::new(RwLock::new(inner)),
435        }
436    }
437
438    pub fn next_compactor(&self) -> Option<Arc<Compactor>> {
439        self.inner.read().next_compactor()
440    }
441
442    pub fn add_compactor(&self, context_id: HummockContextId) -> CompactorSubscribeStreamReceiver {
443        self.inner.write().add_compactor(context_id)
444    }
445
446    pub fn abort_all_compactors(&self) {
447        self.inner.write().abort_all_compactors();
448    }
449
450    pub fn remove_compactor(&self, context_id: HummockContextId) {
451        self.inner.write().remove_compactor(context_id)
452    }
453
454    pub fn get_compactor(&self, context_id: HummockContextId) -> Option<Arc<Compactor>> {
455        self.inner.read().get_compactor(context_id)
456    }
457
458    pub fn check_tasks_status(
459        &self,
460        tasks: &[HummockCompactionTaskId],
461        slow_task_duration: Duration,
462    ) -> HashMap<HummockCompactionTaskId, (Duration, &'static str)> {
463        self.inner
464            .read()
465            .check_tasks_status(tasks, slow_task_duration)
466    }
467
468    pub fn get_heartbeat_expired_tasks(&self) -> Vec<CompactTask> {
469        self.inner.read().get_heartbeat_expired_tasks()
470    }
471
472    pub fn initiate_task_heartbeat(&self, task: CompactTask) {
473        self.inner.write().initiate_task_heartbeat(task);
474    }
475
476    pub fn remove_task_heartbeat(&self, task_id: u64) {
477        self.inner.write().remove_task_heartbeat(task_id);
478    }
479
480    pub fn update_task_heartbeats(
481        &self,
482        progress_list: &Vec<CompactTaskProgress>,
483    ) -> (Vec<HummockCompactionTaskId>, Vec<HummockCompactionTaskId>) {
484        self.inner.write().update_task_heartbeats(progress_list)
485    }
486
487    pub fn compactor_num(&self) -> usize {
488        self.inner.read().compactor_num()
489    }
490
491    pub fn get_progress(&self) -> Vec<CompactTaskProgress> {
492        self.inner.read().get_progress()
493    }
494}
495
496impl IcebergCompactor {
497    pub fn new(
498        context_id: HummockContextId,
499        sender: IcebergCompactorSubscribeStreamSender,
500    ) -> Self {
501        Self { context_id, sender }
502    }
503
504    pub fn context_id(&self) -> HummockContextId {
505        self.context_id
506    }
507
508    pub fn send_event(&self, event: IcebergCompactorSubscribeResponseEvent) -> MetaResult<()> {
509        fail_point!("iceberg_compaction_send_task_fail", |_| Err(
510            anyhow::anyhow!("iceberg_compaction_send_task_fail").into()
511        ));
512
513        self.sender
514            .send(Ok(SubscribeIcebergCompactionEventResponse {
515                create_at: SystemTime::now()
516                    .duration_since(SystemTime::UNIX_EPOCH)
517                    .expect("Clock may have gone backwards")
518                    .as_millis() as u64,
519                event: Some(event),
520            }))
521            .map_err(|e| anyhow::anyhow!(e))?;
522
523        Ok(())
524    }
525
526    pub fn cancel_task(&self, task_id: IcebergCompactionTaskId) -> MetaResult<()> {
527        self.send_event(IcebergResponseEvent::CancelCompactTask(
528            IcebergCancelCompactTask { task_id },
529        ))
530    }
531}
532
533pub struct IcebergCompactorManagerInner {
534    pub compactor_map: HashMap<HummockContextId, Arc<IcebergCompactor>>,
535}
536
537pub struct IcebergCompactorManager {
538    inner: Arc<RwLock<IcebergCompactorManagerInner>>,
539}
540
541impl IcebergCompactorManager {
542    pub fn new() -> Self {
543        Self {
544            inner: Arc::new(RwLock::new(IcebergCompactorManagerInner {
545                compactor_map: HashMap::new(),
546            })),
547        }
548    }
549
550    pub fn add_compactor(
551        &self,
552        context_id: HummockContextId,
553    ) -> IcebergCompactorSubscribeStreamReceiver {
554        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
555        self.inner
556            .write()
557            .compactor_map
558            .insert(context_id, Arc::new(IcebergCompactor::new(context_id, tx)));
559        tracing::info!(context_id = %context_id, "Added iceberg compactor session");
560        rx
561    }
562
563    pub fn remove_compactor(&self, context_id: HummockContextId) {
564        if self
565            .inner
566            .write()
567            .compactor_map
568            .remove(&context_id)
569            .is_some()
570        {
571            tracing::info!(context_id = %context_id, "Removed iceberg compactor session");
572        }
573    }
574
575    pub fn get_compactor(&self, context_id: HummockContextId) -> Option<Arc<IcebergCompactor>> {
576        self.inner.read().compactor_map.get(&context_id).cloned()
577    }
578
579    pub fn next_compactor(&self) -> Option<Arc<IcebergCompactor>> {
580        use rand::Rng;
581        let compactor_map = &self.inner.read().compactor_map;
582        if compactor_map.is_empty() {
583            return None;
584        }
585        let rand_index = rand::rng().random_range(0..compactor_map.len());
586        compactor_map.values().nth(rand_index).cloned()
587    }
588
589    pub fn compactor_num(&self) -> usize {
590        self.inner.read().compactor_map.len()
591    }
592}
593
594#[cfg(test)]
595mod tests {
596    use std::sync::Arc;
597    use std::time::Duration;
598
599    use risingwave_hummock_sdk::compact_task::CompactTask;
600    use risingwave_hummock_sdk::compaction_group::StaticCompactionGroupId;
601    use risingwave_pb::hummock::CompactTaskProgress;
602    use risingwave_pb::iceberg_compaction::subscribe_iceberg_compaction_event_response::Event as IcebergResponseEvent;
603    use risingwave_rpc_client::HummockMetaClient;
604
605    use super::CompactorManagerInner;
606    use crate::hummock::compaction::selector::default_compaction_selector;
607    use crate::hummock::test_utils::{
608        add_ssts, register_table_ids_to_compaction_group, setup_compute_env,
609    };
610    use crate::hummock::{CompactorManager, IcebergCompactorManager, MockHummockMetaClient};
611
612    #[test]
613    fn test_update_task_heartbeats_returns_cancellation_ids() {
614        let mut manager = CompactorManagerInner::for_test();
615        for task_id in [1, 2] {
616            manager.initiate_task_heartbeat(CompactTask {
617                task_id,
618                ..Default::default()
619            });
620        }
621        manager.task_heartbeats.get_mut(&1).unwrap().expire_at = 0;
622        manager.task_heartbeats.get_mut(&2).unwrap().expire_at = u64::MAX;
623
624        let progress = [1, 2, 3]
625            .into_iter()
626            .map(|task_id| CompactTaskProgress {
627                task_id,
628                ..Default::default()
629            })
630            .collect();
631        let (expired, untracked) = manager.update_task_heartbeats(&progress);
632
633        assert_eq!(expired, vec![1]);
634        assert_eq!(untracked, vec![3]);
635        assert_eq!(manager.get_progress().len(), 2);
636    }
637
638    #[tokio::test]
639    async fn test_compactor_manager() {
640        // Initialize metastore with task assignment.
641        let (env, context_id) = {
642            let (env, hummock_manager, _cluster_manager, worker_id) = setup_compute_env(80).await;
643            let context_id = worker_id as _;
644            let hummock_meta_client: Arc<dyn HummockMetaClient> = Arc::new(
645                MockHummockMetaClient::new(hummock_manager.clone(), context_id),
646            );
647            let compactor_manager = hummock_manager.compactor_manager.clone();
648            register_table_ids_to_compaction_group(
649                hummock_manager.as_ref(),
650                &[1],
651                StaticCompactionGroupId::StateDefault,
652            )
653            .await;
654            let _sst_infos =
655                add_ssts(1, hummock_manager.as_ref(), hummock_meta_client.clone()).await;
656            let _receiver = compactor_manager.add_compactor(context_id);
657            hummock_manager
658                .get_compact_task(
659                    context_id,
660                    StaticCompactionGroupId::StateDefault,
661                    &mut *default_compaction_selector(),
662                )
663                .await
664                .unwrap()
665                .unwrap();
666            (env, context_id)
667        };
668
669        // Restart. Set task_expired_seconds to 0 only to speed up test.
670        let compactor_manager = CompactorManager::with_meta(env).await.unwrap();
671        // Because task assignment exists.
672        // Because compactor gRPC is not established yet.
673        assert_eq!(compactor_manager.compactor_num(), 0);
674        assert!(compactor_manager.get_compactor(context_id).is_none());
675
676        // Ensure task is expired.
677        tokio::time::sleep(Duration::from_secs(2)).await;
678        let expired = compactor_manager.get_heartbeat_expired_tasks();
679        assert_eq!(expired.len(), 1);
680
681        // Mimic no-op compaction heartbeat
682        assert_eq!(compactor_manager.get_heartbeat_expired_tasks().len(), 1);
683
684        // Mimic compaction heartbeat with invalid task id
685        compactor_manager.update_task_heartbeats(&vec![CompactTaskProgress {
686            task_id: expired[0].task_id + 1,
687            num_ssts_sealed: 1,
688            num_ssts_uploaded: 1,
689            num_progress_key: 100,
690            ..Default::default()
691        }]);
692        assert_eq!(compactor_manager.get_heartbeat_expired_tasks().len(), 1);
693
694        // Mimic effective compaction heartbeat
695        compactor_manager.update_task_heartbeats(&vec![CompactTaskProgress {
696            task_id: expired[0].task_id,
697            num_ssts_sealed: 1,
698            num_ssts_uploaded: 1,
699            num_progress_key: 100,
700            ..Default::default()
701        }]);
702        assert_eq!(compactor_manager.get_heartbeat_expired_tasks().len(), 0);
703
704        // Test add
705        assert_eq!(compactor_manager.compactor_num(), 0);
706        assert!(compactor_manager.get_compactor(context_id).is_none());
707        compactor_manager.add_compactor(context_id);
708        assert_eq!(compactor_manager.compactor_num(), 1);
709        assert_eq!(
710            compactor_manager
711                .get_compactor(context_id)
712                .unwrap()
713                .context_id(),
714            context_id
715        );
716        // Test remove
717        compactor_manager.remove_compactor(context_id);
718        assert_eq!(compactor_manager.compactor_num(), 0);
719        assert!(compactor_manager.get_compactor(context_id).is_none());
720    }
721
722    #[test]
723    fn test_iceberg_compactor_manager() {
724        // Test Add and Remove Iceberg Compactor
725        let iceberg_context_id = 1000.into();
726        let iceberg_compactor_manager = IcebergCompactorManager::new();
727        assert_eq!(iceberg_compactor_manager.compactor_num(), 0);
728        assert!(
729            iceberg_compactor_manager
730                .get_compactor(iceberg_context_id)
731                .is_none()
732        );
733        let mut receiver = iceberg_compactor_manager.add_compactor(iceberg_context_id);
734        assert_eq!(iceberg_compactor_manager.compactor_num(), 1);
735        let compactor = iceberg_compactor_manager
736            .get_compactor(iceberg_context_id)
737            .unwrap();
738        assert_eq!(compactor.context_id(), iceberg_context_id);
739
740        compactor.cancel_task(42.into()).unwrap();
741        let event = receiver.try_recv().unwrap().unwrap().event.unwrap();
742        match event {
743            IcebergResponseEvent::CancelCompactTask(cancel_task) => {
744                assert_eq!(cancel_task.task_id, 42);
745            }
746            other => panic!("unexpected iceberg compactor event: {other:?}"),
747        }
748
749        // Test remove
750        iceberg_compactor_manager.remove_compactor(iceberg_context_id);
751        assert_eq!(iceberg_compactor_manager.compactor_num(), 0);
752        assert!(
753            iceberg_compactor_manager
754                .get_compactor(iceberg_context_id)
755                .is_none()
756        );
757    }
758}