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