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