Skip to main content

mito2/worker/
handle_truncate.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 truncate related requests.
16
17use common_telemetry::{debug, info, warn};
18use store_api::logstore::LogStore;
19use store_api::region_request::RegionTruncateRequest;
20use store_api::storage::RegionId;
21
22use crate::error::RegionNotFoundSnafu;
23use crate::manifest::action::{RegionTruncate, TruncateKind};
24use crate::region::RegionLeaderState;
25use crate::request::{DdlRequest, DiscardUnflushedResult, OptionOutputTx, TruncateResult};
26use crate::worker::RegionWorkerLoop;
27
28impl<S: LogStore> RegionWorkerLoop<S> {
29    pub(crate) async fn handle_truncate_request(
30        &mut self,
31        region_id: RegionId,
32        req: RegionTruncateRequest,
33        sender: OptionOutputTx,
34    ) {
35        let region = match self.regions.writable_non_staging_region(region_id) {
36            Ok(region) => region,
37            Err(e) => {
38                sender.send(Err(e));
39                return;
40            }
41        };
42
43        // Fence compaction before any later flush wait so handed-off DDLs keep admission paused.
44        let (sender, req) = match self.compaction_scheduler.try_cancel_and_add_ddl(
45            region_id,
46            sender,
47            req,
48            DdlRequest::Truncate,
49        ) {
50            Ok(()) => {
51                self.listener.on_compaction_cancel_requested(region_id);
52                return;
53            }
54            Err(request) => request,
55        };
56
57        let (sender, req) = match self.flush_scheduler.try_cancel_and_add_ddl(
58            region_id,
59            sender,
60            req,
61            DdlRequest::Truncate,
62        ) {
63            Ok(()) => {
64                self.listener.on_flush_cancel_requested(region_id);
65                return;
66            }
67            Err(request) => request,
68        };
69
70        let version_data = region.version_control.current();
71
72        match req {
73            RegionTruncateRequest::All => {
74                info!("Try to fully truncate region {}", region_id);
75
76                let truncated_entry_id = version_data.last_entry_id;
77                let truncated_sequence = version_data.committed_sequence;
78
79                // Write region truncated to manifest.
80                let truncate = RegionTruncate {
81                    region_id,
82                    kind: TruncateKind::All {
83                        truncated_entry_id,
84                        truncated_sequence,
85                    },
86                    timestamp_ms: None,
87                };
88
89                self.handle_manifest_truncate_action(region, truncate, sender);
90            }
91            RegionTruncateRequest::Unflushed => {
92                let memtables = &version_data.version.memtables;
93                if memtables.is_empty()
94                    && version_data.version.flushed_entry_id == version_data.last_entry_id
95                    && version_data.version.flushed_sequence == version_data.committed_sequence
96                {
97                    self.update_topic_latest_entry_id(&region);
98                    let result = self
99                        .wal
100                        .obsolete(
101                            region_id,
102                            version_data.version.flushed_entry_id,
103                            &region.provider,
104                        )
105                        .await
106                        .map(|_| 0);
107                    sender.send(result);
108                    return;
109                }
110
111                let discarded_rows = memtables.num_rows();
112                let discarded_bytes =
113                    (memtables.mutable_usage() + memtables.immutables_usage()) as u64;
114                warn!(
115                    "Discarding unflushed data from region {}, entry_id: {}, sequence: {}, estimated rows: {}, estimated bytes: {}",
116                    region_id,
117                    version_data.last_entry_id,
118                    version_data.committed_sequence,
119                    discarded_rows,
120                    discarded_bytes,
121                );
122
123                self.handle_manifest_discard_unflushed_action(
124                    region,
125                    version_data.last_entry_id,
126                    version_data.committed_sequence,
127                    discarded_rows,
128                    discarded_bytes,
129                    sender,
130                );
131            }
132            RegionTruncateRequest::ByTimeRanges { time_ranges } => {
133                info!(
134                    "Try to partially truncate region {} by time ranges: {:?}",
135                    region_id, time_ranges
136                );
137                // find all files that are fully contained in the time ranges
138                let mut files_to_truncate = Vec::new();
139                for level in version_data.version.ssts.levels() {
140                    for file in level.files() {
141                        let file_time_range = file.time_range();
142                        // TODO(discord9): This is a naive way to check if is contained, we should
143                        // optimize it later.
144                        let is_subset = time_ranges.iter().any(|(start, end)| {
145                            file_time_range.0 >= *start && file_time_range.1 <= *end
146                        });
147
148                        if is_subset {
149                            files_to_truncate.push(file.meta_ref().clone());
150                        }
151                    }
152                }
153                debug!(
154                    "Found {} files to partially truncate in region {}",
155                    files_to_truncate.len(),
156                    region_id
157                );
158
159                // this could happen if all files are not fully contained in the time ranges
160                if files_to_truncate.is_empty() {
161                    info!("No files to truncate in region {}", region_id);
162                    // directly send success back as no files to truncate
163                    // and no background notify is needed
164                    sender.send(Ok(0));
165                    return;
166                }
167                let truncate = RegionTruncate {
168                    region_id,
169                    kind: TruncateKind::Partial {
170                        files_to_remove: files_to_truncate,
171                    },
172                    timestamp_ms: None,
173                };
174                self.handle_manifest_truncate_action(region, truncate, sender);
175            }
176        };
177    }
178
179    /// Handles truncate result.
180    pub(crate) async fn handle_truncate_result(&mut self, truncate_result: TruncateResult) {
181        let region_id = truncate_result.region_id;
182        let Some(region) = self.regions.get_region(region_id) else {
183            truncate_result.sender.send(
184                RegionNotFoundSnafu {
185                    region_id: truncate_result.region_id,
186                }
187                .fail(),
188            );
189            return;
190        };
191
192        // We are already in the worker loop so we can set the state first.
193        region.switch_state_to_writable(RegionLeaderState::Truncating);
194
195        match truncate_result.result {
196            Ok(()) => {
197                // Applies the truncate action to the region.
198                region
199                    .version_control
200                    .truncate(truncate_result.kind.clone());
201            }
202            Err(e) => {
203                // Unable to truncate the region.
204                truncate_result.sender.send(Err(e));
205                return;
206            }
207        }
208
209        // Notifies flush scheduler.
210        self.flush_scheduler.on_region_truncated(region_id);
211        // Notifies compaction scheduler.
212        self.compaction_scheduler.on_region_truncated(region_id);
213        // Notifies index build scheduler.
214        self.index_build_scheduler
215            .on_region_truncated(region_id)
216            .await;
217
218        if let TruncateKind::All {
219            truncated_entry_id,
220            truncated_sequence: _,
221        } = &truncate_result.kind
222        {
223            // Make all data obsolete.
224            if let Err(e) = self
225                .wal
226                .obsolete(region_id, *truncated_entry_id, &region.provider)
227                .await
228            {
229                truncate_result.sender.send(Err(e));
230                return;
231            }
232        }
233
234        info!(
235            "Complete truncating region: {}, kind: {:?}.",
236            region_id, truncate_result.kind
237        );
238
239        truncate_result.sender.send(Ok(0));
240    }
241
242    /// Handles the result of advancing the replay frontier for unflushed data.
243    pub(crate) async fn handle_discard_unflushed_result(&mut self, result: DiscardUnflushedResult) {
244        let region_id = result.region_id;
245        let Some(region) = self.regions.get_region(region_id) else {
246            result.sender.send(RegionNotFoundSnafu { region_id }.fail());
247            return;
248        };
249
250        region.switch_state_to_writable(RegionLeaderState::Truncating);
251
252        if let Err(e) = result.result {
253            result.sender.send(Err(e));
254            return;
255        }
256
257        region
258            .version_control
259            .discard_unflushed(result.discarded_entry_id, result.discarded_sequence);
260
261        // The flush and compaction schedulers are already quiesced because the request
262        // queued itself behind any running job, so these two are usually no-ops. Index
263        // builds aren't queued that way, so pending builds for surviving files are
264        // retired here. They would abort anyway while the region sits in `Truncating`,
265        // and those files may need a later index rebuild.
266        self.flush_scheduler.on_region_truncated(region_id);
267        self.compaction_scheduler.on_region_truncated(region_id);
268        self.index_build_scheduler
269            .on_region_truncated(region_id)
270            .await;
271        self.update_topic_latest_entry_id(&region);
272
273        // Discarding memtables releases write buffer memory. Notify other workers and
274        // retry requests stalled on this worker before WAL cleanup, which may fail even
275        // though the memory has already been released.
276        self.notify_group();
277        self.handle_stalled_requests().await;
278
279        if let Err(e) = self
280            .wal
281            .obsolete(region_id, result.discarded_entry_id, &region.provider)
282            .await
283        {
284            result.sender.send(Err(e));
285            return;
286        }
287
288        warn!(
289            "Discarded unflushed data from region {}, entry_id: {}, sequence: {}, estimated rows: {}, estimated bytes: {}",
290            region_id,
291            result.discarded_entry_id,
292            result.discarded_sequence,
293            result.discarded_rows,
294            result.discarded_bytes,
295        );
296        result.sender.send(Ok(0));
297    }
298}