Skip to main content

datanode/heartbeat/handler/
gc_worker.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::{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        // Always use the smallest region id on datanode as the target region id for task tracker
58        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        // Group regions by table_id
63        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                        // Merge reports
107                        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    /// Create a GC worker for the given table and region IDs.
174    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, &region_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    /// Get the access layer for the given table and region IDs.
214    /// It also returns the mito regions if they are found in the engine.
215    ///
216    /// This method validates:
217    /// 1. Any found region must be a Leader (not Follower)
218    /// 2. Any missing region must not be routed to another datanode
219    ///
220    /// The AccessLayer is always constructed from table metadata for consistency.
221    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        // 1. Collect mito regions and validate Leader status
228        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                // Validation: Check if region is a leader
235                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        // 2. Validate that missing regions are not routed to other datanodes
249        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        // 3. Construct AccessLayer directly from table metadata
260        let access_layer = Self::construct_access_layer(ctx, mito_engine, table_id).await?;
261
262        Ok((access_layer, mito_regions))
263    }
264
265    /// Manually construct an access layer from table metadata.
266    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    /// Validate that the given regions are not routed to other datanodes.
320    ///
321    /// If any region is still active on another datanode (has a leader_peer in route table),
322    /// this function returns an error to prevent accidental deletion of files
323    /// that are still in use.
324    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        // Get table route
336        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                // Table route not found, all regions are likely deleted
345                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        // Get region routes for physical table
354        let region_routes = match table_route.region_routes() {
355            Ok(routes) => routes,
356            Err(_) => {
357                // Logical table, skip validation
358                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        // Check each missing region
372        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                    // Region still has a leader on some datanode.
376                    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            // Region not in route table: treat as deleted and allow GC.
395        }
396
397        Ok(())
398    }
399}