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};
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        // Always use the smallest region id on datanode as the target region id for task tracker
57        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        // Group regions by table_id
62        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                        // Merge reports
106                        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    /// Create a GC worker for the given table and region IDs.
143    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, &region_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    /// Get the access layer for the given table and region IDs.
183    /// It also returns the mito regions if they are found in the engine.
184    ///
185    /// This method validates:
186    /// 1. Any found region must be a Leader (not Follower)
187    /// 2. Any missing region must not be routed to another datanode
188    ///
189    /// The AccessLayer is always constructed from table metadata for consistency.
190    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        // 1. Collect mito regions and validate Leader status
197        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                // Validation: Check if region is a leader
204                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        // 2. Validate that missing regions are not routed to other datanodes
218        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        // 3. Construct AccessLayer directly from table metadata
229        let access_layer = Self::construct_access_layer(ctx, mito_engine, table_id).await?;
230
231        Ok((access_layer, mito_regions))
232    }
233
234    /// Manually construct an access layer from table metadata.
235    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    /// Validate that the given regions are not routed to other datanodes.
289    ///
290    /// If any region is still active on another datanode (has a leader_peer in route table),
291    /// this function returns an error to prevent accidental deletion of files
292    /// that are still in use.
293    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        // Get table route
305        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                // Table route not found, all regions are likely deleted
314                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        // Get region routes for physical table
323        let region_routes = match table_route.region_routes() {
324            Ok(routes) => routes,
325            Err(_) => {
326                // Logical table, skip validation
327                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        // Check each missing region
341        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                    // Region still has a leader on some datanode.
345                    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            // Region not in route table: treat as deleted and allow GC.
364        }
365
366        Ok(())
367    }
368}