Skip to main content

mito2/worker/
handle_drop.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
15//! Handling drop request.
16
17use std::time::Duration;
18
19use bytes::Bytes;
20use common_telemetry::{error, info, warn};
21use futures::TryStreamExt;
22use object_store::util::join_path;
23use object_store::{EntryMode, ObjectStore};
24use snafu::ResultExt;
25use store_api::logstore::LogStore;
26use store_api::metadata::RegionMetadataRef;
27use store_api::region_request::{AffectedRows, PathType, RegionDropRequest};
28use store_api::storage::RegionId;
29use tokio::time::sleep;
30
31use crate::cache::CacheManagerRef;
32use crate::engine::region_hook::RegionHookRef;
33use crate::error::{OpenDalSnafu, Result};
34use crate::region::{MitoRegionRef, RegionLeaderState, RegionMapRef};
35use crate::request::{DdlRequest, OptionOutputTx};
36use crate::sst::index::intermediate::IntermediateManager;
37use crate::worker::{DROPPING_MARKER_FILE, RegionWorkerLoop};
38
39const GC_TASK_INTERVAL_SEC: u64 = 5 * 60; // 5 minutes
40const MAX_RETRY_TIMES: u64 = 12; // 1 hours (5m * 12)
41
42impl<S> RegionWorkerLoop<S>
43where
44    S: LogStore,
45{
46    pub(crate) async fn handle_drop_request(
47        &mut self,
48        region_id: RegionId,
49        req: RegionDropRequest,
50        sender: OptionOutputTx,
51    ) {
52        let region = match self.regions.writable_region(region_id) {
53            Ok(region) => region,
54            Err(e) => {
55                sender.send(Err(e));
56                return;
57            }
58        };
59
60        let (sender, req) = match self.flush_scheduler.try_cancel_and_add_ddl(
61            region_id,
62            sender,
63            req,
64            DdlRequest::Drop,
65        ) {
66            Ok(()) => {
67                self.listener.on_flush_cancel_requested(region_id);
68                return;
69            }
70            Err(request) => request,
71        };
72
73        let result = self.drop_region(region, req.partial_drop).await;
74        sender.send(result);
75    }
76
77    async fn drop_region(
78        &mut self,
79        region: MitoRegionRef,
80        partial_drop: bool,
81    ) -> Result<AffectedRows> {
82        let region_id = region.region_id;
83
84        info!("Try to drop region: {}, worker: {}", region_id, self.id);
85
86        let is_staging = region.is_staging();
87        let expect_state = if is_staging {
88            RegionLeaderState::Staging
89        } else {
90            RegionLeaderState::Writable
91        };
92        // Marks the region as dropping.
93        region.set_dropping(expect_state)?;
94        // Writes dropping marker
95        // We rarely drop a region so we still operate in the worker loop.
96        let region_dir = region.access_layer.build_region_dir(region_id);
97        let path_type = region.access_layer.path_type();
98        let table_dir = region.access_layer.table_dir().to_string();
99        let marker_path = join_path(&region_dir, DROPPING_MARKER_FILE);
100        region
101            .access_layer
102            .object_store()
103            .write(&marker_path, Bytes::new())
104            .await
105            .context(OpenDalSnafu)
106            .inspect_err(|e| {
107                error!(e; "Failed to write the drop marker file for region {}", region_id);
108
109                // Sets the state back to writable. It's possible that the marker file has been written.
110                // We set the state back to writable so we can retry the drop operation.
111                region.switch_state_to_writable(RegionLeaderState::Dropping);
112            })?;
113
114        region.stop().await;
115        // Removes this region from region map to prevent other requests from accessing this region
116        self.regions.remove_region(region_id);
117        self.dropping_regions.insert_region(region.clone());
118
119        // Delete region data in WAL.
120        self.wal
121            .obsolete(
122                region_id,
123                region.version_control.current().last_entry_id,
124                &region.provider,
125            )
126            .await?;
127        self.cleanup_dropped_region_runtime_state(region_id).await;
128
129        // Marks region version as dropped
130        region.version_control.mark_dropped();
131        info!(
132            "Region {} is dropped logically, but some files are not deleted yet",
133            region_id
134        );
135
136        self.region_count.dec();
137
138        // Notify registered hooks that the region has been logically dropped, and
139        // prepare a payload for the background GC task to fire the terminal
140        // `on_region_files_removed`. When no hook is registered this allocates
141        // nothing — no metadata snapshot is taken and no payload is built.
142        let hook_payload = match region.manifest_ctx.hook() {
143            Some(hook) => {
144                let region_metadata = region.metadata();
145                hook.on_region_dropped(region_id, &region_metadata).await;
146                Some(DropHookPayload {
147                    hook,
148                    metadata: region_metadata,
149                })
150            }
151            None => None,
152        };
153
154        let object_store = region.access_layer.object_store().clone();
155        let dropping_regions = self.dropping_regions.clone();
156        let listener = self.listener.clone();
157        let intm_manager = self.intermediate_manager.clone();
158        let cache_manager = self.cache_manager.clone();
159        let gc_enabled = self.file_ref_manager.is_gc_enabled();
160
161        common_runtime::spawn_global(async move {
162            let removed = if gc_enabled {
163                later_drop_task_with_global_gc(
164                    region_id,
165                    region_dir.clone(),
166                    path_type,
167                    object_store,
168                    dropping_regions,
169                    partial_drop,
170                    hook_payload.as_ref(),
171                )
172                .await
173            } else {
174                let gc_duration = listener
175                    .on_later_drop_begin(region_id)
176                    .unwrap_or(Duration::from_secs(GC_TASK_INTERVAL_SEC));
177
178                later_drop_task_without_global_gc(
179                    region_id,
180                    region_dir.clone(),
181                    object_store,
182                    dropping_regions,
183                    gc_duration,
184                    hook_payload.as_ref(),
185                )
186                .await
187            };
188
189            cleanup_region_file_artifacts(region_id, &table_dir, &intm_manager, &cache_manager)
190                .await;
191
192            listener.on_later_drop_end(region_id, removed);
193        });
194
195        Ok(0)
196    }
197
198    /// Cleans runtime state for a region that is no longer available to serve requests.
199    pub(crate) async fn cleanup_dropped_region_runtime_state(&mut self, region_id: RegionId) {
200        // Notifies flush scheduler.
201        self.flush_scheduler.on_region_dropped(region_id);
202        // Notifies compaction scheduler.
203        self.compaction_scheduler.on_region_dropped(region_id);
204        // Notifies index build scheduler.
205        self.index_build_scheduler
206            .on_region_dropped(region_id)
207            .await;
208    }
209}
210
211/// Cleans files and caches that are produced at runtime but are not part of the
212/// primary region directory deletion.
213pub(crate) async fn cleanup_region_file_artifacts(
214    region_id: RegionId,
215    table_dir: &str,
216    intermediate_manager: &IntermediateManager,
217    cache_manager: &CacheManagerRef,
218) {
219    if let Err(err) = intermediate_manager.prune_region_dir(&region_id).await {
220        warn!(err; "Failed to prune intermediate region directory, region_id: {}", region_id);
221    }
222
223    if let Some(write_cache) = cache_manager.write_cache()
224        && let Some(manifest_cache) = write_cache.manifest_cache()
225    {
226        manifest_cache.clean_manifests(table_dir).await;
227    }
228}
229
230/// Removes a region directory for full-drop style cleanup.
231///
232/// Full drop and purge/offline cleanup force physical deletion. Only partial
233/// drop may leave data files for global GC.
234pub(crate) async fn remove_region_dir_for_full_drop(
235    region_path: &str,
236    object_store: &ObjectStore,
237) -> Result<()> {
238    remove_region_dir_once(region_path, object_store, true).await?;
239    Ok(())
240}
241
242/// Carries the region hook and region metadata into the background GC task so
243/// it can fire [`RegionHook::on_region_files_removed`] once the dropped region's
244/// directory is physically deleted.
245///
246/// Only constructed when a hook is registered; the task receives it as
247/// `Option<&DropHookPayload>` so the no-hook path allocates nothing.
248///
249/// [`RegionHook::on_region_files_removed`]: crate::engine::region_hook::RegionHook::on_region_files_removed
250struct DropHookPayload {
251    hook: RegionHookRef,
252    metadata: RegionMetadataRef,
253}
254
255/// Background GC task to remove the entire region path once one of the following
256/// conditions is true:
257/// - It finds there is no parquet file left.
258/// - After `gc_duration`.
259///
260/// Returns whether the path is removed.
261///
262/// This task will retry on failure and keep running until finished. Any resource
263/// captured by it will not be released before then. Be sure to only pass weak reference
264/// if something is depended on ref-count mechanism.
265async fn later_drop_task_without_global_gc(
266    region_id: RegionId,
267    region_path: String,
268    object_store: ObjectStore,
269    dropping_regions: RegionMapRef,
270    gc_duration: Duration,
271    hook_payload: Option<&DropHookPayload>,
272) -> bool {
273    remove_region_with_retry(
274        region_id,
275        region_path,
276        object_store,
277        dropping_regions,
278        Some(gc_duration),
279        false,
280        hook_payload,
281    )
282    .await
283}
284
285async fn remove_region_with_retry(
286    region_id: RegionId,
287    region_path: String,
288    object_store: ObjectStore,
289    dropping_regions: std::sync::Arc<crate::region::RegionMap>,
290    gc_duration: Option<Duration>,
291    mut force: bool,
292    hook_payload: Option<&DropHookPayload>,
293) -> bool {
294    for _ in 0..MAX_RETRY_TIMES {
295        let result = remove_region_dir_once(&region_path, &object_store, force).await;
296        match result {
297            Err(err) => {
298                warn!(
299                    "Error occurs during trying to GC region dir {}: {}",
300                    region_path, err
301                );
302            }
303            Ok(true) => {
304                dropping_regions.remove_region(region_id);
305                info!("Region {} is dropped, force: {}", region_path, force);
306                // The region directory has been physically deleted; fire the
307                // terminal file-lifecycle event. The partial-drop/global-GC path
308                // never reaches here (it does not delete the directory itself).
309                if let Some(hook_payload) = hook_payload {
310                    hook_payload
311                        .hook
312                        .on_region_files_removed(region_id, &hook_payload.metadata)
313                        .await;
314                }
315                return true;
316            }
317            Ok(false) => (),
318        }
319        if let Some(duration) = gc_duration {
320            sleep(duration).await;
321        }
322        // Force recycle after gc duration.
323        force = true;
324    }
325
326    warn!(
327        "Failed to GC region dir {} after {} retries, giving up",
328        region_path, MAX_RETRY_TIMES
329    );
330
331    false
332}
333
334async fn later_drop_task_with_global_gc(
335    region_id: RegionId,
336    region_path: String,
337    path_type: PathType,
338    object_store: ObjectStore,
339    dropping_regions: RegionMapRef,
340    partial_drop: bool,
341    hook_payload: Option<&DropHookPayload>,
342) -> bool {
343    // For metadata regions or regions marked for full deletion (such as when dropping a table)
344    // the region directory is forcefully removed immediately.
345    //
346    // TODO(discord9): Evaluate removing files instantly rather than waiting for the GC period.
347    if should_force_remove_region_dir(path_type, partial_drop) {
348        remove_region_with_retry(
349            region_id,
350            region_path,
351            object_store,
352            dropping_regions,
353            None,
354            true,
355            hook_payload,
356        )
357        .await
358    } else {
359        // left for global gc
360        dropping_regions.remove_region(region_id);
361        true
362    }
363}
364
365fn should_force_remove_region_dir(path_type: PathType, partial_drop: bool) -> bool {
366    path_type == PathType::Metadata || !partial_drop
367}
368
369// TODO(ruihang): place the marker in a separate dir
370/// Removes region dir if there is no parquet files, returns whether the directory is removed.
371/// If `force = true`, always removes the dir.
372pub(crate) async fn remove_region_dir_once(
373    region_path: &str,
374    object_store: &ObjectStore,
375    force: bool,
376) -> Result<bool> {
377    // list all files under the given region path to check if there are un-deleted parquet files
378    let mut has_parquet_file = false;
379    // record all paths that neither ends with .parquet nor the marker file
380    let mut files_to_remove_first = vec![];
381    let mut files = object_store
382        .lister_with(region_path)
383        .await
384        .context(OpenDalSnafu)?;
385    while let Some(file) = files.try_next().await.context(OpenDalSnafu)? {
386        if !force && file.path().ends_with(".parquet") {
387            // If not in force mode, we only remove the region dir if there is no parquet file
388            has_parquet_file = true;
389            break;
390        } else if !file.path().ends_with(DROPPING_MARKER_FILE) {
391            let meta = file.metadata();
392            if meta.mode() == EntryMode::FILE {
393                files_to_remove_first.push(file.path().to_string());
394            }
395        }
396    }
397
398    if !has_parquet_file {
399        // no parquet file found, delete the region path
400        // first delete all files other than the marker
401        object_store
402            .delete_iter(files_to_remove_first)
403            .await
404            .context(OpenDalSnafu)?;
405        // then remove the marker with this dir
406        object_store
407            .delete_with(region_path)
408            .recursive(true)
409            .await
410            .context(OpenDalSnafu)?;
411        Ok(true)
412    } else {
413        Ok(false)
414    }
415}