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        if let Some(store) = &self.series_index_store {
130            crate::series_index::delete_catalogs(store, region_id).await;
131        }
132
133        // Marks region version as dropped
134        region.version_control.mark_dropped();
135        region.series_index_version_control.mark_dropped();
136        info!(
137            "Region {} is dropped logically, but some files are not deleted yet",
138            region_id
139        );
140
141        self.region_count.dec();
142
143        // Notify registered hooks that the region has been logically dropped, and
144        // prepare a payload for the background GC task to fire the terminal
145        // `on_region_files_removed`. When no hook is registered this allocates
146        // nothing — no metadata snapshot is taken and no payload is built.
147        let hook_payload = match region.manifest_ctx.hook() {
148            Some(hook) => {
149                let region_metadata = region.metadata();
150                hook.on_region_dropped(region_id, &region_metadata).await;
151                Some(DropHookPayload {
152                    hook,
153                    metadata: region_metadata,
154                })
155            }
156            None => None,
157        };
158
159        let object_store = region.access_layer.object_store().clone();
160        let dropping_regions = self.dropping_regions.clone();
161        let listener = self.listener.clone();
162        let intm_manager = self.intermediate_manager.clone();
163        let cache_manager = self.cache_manager.clone();
164        let gc_enabled = self.file_ref_manager.is_gc_enabled();
165
166        common_runtime::spawn_global(async move {
167            let removed = if gc_enabled {
168                later_drop_task_with_global_gc(
169                    region_id,
170                    region_dir.clone(),
171                    path_type,
172                    object_store,
173                    dropping_regions,
174                    partial_drop,
175                    hook_payload.as_ref(),
176                )
177                .await
178            } else {
179                let gc_duration = listener
180                    .on_later_drop_begin(region_id)
181                    .unwrap_or(Duration::from_secs(GC_TASK_INTERVAL_SEC));
182
183                later_drop_task_without_global_gc(
184                    region_id,
185                    region_dir.clone(),
186                    object_store,
187                    dropping_regions,
188                    gc_duration,
189                    hook_payload.as_ref(),
190                )
191                .await
192            };
193
194            cleanup_region_file_artifacts(region_id, &table_dir, &intm_manager, &cache_manager)
195                .await;
196
197            listener.on_later_drop_end(region_id, removed);
198        });
199
200        Ok(0)
201    }
202
203    /// Cleans runtime state for a region that is no longer available to serve requests.
204    pub(crate) async fn cleanup_dropped_region_runtime_state(&mut self, region_id: RegionId) {
205        // Notifies flush scheduler.
206        self.flush_scheduler.on_region_dropped(region_id);
207        // Notifies compaction scheduler.
208        self.compaction_scheduler.on_region_dropped(region_id);
209        // Notifies index build scheduler.
210        self.index_build_scheduler
211            .on_region_dropped(region_id)
212            .await;
213    }
214}
215
216/// Cleans files and caches that are produced at runtime but are not part of the
217/// primary region directory deletion.
218pub(crate) async fn cleanup_region_file_artifacts(
219    region_id: RegionId,
220    table_dir: &str,
221    intermediate_manager: &IntermediateManager,
222    cache_manager: &CacheManagerRef,
223) {
224    if let Err(err) = intermediate_manager.prune_region_dir(&region_id).await {
225        warn!(err; "Failed to prune intermediate region directory, region_id: {}", region_id);
226    }
227
228    if let Some(write_cache) = cache_manager.write_cache()
229        && let Some(manifest_cache) = write_cache.manifest_cache()
230    {
231        manifest_cache.clean_manifests(table_dir).await;
232    }
233}
234
235/// Removes a region directory for full-drop style cleanup.
236///
237/// Full drop and purge/offline cleanup force physical deletion. Only partial
238/// drop may leave data files for global GC.
239pub(crate) async fn remove_region_dir_for_full_drop(
240    region_path: &str,
241    object_store: &ObjectStore,
242) -> Result<()> {
243    remove_region_dir_once(region_path, object_store, true).await?;
244    Ok(())
245}
246
247/// Carries the region hook and region metadata into the background GC task so
248/// it can fire [`RegionHook::on_region_files_removed`] once the dropped region's
249/// directory is physically deleted.
250///
251/// Only constructed when a hook is registered; the task receives it as
252/// `Option<&DropHookPayload>` so the no-hook path allocates nothing.
253///
254/// [`RegionHook::on_region_files_removed`]: crate::engine::region_hook::RegionHook::on_region_files_removed
255struct DropHookPayload {
256    hook: RegionHookRef,
257    metadata: RegionMetadataRef,
258}
259
260/// Background GC task to remove the entire region path once one of the following
261/// conditions is true:
262/// - It finds there is no parquet file left.
263/// - After `gc_duration`.
264///
265/// Returns whether the path is removed.
266///
267/// This task will retry on failure and keep running until finished. Any resource
268/// captured by it will not be released before then. Be sure to only pass weak reference
269/// if something is depended on ref-count mechanism.
270async fn later_drop_task_without_global_gc(
271    region_id: RegionId,
272    region_path: String,
273    object_store: ObjectStore,
274    dropping_regions: RegionMapRef,
275    gc_duration: Duration,
276    hook_payload: Option<&DropHookPayload>,
277) -> bool {
278    remove_region_with_retry(
279        region_id,
280        region_path,
281        object_store,
282        dropping_regions,
283        Some(gc_duration),
284        false,
285        hook_payload,
286    )
287    .await
288}
289
290async fn remove_region_with_retry(
291    region_id: RegionId,
292    region_path: String,
293    object_store: ObjectStore,
294    dropping_regions: std::sync::Arc<crate::region::RegionMap>,
295    gc_duration: Option<Duration>,
296    mut force: bool,
297    hook_payload: Option<&DropHookPayload>,
298) -> bool {
299    for _ in 0..MAX_RETRY_TIMES {
300        let result = remove_region_dir_once(&region_path, &object_store, force).await;
301        match result {
302            Err(err) => {
303                warn!(
304                    "Error occurs during trying to GC region dir {}: {}",
305                    region_path, err
306                );
307            }
308            Ok(true) => {
309                dropping_regions.remove_region(region_id);
310                info!("Region {} is dropped, force: {}", region_path, force);
311                // The region directory has been physically deleted; fire the
312                // terminal file-lifecycle event. The partial-drop/global-GC path
313                // never reaches here (it does not delete the directory itself).
314                if let Some(hook_payload) = hook_payload {
315                    hook_payload
316                        .hook
317                        .on_region_files_removed(region_id, &hook_payload.metadata)
318                        .await;
319                }
320                return true;
321            }
322            Ok(false) => (),
323        }
324        if let Some(duration) = gc_duration {
325            sleep(duration).await;
326        }
327        // Force recycle after gc duration.
328        force = true;
329    }
330
331    warn!(
332        "Failed to GC region dir {} after {} retries, giving up",
333        region_path, MAX_RETRY_TIMES
334    );
335
336    false
337}
338
339async fn later_drop_task_with_global_gc(
340    region_id: RegionId,
341    region_path: String,
342    path_type: PathType,
343    object_store: ObjectStore,
344    dropping_regions: RegionMapRef,
345    partial_drop: bool,
346    hook_payload: Option<&DropHookPayload>,
347) -> bool {
348    // For metadata regions or regions marked for full deletion (such as when dropping a table)
349    // the region directory is forcefully removed immediately.
350    //
351    // TODO(discord9): Evaluate removing files instantly rather than waiting for the GC period.
352    if should_force_remove_region_dir(path_type, partial_drop) {
353        remove_region_with_retry(
354            region_id,
355            region_path,
356            object_store,
357            dropping_regions,
358            None,
359            true,
360            hook_payload,
361        )
362        .await
363    } else {
364        // left for global gc
365        dropping_regions.remove_region(region_id);
366        true
367    }
368}
369
370fn should_force_remove_region_dir(path_type: PathType, partial_drop: bool) -> bool {
371    path_type == PathType::Metadata || !partial_drop
372}
373
374// TODO(ruihang): place the marker in a separate dir
375/// Removes region dir if there is no parquet files, returns whether the directory is removed.
376/// If `force = true`, always removes the dir.
377pub(crate) async fn remove_region_dir_once(
378    region_path: &str,
379    object_store: &ObjectStore,
380    force: bool,
381) -> Result<bool> {
382    // list all files under the given region path to check if there are un-deleted parquet files
383    let mut has_parquet_file = false;
384    // record all paths that neither ends with .parquet nor the marker file
385    let mut files_to_remove_first = vec![];
386    let mut files = object_store
387        .lister_with(region_path)
388        .await
389        .context(OpenDalSnafu)?;
390    while let Some(file) = files.try_next().await.context(OpenDalSnafu)? {
391        if !force && file.path().ends_with(".parquet") {
392            // If not in force mode, we only remove the region dir if there is no parquet file
393            has_parquet_file = true;
394            break;
395        } else if !file.path().ends_with(DROPPING_MARKER_FILE) {
396            let meta = file.metadata();
397            if meta.mode() == EntryMode::FILE {
398                files_to_remove_first.push(file.path().to_string());
399            }
400        }
401    }
402
403    if !has_parquet_file {
404        // no parquet file found, delete the region path
405        // first delete all files other than the marker
406        object_store
407            .delete_iter(files_to_remove_first)
408            .await
409            .context(OpenDalSnafu)?;
410        // then remove the marker with this dir
411        object_store
412            .delete_with(region_path)
413            .recursive(true)
414            .await
415            .context(OpenDalSnafu)?;
416        Ok(true)
417    } else {
418        Ok(false)
419    }
420}