1use 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 pub(crate) table_metadata_manager: TableMetadataManagerRef,
156 pub(crate) procedure_manager: ProcedureManagerRef,
158 #[cfg(feature = "enterprise")]
160 pub(crate) ddl_manager: DdlManagerRef,
161 #[cfg(feature = "enterprise")]
164 in_flight_purges: Arc<Mutex<HashSet<TableId>>>,
165 #[cfg(feature = "enterprise")]
166 purge_scan_cursor: AtomicUsize,
167 pub(crate) meta_peer_client: MetaPeerClientRef,
169 pub(crate) mailbox: MailboxRef,
171 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}