Skip to main content

meta_srv/gc/
ctx.rs

1// Copyright 2023 Greptime Team
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;
16#[cfg(feature = "enterprise")]
17use std::collections::HashSet;
18#[cfg(feature = "enterprise")]
19use std::sync::atomic::{AtomicUsize, Ordering};
20#[cfg(feature = "enterprise")]
21use std::sync::{Arc, Mutex};
22use std::time::Duration;
23
24use common_meta::datanode::RegionStat;
25#[cfg(feature = "enterprise")]
26use common_meta::ddl_manager::DdlManagerRef;
27#[cfg(feature = "enterprise")]
28use common_meta::key::DroppedTableName;
29use common_meta::key::TableMetadataManagerRef;
30use common_meta::key::table_repart::TableRepartValue;
31use common_meta::key::table_route::PhysicalTableRouteValue;
32#[cfg(feature = "enterprise")]
33use common_meta::rpc::ddl::PurgeDroppedTableTask;
34use common_procedure::{ProcedureContext, ProcedureManagerRef, ProcedureWithId, watcher};
35use common_telemetry::debug;
36use snafu::{OptionExt as _, ResultExt as _};
37use store_api::storage::{GcReport, RegionId};
38use table::metadata::TableId;
39
40use crate::cluster::MetaPeerClientRef;
41use crate::error::{self, Result, TableMetadataManagerSnafu};
42use crate::gc::Region2Peers;
43use crate::gc::procedure::BatchGcProcedure;
44#[cfg(feature = "enterprise")]
45use crate::metrics::METRIC_META_GC_SOFT_DROP_PURGES_TOTAL;
46use crate::service::mailbox::MailboxRef;
47
48#[async_trait::async_trait]
49pub(crate) trait SchedulerCtx: Send + Sync {
50    async fn get_table_to_region_stats(&self) -> Result<HashMap<TableId, Vec<RegionStat>>>;
51
52    async fn get_table_reparts(&self) -> Result<Vec<(TableId, TableRepartValue)>>;
53
54    async fn get_table_route(
55        &self,
56        table_id: TableId,
57    ) -> Result<(TableId, PhysicalTableRouteValue)>;
58
59    async fn batch_get_table_route(
60        &self,
61        table_ids: &[TableId],
62    ) -> Result<HashMap<TableId, PhysicalTableRouteValue>>;
63
64    async fn gc_regions(
65        &self,
66        region_ids: &[RegionId],
67        full_file_listing: bool,
68        timeout: Duration,
69        region_routes_override: Region2Peers,
70        procedure_context: ProcedureContext,
71    ) -> Result<GcReport>;
72
73    #[cfg(feature = "enterprise")]
74    async fn list_dropped_tables(&self) -> Result<Vec<DroppedTableName>>;
75
76    #[cfg(feature = "enterprise")]
77    async fn purge_dropped_table(&self, table_id: TableId) -> Result<()>;
78
79    #[cfg(feature = "enterprise")]
80    fn try_reserve_purge(
81        &self,
82        table_id: TableId,
83        max_in_flight: usize,
84    ) -> Option<PurgeReservation>;
85
86    #[cfg(feature = "enterprise")]
87    fn next_purge_scan_start(&self, _table_count: usize) -> usize {
88        0
89    }
90}
91
92#[cfg(feature = "enterprise")]
93pub(crate) enum PurgeOutcome {
94    Succeeded,
95    Failed,
96}
97
98#[cfg(feature = "enterprise")]
99pub(crate) struct PurgeReservation {
100    table_id: TableId,
101    in_flight: Arc<Mutex<HashSet<TableId>>>,
102    outcome_recorded: bool,
103}
104
105#[cfg(feature = "enterprise")]
106impl PurgeReservation {
107    pub(crate) fn try_new(
108        in_flight: Arc<Mutex<HashSet<TableId>>>,
109        table_id: TableId,
110        max_in_flight: usize,
111    ) -> Option<Self> {
112        let mut tables = in_flight
113            .lock()
114            .unwrap_or_else(|poisoned| poisoned.into_inner());
115        if tables.len() >= max_in_flight || !tables.insert(table_id) {
116            return None;
117        }
118        drop(tables);
119        Some(Self {
120            table_id,
121            in_flight,
122            outcome_recorded: false,
123        })
124    }
125
126    pub(crate) fn record_outcome(mut self, outcome: PurgeOutcome) {
127        let status = match outcome {
128            PurgeOutcome::Succeeded => "succeeded",
129            PurgeOutcome::Failed => "failed",
130        };
131        METRIC_META_GC_SOFT_DROP_PURGES_TOTAL
132            .with_label_values(&[status])
133            .inc();
134        self.outcome_recorded = true;
135    }
136}
137
138#[cfg(feature = "enterprise")]
139impl Drop for PurgeReservation {
140    fn drop(&mut self) {
141        if !self.outcome_recorded {
142            METRIC_META_GC_SOFT_DROP_PURGES_TOTAL
143                .with_label_values(&["cancelled"])
144                .inc();
145        }
146        self.in_flight
147            .lock()
148            .unwrap_or_else(|poisoned| poisoned.into_inner())
149            .remove(&self.table_id);
150    }
151}
152
153pub(crate) struct DefaultGcSchedulerCtx {
154    /// The metadata manager.
155    pub(crate) table_metadata_manager: TableMetadataManagerRef,
156    /// Procedure manager.
157    pub(crate) procedure_manager: ProcedureManagerRef,
158    /// DDL manager used to submit the existing purge procedure.
159    #[cfg(feature = "enterprise")]
160    pub(crate) ddl_manager: DdlManagerRef,
161    /// Process-local reservations for purge procedures submitted by this scheduler.
162    /// Procedure recovery after a metasrv restart may outlive this set.
163    #[cfg(feature = "enterprise")]
164    in_flight_purges: Arc<Mutex<HashSet<TableId>>>,
165    #[cfg(feature = "enterprise")]
166    purge_scan_cursor: AtomicUsize,
167    /// For getting `RegionStats`.
168    pub(crate) meta_peer_client: MetaPeerClientRef,
169    /// The mailbox to send messages.
170    pub(crate) mailbox: MailboxRef,
171    /// The server address.
172    pub(crate) server_addr: String,
173}
174
175impl DefaultGcSchedulerCtx {
176    pub fn try_new(
177        table_metadata_manager: TableMetadataManagerRef,
178        procedure_manager: ProcedureManagerRef,
179        #[cfg(feature = "enterprise")] ddl_manager: DdlManagerRef,
180        meta_peer_client: MetaPeerClientRef,
181        mailbox: MailboxRef,
182        server_addr: String,
183    ) -> Result<Self> {
184        Ok(Self {
185            table_metadata_manager,
186            procedure_manager,
187            #[cfg(feature = "enterprise")]
188            ddl_manager,
189            #[cfg(feature = "enterprise")]
190            in_flight_purges: Arc::new(Mutex::new(HashSet::new())),
191            #[cfg(feature = "enterprise")]
192            purge_scan_cursor: AtomicUsize::new(0),
193            meta_peer_client,
194            mailbox,
195            server_addr,
196        })
197    }
198}
199
200#[async_trait::async_trait]
201impl SchedulerCtx for DefaultGcSchedulerCtx {
202    async fn get_table_to_region_stats(&self) -> Result<HashMap<TableId, Vec<RegionStat>>> {
203        let dn_stats = self.meta_peer_client.get_all_dn_stat_kvs().await?;
204        let mut table_to_region_stats: HashMap<TableId, Vec<RegionStat>> = HashMap::new();
205        for (_dn_id, stats) in dn_stats {
206            let stats = stats.stats;
207
208            let Some(latest_stat) = stats.iter().max_by_key(|s| s.timestamp_millis).cloned() else {
209                continue;
210            };
211
212            for region_stat in latest_stat.region_stats {
213                table_to_region_stats
214                    .entry(region_stat.id.table_id())
215                    .or_default()
216                    .push(region_stat);
217            }
218        }
219        Ok(table_to_region_stats)
220    }
221
222    async fn get_table_reparts(&self) -> Result<Vec<(TableId, TableRepartValue)>> {
223        self.table_metadata_manager
224            .table_repart_manager()
225            .table_reparts()
226            .await
227            .context(TableMetadataManagerSnafu)
228    }
229
230    async fn get_table_route(
231        &self,
232        table_id: TableId,
233    ) -> Result<(TableId, PhysicalTableRouteValue)> {
234        self.table_metadata_manager
235            .table_route_manager()
236            .get_physical_table_route(table_id)
237            .await
238            .context(TableMetadataManagerSnafu)
239    }
240
241    async fn batch_get_table_route(
242        &self,
243        table_ids: &[TableId],
244    ) -> Result<HashMap<TableId, PhysicalTableRouteValue>> {
245        self.table_metadata_manager
246            .table_route_manager()
247            .batch_get_physical_table_routes(table_ids)
248            .await
249            .context(TableMetadataManagerSnafu)
250    }
251
252    async fn gc_regions(
253        &self,
254        region_ids: &[RegionId],
255        full_file_listing: bool,
256        timeout: Duration,
257        region_routes_override: Region2Peers,
258        procedure_context: ProcedureContext,
259    ) -> Result<GcReport> {
260        self.gc_regions_inner(
261            region_ids,
262            full_file_listing,
263            timeout,
264            region_routes_override,
265            procedure_context,
266        )
267        .await
268    }
269
270    #[cfg(feature = "enterprise")]
271    async fn list_dropped_tables(&self) -> Result<Vec<DroppedTableName>> {
272        self.table_metadata_manager
273            .list_dropped_tables()
274            .await
275            .context(TableMetadataManagerSnafu)
276    }
277
278    #[cfg(feature = "enterprise")]
279    async fn purge_dropped_table(&self, table_id: TableId) -> Result<()> {
280        self.ddl_manager
281            .submit_expired_purge_dropped_table_task(PurgeDroppedTableTask { table_id })
282            .await
283            .context(error::SubmitDdlTaskSnafu)?;
284        Ok(())
285    }
286
287    #[cfg(feature = "enterprise")]
288    fn try_reserve_purge(
289        &self,
290        table_id: TableId,
291        max_in_flight: usize,
292    ) -> Option<PurgeReservation> {
293        PurgeReservation::try_new(self.in_flight_purges.clone(), table_id, max_in_flight)
294    }
295
296    #[cfg(feature = "enterprise")]
297    fn next_purge_scan_start(&self, table_count: usize) -> usize {
298        if table_count == 0 {
299            return 0;
300        }
301        self.purge_scan_cursor.fetch_add(1, Ordering::Relaxed) % table_count
302    }
303}
304
305impl DefaultGcSchedulerCtx {
306    async fn gc_regions_inner(
307        &self,
308        region_ids: &[RegionId],
309        full_file_listing: bool,
310        timeout: Duration,
311        region_routes_override: Region2Peers,
312        procedure_context: ProcedureContext,
313    ) -> Result<GcReport> {
314        debug!(
315            "Sending GC instruction for {} regions (full_file_listing: {})",
316            region_ids.len(),
317            full_file_listing
318        );
319
320        let procedure = BatchGcProcedure::new(
321            self.mailbox.clone(),
322            self.table_metadata_manager.clone(),
323            self.server_addr.clone(),
324            region_ids.to_vec(),
325            full_file_listing,
326            timeout,
327            region_routes_override,
328        );
329        let procedure_with_id =
330            ProcedureWithId::with_random_id(Box::new(procedure)).with_context(procedure_context);
331
332        let id = procedure_with_id.id;
333
334        let mut watcher = self
335            .procedure_manager
336            .submit(procedure_with_id)
337            .await
338            .context(error::SubmitProcedureSnafu)?;
339        let res = watcher::wait(&mut watcher)
340            .await
341            .context(error::WaitProcedureSnafu)?
342            .with_context(|| error::UnexpectedSnafu {
343                violated: format!(
344                    "GC procedure {id} successfully completed but no result returned"
345                ),
346            })?;
347
348        let gc_report = BatchGcProcedure::cast_result(res)?;
349
350        Ok(gc_report)
351    }
352}
353
354#[cfg(all(test, feature = "enterprise"))]
355mod tests {
356    use std::panic::{AssertUnwindSafe, catch_unwind};
357
358    use super::*;
359    use crate::metrics::METRIC_META_GC_SOFT_DROP_PURGES_TOTAL;
360
361    #[test]
362    fn test_purge_reservation_releases_slot_on_panic() {
363        let in_flight = Arc::new(std::sync::Mutex::new(HashSet::new()));
364        let cancelled = METRIC_META_GC_SOFT_DROP_PURGES_TOTAL.with_label_values(&["cancelled"]);
365        let before = cancelled.get();
366        let reservation = PurgeReservation::try_new(in_flight.clone(), 1, 1).unwrap();
367
368        let result = catch_unwind(AssertUnwindSafe(|| {
369            let _reservation = reservation;
370            panic!("mock purge panic");
371        }));
372
373        assert!(result.is_err());
374        assert!(PurgeReservation::try_new(in_flight, 2, 1).is_some());
375        assert!(cancelled.get() > before);
376    }
377
378    #[tokio::test]
379    async fn test_purge_reservation_releases_slot_on_task_abort() {
380        let in_flight = Arc::new(std::sync::Mutex::new(HashSet::new()));
381        let cancelled = METRIC_META_GC_SOFT_DROP_PURGES_TOTAL.with_label_values(&["cancelled"]);
382        let before = cancelled.get();
383        let reservation = PurgeReservation::try_new(in_flight.clone(), 1, 1).unwrap();
384        let handle = tokio::spawn(async move {
385            let _reservation = reservation;
386            std::future::pending::<()>().await;
387        });
388        tokio::task::yield_now().await;
389
390        handle.abort();
391        let error = handle.await.unwrap_err();
392
393        assert!(error.is_cancelled());
394        assert!(PurgeReservation::try_new(in_flight, 2, 1).is_some());
395        assert!(cancelled.get() > before);
396    }
397}