datanode/heartbeat/handler/
gc_worker.rs1use std::collections::{BTreeMap, HashMap};
16use std::sync::Arc;
17
18use common_meta::instruction::{GcRegions, GcRegionsReply, InstructionReply};
19use common_meta::key::table_info::TableInfoManager;
20use common_meta::key::table_route::TableRouteManager;
21use common_telemetry::{debug, warn};
22use mito2::access_layer::{AccessLayer, AccessLayerRef};
23use mito2::engine::MitoEngine;
24use mito2::gc::LocalGcWorker;
25use mito2::region::MitoRegionRef;
26use snafu::{OptionExt, ResultExt};
27use store_api::path_utils::table_dir;
28use store_api::region_request::PathType;
29use store_api::storage::{FileRefsManifest, GcReport, RegionId};
30use table::requests::STORAGE_KEY;
31use tracing::Instrument;
32
33use crate::error::{GcMitoEngineSnafu, GetMetadataSnafu, Result, UnexpectedSnafu};
34use crate::heartbeat::handler::{HandlerContext, InstructionHandler};
35
36pub struct GcRegionsHandler;
37
38#[async_trait::async_trait]
39impl InstructionHandler for GcRegionsHandler {
40 type Instruction = GcRegions;
41
42 async fn handle(
43 &self,
44 ctx: &HandlerContext,
45 gc_regions: Self::Instruction,
46 ) -> Option<InstructionReply> {
47 let region_ids = gc_regions.regions.clone();
48 debug!("Received gc regions instruction: {:?}", region_ids);
49
50 if region_ids.is_empty() {
51 return Some(InstructionReply::GcRegions(GcRegionsReply {
52 result: Ok(GcReport::default()),
53 }));
54 }
55
56 let mut sorted_region_ids = gc_regions.regions.clone();
58 sorted_region_ids.sort_by_key(|r| r.region_number());
59 let target_region_id = sorted_region_ids[0];
60
61 let mut table_to_regions: HashMap<u32, Vec<RegionId>> = HashMap::new();
63 for rid in region_ids {
64 table_to_regions
65 .entry(rid.table_id())
66 .or_default()
67 .push(rid);
68 }
69
70 let file_refs_manifest = gc_regions.file_refs_manifest.clone();
71 let full_file_listing = gc_regions.full_file_listing;
72
73 let ctx_clone = ctx.clone();
74 let register_result = ctx
75 .gc_tasks
76 .try_register(
77 target_region_id,
78 Box::pin(
79 async move {
80 let mut reports = Vec::with_capacity(table_to_regions.len());
81 for (table_id, regions) in table_to_regions {
82 debug!(
83 "Starting gc worker for table {}, regions: {:?}",
84 table_id, regions
85 );
86 let gc_worker = GcRegionsHandler::create_gc_worker(
87 &ctx_clone,
88 table_id,
89 regions,
90 &file_refs_manifest,
91 full_file_listing,
92 )
93 .await?;
94
95 let report = gc_worker.run().await.context(GcMitoEngineSnafu {
96 region_id: target_region_id,
97 })?;
98 debug!(
99 "Gc worker for table {} finished, report: {:?}",
100 table_id, report
101 );
102 reports.push(report);
103 }
104
105 let mut merged_report = GcReport::default();
107 for report in reports {
108 merged_report.merge(report);
109 }
110
111 Ok(merged_report)
112 }
113 .instrument(common_telemetry::tracing::info_span!("gc_worker_run")),
114 ),
115 )
116 .await;
117
118 if register_result.is_busy() {
119 warn!("Another gc task is running for the region: {target_region_id}");
120 return Some(InstructionReply::GcRegions(GcRegionsReply {
121 result: Err(
122 common_meta::instruction::InstructionError::legacy_internal_retryable(format!(
123 "Another gc task is running for the region: {target_region_id}"
124 )),
125 ),
126 }));
127 }
128 let mut watcher = register_result.into_watcher();
129 let result = ctx.gc_tasks.wait_until_finish(&mut watcher).await;
130 match result {
131 Ok(report) => Some(InstructionReply::GcRegions(GcRegionsReply {
132 result: Ok(report),
133 })),
134 Err(err) => Some(InstructionReply::GcRegions(GcRegionsReply {
135 result: Err(common_meta::instruction::InstructionError::from_error(&err)),
136 })),
137 }
138 }
139}
140
141impl GcRegionsHandler {
142 async fn create_gc_worker(
144 ctx: &HandlerContext,
145 table_id: u32,
146 region_ids: Vec<RegionId>,
147 file_ref_manifest: &FileRefsManifest,
148 full_file_listing: bool,
149 ) -> Result<LocalGcWorker> {
150 debug_assert!(!region_ids.is_empty(), "region_ids should not be empty");
151
152 let mito_engine = ctx
153 .region_server
154 .mito_engine()
155 .with_context(|| UnexpectedSnafu {
156 violated: "MitoEngine not found".to_string(),
157 })?;
158
159 let (access_layer, mito_regions) =
160 Self::get_access_layer(ctx, &mito_engine, table_id, ®ion_ids).await?;
161
162 let cache_manager = mito_engine.cache_manager();
163
164 let gc_worker = LocalGcWorker::try_new(
165 access_layer,
166 Some(cache_manager),
167 mito_regions,
168 mito_engine.mito_config().gc.clone(),
169 file_ref_manifest.clone(),
170 &mito_engine.gc_limiter(),
171 full_file_listing,
172 mito_engine.region_hook(),
173 )
174 .await
175 .context(GcMitoEngineSnafu {
176 region_id: region_ids[0],
177 })?;
178
179 Ok(gc_worker)
180 }
181
182 async fn get_access_layer(
191 ctx: &HandlerContext,
192 mito_engine: &MitoEngine,
193 table_id: u32,
194 region_ids: &[RegionId],
195 ) -> Result<(AccessLayerRef, BTreeMap<RegionId, Option<MitoRegionRef>>)> {
196 let mut mito_regions = BTreeMap::new();
198
199 for rid in region_ids {
200 let region = mito_engine.find_region(*rid);
201
202 if let Some(ref r) = region {
203 if r.is_follower() {
205 return Err(UnexpectedSnafu {
206 violated: format!(
207 "Region {} is a follower, cannot perform GC on follower regions",
208 rid
209 ),
210 }
211 .build());
212 }
213 }
214 mito_regions.insert(*rid, region);
215 }
216
217 let missing_regions: Vec<_> = mito_regions
219 .iter()
220 .filter(|(_, r)| r.is_none())
221 .map(|(rid, _)| *rid)
222 .collect();
223
224 if !missing_regions.is_empty() {
225 Self::validate_regions_not_routed_elsewhere(ctx, table_id, &missing_regions).await?;
226 }
227
228 let access_layer = Self::construct_access_layer(ctx, mito_engine, table_id).await?;
230
231 Ok((access_layer, mito_regions))
232 }
233
234 async fn construct_access_layer(
236 ctx: &HandlerContext,
237 mito_engine: &MitoEngine,
238 table_id: u32,
239 ) -> Result<AccessLayerRef> {
240 let table_info_manager = TableInfoManager::new(ctx.kv_backend.clone());
241 let table_info_value = table_info_manager
242 .get(table_id)
243 .await
244 .context(GetMetadataSnafu)?
245 .with_context(|| UnexpectedSnafu {
246 violated: format!("Table metadata not found for table {}", table_id),
247 })?;
248
249 let table_dir = table_dir(&table_info_value.region_storage_path(), table_id);
250 let storage_name = table_info_value
251 .table_info
252 .meta
253 .options
254 .extra_options
255 .get(STORAGE_KEY);
256 let engine = &table_info_value.table_info.meta.engine;
257 let path_type = match engine.as_str() {
258 common_catalog::consts::MITO2_ENGINE => PathType::Bare,
259 common_catalog::consts::MITO_ENGINE => PathType::Bare,
260 common_catalog::consts::METRIC_ENGINE => PathType::Data,
261 _ => PathType::Bare,
262 };
263
264 let object_store = if let Some(name) = storage_name {
265 mito_engine
266 .object_store_manager()
267 .find(name)
268 .cloned()
269 .with_context(|| UnexpectedSnafu {
270 violated: format!("Object store {} not found", name),
271 })?
272 } else {
273 mito_engine
274 .object_store_manager()
275 .default_object_store()
276 .clone()
277 };
278
279 Ok(Arc::new(AccessLayer::new(
280 table_dir,
281 path_type,
282 object_store,
283 mito_engine.puffin_manager_factory().clone(),
284 mito_engine.intermediate_manager().clone(),
285 )))
286 }
287
288 async fn validate_regions_not_routed_elsewhere(
294 ctx: &HandlerContext,
295 table_id: u32,
296 missing_region_ids: &[RegionId],
297 ) -> Result<()> {
298 if missing_region_ids.is_empty() {
299 return Ok(());
300 }
301
302 let table_route_manager = TableRouteManager::new(ctx.kv_backend.clone());
303
304 let table_route = match table_route_manager
306 .table_route_storage()
307 .get(table_id)
308 .await
309 .context(GetMetadataSnafu)?
310 {
311 Some(route) => route,
312 None => {
313 debug!(
315 "Table route not found for table {}, regions {:?} are considered deleted",
316 table_id, missing_region_ids
317 );
318 return Ok(());
319 }
320 };
321
322 let region_routes = match table_route.region_routes() {
324 Ok(routes) => routes,
325 Err(_) => {
326 debug!(
328 "Table {} is a logical table, skipping region route validation",
329 table_id
330 );
331 return Ok(());
332 }
333 };
334
335 let region_routes_map: HashMap<RegionId, _> = region_routes
336 .iter()
337 .map(|route| (route.region.id, route))
338 .collect();
339
340 for region_id in missing_region_ids {
342 if let Some(route) = region_routes_map.get(region_id) {
343 if let Some(leader_peer) = &route.leader_peer {
344 return Err(UnexpectedSnafu {
346 violated: format!(
347 "Region {} is not on this datanode but is routed to datanode {}. \
348 GC request may have been sent to wrong datanode.",
349 region_id, leader_peer.id
350 ),
351 }
352 .build());
353 }
354
355 return Err(UnexpectedSnafu {
356 violated: format!(
357 "Region {} has no leader in route table; refusing GC without explicit tombstone/deleted state.",
358 region_id
359 ),
360 }
361 .build());
362 }
363 }
365
366 Ok(())
367 }
368}