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