Skip to main content

mito2/worker/
handle_alter.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 alter related requests.
16
17use std::str::FromStr;
18use std::sync::Arc;
19
20use common_base::readable_size::ReadableSize;
21use common_telemetry::info;
22use common_telemetry::tracing::warn;
23use humantime_serde::re::humantime;
24use snafu::{ResultExt, ensure};
25use store_api::logstore::LogStore;
26use store_api::metadata::{
27    InvalidSetRegionOptionRequestSnafu, MetadataError, RegionMetadata, RegionMetadataBuilder,
28    RegionMetadataRef,
29};
30use store_api::mito_engine_options;
31use store_api::mito_engine_options::MAX_ROW_GROUP_ROW_COUNT_LIMIT;
32use store_api::region_request::{AlterKind, RegionAlterRequest, SetRegionOption};
33use store_api::storage::RegionId;
34
35use crate::error::{InvalidMetadataSnafu, InvalidRegionRequestSnafu, Result};
36use crate::flush::FlushReason;
37use crate::manifest::action::RegionChange;
38use crate::region::MitoRegionRef;
39use crate::region::options::CompactionOptions::Twcs;
40use crate::region::options::{RegionOptions, TwcsOptions};
41use crate::region::version::VersionRef;
42use crate::request::{DdlRequest, OptionOutputTx, SenderDdlRequest};
43use crate::sst::FormatType;
44use crate::worker::RegionWorkerLoop;
45
46impl<S: LogStore> RegionWorkerLoop<S> {
47    pub(crate) async fn handle_alter_request(
48        &mut self,
49        region_id: RegionId,
50        request: RegionAlterRequest,
51        sender: OptionOutputTx,
52    ) {
53        let skip_wal_only = only_enables_skip_wal(&request.kind);
54        let (region, is_follower) = match self.regions.writable_non_staging_region(region_id) {
55            Ok(region) => (region, false),
56            Err(_) if skip_wal_only => match self.regions.follower_region(region_id) {
57                Ok(region) => (region, true),
58                Err(e) => {
59                    sender.send(Err(e));
60                    return;
61                }
62            },
63            Err(e) => {
64                sender.send(Err(e));
65                return;
66            }
67        };
68
69        info!("Try to alter region: {}, request: {:?}", region_id, request);
70
71        // Followers only accept skip-WAL, which is an in-memory option change and must
72        // never enter the leader path that may flush memtables.
73        if is_follower {
74            let mut options = region.version().options.clone();
75            if !options.skip_wal {
76                info!("Stop writing WAL for follower region: {}", region_id);
77                options.skip_wal = true;
78                region.version_control.alter_options(options);
79            }
80            sender.send(Ok(0));
81            return;
82        }
83
84        // Gets the version before alter.
85        let version = region.version();
86
87        // fast path for memory state changes like options.
88        let set_options = match &request.kind {
89            AlterKind::SetRegionOptions { options } => options.clone(),
90            AlterKind::UnsetRegionOptions { keys } => {
91                // Converts the keys to SetRegionOption.
92                //
93                // It passes an empty string to achieve the purpose of unset
94                keys.iter().map(Into::into).collect()
95            }
96            _ => Vec::new(),
97        };
98        let mut new_options = None;
99        if !set_options.is_empty() {
100            match self.handle_alter_region_options_fast(&region, version.clone(), set_options) {
101                Ok(staged_options) => {
102                    let Some(staged_options) = staged_options else {
103                        // We don't have options to alter after flush.
104                        sender.send(Ok(0));
105                        return;
106                    };
107                    new_options = Some(staged_options);
108                }
109                Err(e) => {
110                    sender.send(Err(e).context(InvalidMetadataSnafu));
111                    return;
112                }
113            }
114        }
115
116        // Validates request.
117        if let Err(e) = request.validate(&version.metadata) {
118            // Invalid request.
119            sender.send(Err(e).context(InvalidRegionRequestSnafu));
120            return;
121        }
122
123        // Checks whether we need to alter the region.
124        if !request.need_alter(&version.metadata) {
125            warn!(
126                "Ignores alter request as it alters nothing, region_id: {}, request: {:?}",
127                region_id, request
128            );
129            sender.send(Ok(0));
130            return;
131        }
132
133        // Checks whether we can alter the region directly.
134        if !version.memtables.is_empty() {
135            // If memtable is not empty, we can't alter it directly and need to flush
136            // all memtables first.
137            info!("Flush region: {} before alteration", region_id);
138
139            // Try to submit a flush task.
140            let task = self.new_flush_task(&region, FlushReason::Alter, None, self.config.clone());
141            if let Err(e) =
142                self.flush_scheduler
143                    .schedule_flush(region.region_id, &region.version_control, task)
144            {
145                // Unable to flush the region, send error to waiter.
146                sender.send(Err(e));
147                return;
148            }
149
150            // Safety: We have requested flush.
151            self.flush_scheduler
152                .add_ddl_request_to_pending(SenderDdlRequest {
153                    region_id,
154                    sender,
155                    request: DdlRequest::Alter(request),
156                });
157
158            return;
159        }
160
161        info!(
162            "Try to alter region {}, version.metadata: {:?}, version.options: {:?}, request: {:?}",
163            region_id, version.metadata, version.options, request,
164        );
165        self.handle_alter_region_with_empty_memtable(region, version, request, new_options, sender);
166    }
167
168    // TODO(yingwen): Optional new options and sst format.
169    /// Handles region metadata and format changes when the region memtable is empty.
170    fn handle_alter_region_with_empty_memtable(
171        &mut self,
172        region: MitoRegionRef,
173        version: VersionRef,
174        request: RegionAlterRequest,
175        new_options: Option<RegionOptions>,
176        sender: OptionOutputTx,
177    ) {
178        let need_index = need_change_index(&request.kind);
179        let new_meta = match metadata_after_alteration(&version.metadata, request) {
180            Ok(new_meta) => new_meta,
181            Err(e) => {
182                sender.send(Err(e));
183                return;
184            }
185        };
186        // Persist the metadata to region's manifest.
187        let options = new_options.as_ref().unwrap_or(&version.options);
188        let change = RegionChange {
189            metadata: new_meta,
190            sst_format: options.sst_format.unwrap_or_default(),
191            append_mode: Some(options.append_mode),
192        };
193        self.handle_manifest_region_change(region, change, need_index, new_options, sender);
194    }
195
196    /// Handles requests that changes region options, like TTL. It only affects memory state
197    /// since changes are persisted in the `DatanodeTableValue` in metasrv.
198    ///
199    /// If the options require empty memtable, it only does validation.
200    ///
201    /// Returns the staged options if they need further alteration.
202    fn handle_alter_region_options_fast(
203        &mut self,
204        region: &MitoRegionRef,
205        version: VersionRef,
206        options: Vec<SetRegionOption>,
207    ) -> std::result::Result<Option<RegionOptions>, MetadataError> {
208        assert!(!options.is_empty());
209
210        let mut all_options_altered = true;
211        let mut current_options = version.options.clone();
212        for option in options.iter().cloned() {
213            match option {
214                SetRegionOption::WriteBufferSize(new_write_buffer_size) => {
215                    info!(
216                        "Update region write_buffer_size: {}, previous: {:?} new: {:?}",
217                        region.region_id, current_options.write_buffer_size, new_write_buffer_size
218                    );
219                    current_options.write_buffer_size = new_write_buffer_size;
220                    current_options.validate().map_err(|e| {
221                        store_api::metadata::InvalidRegionRequestSnafu {
222                            region_id: region.region_id,
223                            err: e.to_string(),
224                        }
225                        .build()
226                    })?;
227                }
228                SetRegionOption::Ttl(new_ttl) => {
229                    info!(
230                        "Update region ttl: {}, previous: {:?} new: {:?}",
231                        region.region_id, current_options.ttl, new_ttl
232                    );
233                    current_options.ttl = new_ttl;
234                }
235                SetRegionOption::Twsc(key, value) => {
236                    let Twcs(options) = &mut current_options.compaction;
237                    set_twcs_options(
238                        options,
239                        &TwcsOptions::default(),
240                        &key,
241                        &value,
242                        region.region_id,
243                    )?;
244                }
245                SetRegionOption::Format(format_str) => {
246                    let new_format = format_str.parse::<FormatType>().map_err(|_| {
247                        store_api::metadata::InvalidRegionRequestSnafu {
248                            region_id: region.region_id,
249                            err: format!("Invalid format type: {}", format_str),
250                        }
251                        .build()
252                    })?;
253                    // If the format is unchanged, we also consider the option is altered.
254                    if new_format != current_options.sst_format.unwrap_or_default() {
255                        all_options_altered = false;
256                    }
257                }
258                SetRegionOption::AppendMode(new_append_mode) => {
259                    // If the append mode is unchanged, we consider the option is altered.
260                    if new_append_mode != current_options.append_mode {
261                        // Validates: only allow changing from false to true.
262                        ensure!(
263                            !current_options.append_mode && new_append_mode,
264                            store_api::metadata::InvalidRegionRequestSnafu {
265                                region_id: region.region_id,
266                                err: "Only allow changing append_mode from false to true",
267                            }
268                        );
269                        // Clear merge_mode since it's incompatible with append_mode.
270                        current_options.merge_mode = None;
271                        all_options_altered = false;
272                    }
273                }
274                SetRegionOption::AutoFlushInterval(new_interval) => {
275                    // The flush logic reads the effective interval from the region's
276                    // current version each cycle, so the change takes effect on the
277                    // next flush without a memtable flush.
278                    if new_interval != current_options.auto_flush_interval {
279                        info!(
280                            "Update region auto_flush_interval: {}, previous: {:?} new: {:?}",
281                            region.region_id, current_options.auto_flush_interval, new_interval
282                        );
283                        current_options.auto_flush_interval = new_interval;
284                    }
285                }
286                SetRegionOption::MaxRowGroupRowCount(new_row_count) => {
287                    if let Some(row_count) = new_row_count {
288                        ensure!(
289                            row_count > 0 && row_count <= MAX_ROW_GROUP_ROW_COUNT_LIMIT,
290                            store_api::metadata::InvalidRegionRequestSnafu {
291                                region_id: region.region_id,
292                                err: format!(
293                                    "max_row_group_row_count must be in (0, \
294                                     {MAX_ROW_GROUP_ROW_COUNT_LIMIT}], got {row_count}"
295                                ),
296                            }
297                        );
298                    }
299                    if new_row_count != current_options.max_row_group_row_count {
300                        all_options_altered = false;
301                    }
302                }
303                SetRegionOption::SkipWal => {
304                    if !current_options.skip_wal {
305                        info!("Stop writing WAL for region: {}", region.region_id);
306                        current_options.skip_wal = true;
307                    }
308                }
309            }
310        }
311        if all_options_altered {
312            region.version_control.alter_options(current_options);
313            Ok(None)
314        } else {
315            let kind = AlterKind::SetRegionOptions { options };
316            Ok(new_region_options_on_empty_memtable(
317                &current_options,
318                &kind,
319            ))
320        }
321    }
322}
323
324fn only_enables_skip_wal(kind: &AlterKind) -> bool {
325    matches!(
326        kind,
327        AlterKind::SetRegionOptions { options }
328            if matches!(options.as_slice(), [SetRegionOption::SkipWal])
329    )
330}
331
332/// Returns the new region options if there are updates to the options.
333fn new_region_options_on_empty_memtable(
334    current_options: &RegionOptions,
335    kind: &AlterKind,
336) -> Option<RegionOptions> {
337    let options = match kind {
338        AlterKind::SetRegionOptions { options } => options.clone(),
339        AlterKind::UnsetRegionOptions { keys } => keys.iter().map(Into::into).collect(),
340        _ => return None,
341    };
342
343    if options.is_empty() {
344        return None;
345    }
346
347    let mut current_options = current_options.clone();
348    for option in &options {
349        match option {
350            SetRegionOption::WriteBufferSize(_)
351            | SetRegionOption::Ttl(_)
352            | SetRegionOption::Twsc(_, _)
353            | SetRegionOption::AutoFlushInterval(_)
354            | SetRegionOption::SkipWal => (),
355            SetRegionOption::Format(format_str) => {
356                // Safety: handle_alter_region_options_fast() has validated this.
357                let new_format = format_str.parse::<FormatType>().unwrap();
358                current_options.sst_format = Some(new_format);
359            }
360            SetRegionOption::AppendMode(new_append_mode) => {
361                if *new_append_mode != current_options.append_mode {
362                    // Safety: handle_alter_region_options_fast() has validated that the only
363                    // supported transition is from false to true.
364                    current_options.append_mode = *new_append_mode;
365                    current_options.merge_mode = None;
366                }
367            }
368            SetRegionOption::MaxRowGroupRowCount(new_row_count) => {
369                current_options.max_row_group_row_count = *new_row_count;
370            }
371        }
372    }
373    Some(current_options)
374}
375
376/// Creates a metadata after applying the alter `request` to the old `metadata`.
377///
378/// Returns an error if the `request` is invalid.
379fn metadata_after_alteration(
380    metadata: &RegionMetadata,
381    request: RegionAlterRequest,
382) -> Result<RegionMetadataRef> {
383    let mut builder = RegionMetadataBuilder::from_existing(metadata.clone());
384    builder
385        .alter(request.kind)
386        .context(InvalidRegionRequestSnafu)?
387        .bump_version();
388    let new_meta = builder.build().context(InvalidMetadataSnafu)?;
389
390    Ok(Arc::new(new_meta))
391}
392
393fn set_twcs_options(
394    options: &mut TwcsOptions,
395    default_option: &TwcsOptions,
396    key: &str,
397    value: &str,
398    region_id: RegionId,
399) -> std::result::Result<(), MetadataError> {
400    match key {
401        mito_engine_options::TWCS_TRIGGER_FILE_NUM => {
402            let files = parse_usize_with_default(key, value, default_option.trigger_file_num)?;
403            log_option_update(region_id, key, options.trigger_file_num, files);
404            options.trigger_file_num = files;
405        }
406        mito_engine_options::TWCS_MAX_OUTPUT_FILE_SIZE => {
407            let size = if value.is_empty() {
408                default_option.max_output_file_size
409            } else {
410                Some(
411                    ReadableSize::from_str(value)
412                        .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?,
413                )
414            };
415            log_option_update(region_id, key, options.max_output_file_size, size);
416            options.max_output_file_size = size;
417        }
418        mito_engine_options::TWCS_TIME_WINDOW => {
419            let window = if value.is_empty() {
420                default_option.time_window
421            } else {
422                Some(
423                    humantime::parse_duration(value)
424                        .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?,
425                )
426            };
427            log_option_update(region_id, key, options.time_window, window);
428            options.time_window = window;
429        }
430        _ => return InvalidSetRegionOptionRequestSnafu { key, value }.fail(),
431    }
432    Ok(())
433}
434
435fn parse_usize_with_default(
436    key: &str,
437    value: &str,
438    default: usize,
439) -> std::result::Result<usize, MetadataError> {
440    if value.is_empty() {
441        Ok(default)
442    } else {
443        value
444            .parse::<usize>()
445            .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())
446    }
447}
448
449fn log_option_update<T: std::fmt::Debug>(
450    region_id: RegionId,
451    option_name: &str,
452    prev_value: T,
453    cur_value: T,
454) {
455    info!(
456        "Update region {}: {}, previous: {:?}, new: {:?}",
457        option_name, region_id, prev_value, cur_value
458    );
459}
460
461/// Used to determine whether we can build index directly after schema change.
462fn need_change_index(kind: &AlterKind) -> bool {
463    match kind {
464        // `SetIndexes` is a fast-path operation because it can build indexes for existing SSTs
465        // in the background, without needing to wait for a flush or compaction cycle.
466        AlterKind::SetIndexes { options: _ } => true,
467        // For AddColumns, DropColumns, UnsetIndexes and ModifyColumnTypes, we don't treat them as index changes.
468        // Index files still need to be rebuilt after schema changes,
469        // but this will happen automatically during flush or compaction.
470        _ => false,
471    }
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    #[test]
479    fn test_new_region_options_with_idempotent_append_mode() {
480        let current_options = RegionOptions::default();
481        let kind = AlterKind::SetRegionOptions {
482            options: vec![
483                SetRegionOption::AppendMode(false),
484                SetRegionOption::MaxRowGroupRowCount(Some(1024)),
485            ],
486        };
487
488        let new_options = new_region_options_on_empty_memtable(&current_options, &kind).unwrap();
489        assert!(!new_options.append_mode);
490        assert_eq!(Some(1024), new_options.max_row_group_row_count);
491    }
492}