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