1use std::str::FromStr;
18use std::sync::Arc;
19
20use common_base::readable_size::ReadableSize;
21use common_telemetry::tracing::warn;
22use common_telemetry::{error, info};
23use humantime_serde::re::humantime;
24use snafu::{ResultExt, ensure};
25use store_api::logstore::LogStore;
26use store_api::logstore::provider::Provider;
27use store_api::metadata::{
28 InvalidSetRegionOptionRequestSnafu, MetadataError, RegionMetadata, RegionMetadataBuilder,
29 RegionMetadataRef,
30};
31use store_api::mito_engine_options;
32use store_api::mito_engine_options::MAX_ROW_GROUP_ROW_COUNT_LIMIT;
33use store_api::region_request::{AlterKind, RegionAlterRequest, SetRegionOption};
34use store_api::storage::RegionId;
35
36use crate::error::{InvalidMetadataSnafu, InvalidRegionRequestSnafu, Result};
37use crate::flush::FlushReason;
38use crate::manifest::action::RegionChange;
39use crate::memtable::ensure_json2_uses_flat_format;
40use crate::region::MitoRegionRef;
41use crate::region::options::CompactionOptions::Twcs;
42use crate::region::options::{RegionOptions, TwcsOptions};
43use crate::region::version::VersionRef;
44use crate::request::{DdlRequest, OptionOutputTx, SenderDdlRequest};
45use crate::sst::FormatType;
46use crate::worker::RegionWorkerLoop;
47
48pub(crate) type PendingAlters = std::collections::HashMap<
49 RegionId,
50 std::collections::VecDeque<(RegionAlterRequest, OptionOutputTx)>,
51>;
52
53impl<S: LogStore> RegionWorkerLoop<S> {
54 pub(crate) async fn handle_alter_request(
55 &mut self,
56 region_id: RegionId,
57 request: RegionAlterRequest,
58 sender: OptionOutputTx,
59 ) {
60 if self.flush_scheduler.has_pending_alter(region_id)
61 || self.pending_alters.contains_key(®ion_id)
62 || self.regions.get_region(region_id).is_some_and(|region| {
63 matches!(
64 region.state(),
65 crate::region::RegionRoleState::Leader(
66 crate::region::RegionLeaderState::Editing
67 | crate::region::RegionLeaderState::Altering
68 )
69 )
70 })
71 {
72 let queue = self.pending_alters.entry(region_id).or_default();
73 if queue.len() >= 128 {
74 sender.send(crate::error::RegionBusySnafu { region_id }.fail());
75 } else {
76 queue.push_back((request, sender));
77 }
78 self.dispatch_pending_region_requests(region_id).await;
79 return;
80 }
81 self.execute_alter_request(region_id, request, sender).await;
82 }
83
84 pub(crate) async fn execute_alter_request(
85 &mut self,
86 region_id: RegionId,
87 request: RegionAlterRequest,
88 sender: OptionOutputTx,
89 ) {
90 let requested_skip_wal = skip_wal_value(&request.kind);
91 let (region, follower_skip_wal) = match self.regions.writable_non_staging_region(region_id)
92 {
93 Ok(region) => (region, None),
94 Err(_) if requested_skip_wal.is_some() => {
95 match self.regions.follower_region(region_id) {
96 Ok(region) => (region, requested_skip_wal),
97 Err(e) => {
98 sender.send(Err(e));
99 return;
100 }
101 }
102 }
103 Err(e) => {
104 sender.send(Err(e));
105 return;
106 }
107 };
108
109 info!("Try to alter region: {}, request: {:?}", region_id, request);
110
111 if let Some(skip_wal) = follower_skip_wal {
114 if let Err(e) = validate_skip_wal_change(®ion, skip_wal) {
115 sender.send(Err(e).context(InvalidMetadataSnafu));
116 return;
117 }
118 let mut options = region.version().options.clone();
119 if options.skip_wal != skip_wal {
120 info!(
121 "Set skip_wal for follower region: {}, previous: {} new: {}",
122 region_id, options.skip_wal, skip_wal
123 );
124 options.skip_wal = skip_wal;
125 region.version_control.alter_options(options);
126 }
127 sender.send(Ok(0));
128 return;
129 }
130
131 let version = region.version();
133
134 let set_options = match &request.kind {
136 AlterKind::SetRegionOptions { options } => options.clone(),
137 AlterKind::UnsetRegionOptions { keys } => {
138 keys.iter().map(Into::into).collect()
142 }
143 _ => Vec::new(),
144 };
145 let mut new_options = None;
146 if !set_options.is_empty() {
147 match self.handle_alter_region_options_fast(®ion, version.clone(), set_options) {
148 Ok(staged_options) => {
149 let Some(staged_options) = staged_options else {
150 sender.send(Ok(0));
152 return;
153 };
154 new_options = Some(staged_options);
155 }
156 Err(e) => {
157 sender.send(Err(e).context(InvalidMetadataSnafu));
158 return;
159 }
160 }
161 }
162
163 if let Err(e) = request.validate(&version.metadata) {
165 sender.send(Err(e).context(InvalidRegionRequestSnafu));
167 return;
168 }
169
170 if !request.need_alter(&version.metadata) {
172 warn!(
173 "Ignores alter request as it alters nothing, region_id: {}, request: {:?}",
174 region_id, request
175 );
176 sender.send(Ok(0));
177 return;
178 }
179
180 if !version.memtables.is_empty() {
182 info!("Flush region: {} before alteration", region_id);
185
186 let task = self.new_flush_task(®ion, FlushReason::Alter, None, self.config.clone());
188 if let Err(e) =
189 self.flush_scheduler
190 .schedule_flush(region.region_id, ®ion.version_control, task)
191 {
192 sender.send(Err(e));
194 return;
195 }
196
197 self.flush_scheduler
199 .add_ddl_request_to_pending(SenderDdlRequest {
200 region_id,
201 sender,
202 request: DdlRequest::Alter(request),
203 });
204
205 return;
206 }
207
208 info!(
209 "Try to alter region {}, version.metadata: {:?}, version.options: {:?}, request: {:?}",
210 region_id, version.metadata, version.options, request,
211 );
212 log_time_index_widening_overflow(region.region_id, &version, &request);
213 self.handle_alter_region_with_empty_memtable(region, version, request, new_options, sender);
214 }
215
216 fn handle_alter_region_with_empty_memtable(
219 &mut self,
220 region: MitoRegionRef,
221 version: VersionRef,
222 request: RegionAlterRequest,
223 new_options: Option<RegionOptions>,
224 sender: OptionOutputTx,
225 ) {
226 let need_index = need_change_index(&request.kind);
227 let new_meta = match metadata_after_alteration(&version.metadata, request) {
228 Ok(new_meta) => new_meta,
229 Err(e) => {
230 sender.send(Err(e));
231 return;
232 }
233 };
234 let options = new_options.as_ref().unwrap_or(&version.options);
235 if let Err(e) = ensure_json2_uses_flat_format(&new_meta, options) {
236 sender.send(Err(e));
237 return;
238 }
239 if let Err(e) = options.validate_column_compression(&new_meta) {
240 sender.send(Err(e));
241 return;
242 }
243 let change = RegionChange {
245 metadata: new_meta,
246 sst_format: options.sst_format.unwrap_or_default(),
247 append_mode: Some(options.append_mode),
248 };
249 self.handle_manifest_region_change(region, change, need_index, new_options, sender);
250 }
251
252 fn handle_alter_region_options_fast(
263 &mut self,
264 region: &MitoRegionRef,
265 version: VersionRef,
266 options: Vec<SetRegionOption>,
267 ) -> std::result::Result<Option<RegionOptions>, MetadataError> {
268 assert!(!options.is_empty());
269
270 let mut all_options_altered = true;
271 let mut current_options = version.options.clone();
272 for option in options.iter().cloned() {
273 match option {
274 SetRegionOption::WriteBufferSize(new_write_buffer_size) => {
275 info!(
276 "Update region write_buffer_size: {}, previous: {:?} new: {:?}",
277 region.region_id, current_options.write_buffer_size, new_write_buffer_size
278 );
279 current_options.write_buffer_size = new_write_buffer_size;
280 current_options.validate().map_err(|e| {
281 store_api::metadata::InvalidRegionRequestSnafu {
282 region_id: region.region_id,
283 err: e.to_string(),
284 }
285 .build()
286 })?;
287 }
288 SetRegionOption::Ttl(new_ttl) => {
289 info!(
290 "Update region ttl: {}, previous: {:?} new: {:?}",
291 region.region_id, current_options.ttl, new_ttl
292 );
293 current_options.ttl = new_ttl;
294 }
295 SetRegionOption::Twsc(key, value) => {
296 let Twcs(options) = &mut current_options.compaction;
297 set_twcs_options(
298 options,
299 &TwcsOptions::default(),
300 &key,
301 &value,
302 region.region_id,
303 )?;
304 if !value.is_empty() {
305 current_options.compaction_override = true;
306 }
307 }
308 SetRegionOption::Format(format_str) => {
309 let new_format = format_str.parse::<FormatType>().map_err(|_| {
310 store_api::metadata::InvalidRegionRequestSnafu {
311 region_id: region.region_id,
312 err: format!("Invalid format type: {}", format_str),
313 }
314 .build()
315 })?;
316 if new_format != current_options.sst_format.unwrap_or_default() {
318 all_options_altered = false;
319 }
320 }
321 SetRegionOption::AppendMode(new_append_mode) => {
322 if new_append_mode != current_options.append_mode {
324 ensure!(
326 !current_options.append_mode && new_append_mode,
327 store_api::metadata::InvalidRegionRequestSnafu {
328 region_id: region.region_id,
329 err: "Only allow changing append_mode from false to true",
330 }
331 );
332 current_options.merge_mode = None;
334 all_options_altered = false;
335 }
336 }
337 SetRegionOption::AutoFlushInterval(new_interval) => {
338 if new_interval != current_options.auto_flush_interval {
342 info!(
343 "Update region auto_flush_interval: {}, previous: {:?} new: {:?}",
344 region.region_id, current_options.auto_flush_interval, new_interval
345 );
346 current_options.auto_flush_interval = new_interval;
347 }
348 }
349 SetRegionOption::CompactionOutputParallelism(parallelism) => {
350 current_options.compaction_output_parallelism = parallelism;
353 }
354 SetRegionOption::MaxRowGroupRowCount(new_row_count) => {
355 if let Some(row_count) = new_row_count {
356 ensure!(
357 row_count > 0 && row_count <= MAX_ROW_GROUP_ROW_COUNT_LIMIT,
358 store_api::metadata::InvalidRegionRequestSnafu {
359 region_id: region.region_id,
360 err: format!(
361 "max_row_group_row_count must be in (0, \
362 {MAX_ROW_GROUP_ROW_COUNT_LIMIT}], got {row_count}"
363 ),
364 }
365 );
366 }
367 if new_row_count != current_options.max_row_group_row_count {
368 all_options_altered = false;
369 }
370 }
371 SetRegionOption::PreserveRowSequence(new_preserve) => {
372 if new_preserve != current_options.preserve_row_sequence {
373 info!(
374 "Update region preserve_row_sequence: {}, previous: {:?} new: {:?}",
375 region.region_id, current_options.preserve_row_sequence, new_preserve
376 );
377 current_options.preserve_row_sequence = new_preserve;
378 }
379 }
380 SetRegionOption::SkipWal(skip_wal) => {
381 validate_skip_wal_change(region, skip_wal)?;
382 if current_options.skip_wal != skip_wal {
383 info!(
384 "Set skip_wal for region: {}, previous: {} new: {}",
385 region.region_id, current_options.skip_wal, skip_wal
386 );
387 current_options.skip_wal = skip_wal;
388 }
389 }
390 }
391 }
392 let kind = AlterKind::SetRegionOptions { options };
393 let candidate = new_region_options_on_empty_memtable(¤t_options, &kind)
400 .unwrap_or_else(|| current_options.clone());
401 candidate.validate().map_err(|e| {
402 store_api::metadata::InvalidRegionRequestSnafu {
403 region_id: region.region_id,
404 err: e.to_string(),
405 }
406 .build()
407 })?;
408 candidate
409 .validate_column_compression(&version.metadata)
410 .map_err(|e| {
411 store_api::metadata::InvalidRegionRequestSnafu {
412 region_id: region.region_id,
413 err: e.to_string(),
414 }
415 .build()
416 })?;
417 if all_options_altered {
418 region.version_control.alter_options(candidate);
419 Ok(None)
420 } else {
421 Ok(Some(candidate))
424 }
425 }
426}
427
428fn skip_wal_value(kind: &AlterKind) -> Option<bool> {
429 let AlterKind::SetRegionOptions { options } = kind else {
430 return None;
431 };
432 let [SetRegionOption::SkipWal(skip_wal)] = options.as_slice() else {
433 return None;
434 };
435 Some(*skip_wal)
436}
437
438fn validate_skip_wal_change(
439 region: &MitoRegionRef,
440 skip_wal: bool,
441) -> std::result::Result<(), MetadataError> {
442 ensure!(
443 skip_wal || !matches!(®ion.provider, Provider::Noop),
444 store_api::metadata::InvalidRegionRequestSnafu {
445 region_id: region.region_id,
446 err: "cannot enable WAL because the region uses the Noop WAL provider".to_string(),
447 }
448 );
449 Ok(())
450}
451
452fn new_region_options_on_empty_memtable(
454 current_options: &RegionOptions,
455 kind: &AlterKind,
456) -> Option<RegionOptions> {
457 let options = match kind {
458 AlterKind::SetRegionOptions { options } => options.clone(),
459 AlterKind::UnsetRegionOptions { keys } => keys.iter().map(Into::into).collect(),
460 _ => return None,
461 };
462
463 if options.is_empty() {
464 return None;
465 }
466
467 let mut current_options = current_options.clone();
468 for option in &options {
469 match option {
470 SetRegionOption::WriteBufferSize(_)
471 | SetRegionOption::Ttl(_)
472 | SetRegionOption::Twsc(_, _)
473 | SetRegionOption::AutoFlushInterval(_)
474 | SetRegionOption::CompactionOutputParallelism(_)
475 | SetRegionOption::SkipWal(_) => (),
476 SetRegionOption::Format(format_str) => {
477 let new_format = format_str.parse::<FormatType>().unwrap();
479 current_options.sst_format = Some(new_format);
480 }
481 SetRegionOption::AppendMode(new_append_mode) => {
482 if *new_append_mode != current_options.append_mode {
483 current_options.append_mode = *new_append_mode;
486 current_options.merge_mode = None;
487 }
488 }
489 SetRegionOption::MaxRowGroupRowCount(new_row_count) => {
490 current_options.max_row_group_row_count = *new_row_count;
491 }
492 SetRegionOption::PreserveRowSequence(new_preserve) => {
493 current_options.preserve_row_sequence = *new_preserve;
494 }
495 }
496 }
497 Some(current_options)
498}
499
500fn metadata_after_alteration(
504 metadata: &RegionMetadata,
505 request: RegionAlterRequest,
506) -> Result<RegionMetadataRef> {
507 let mut builder = RegionMetadataBuilder::from_existing(metadata.clone());
508 builder
509 .alter(request.kind)
510 .context(InvalidRegionRequestSnafu)?
511 .bump_version();
512 let new_meta = builder.build().context(InvalidMetadataSnafu)?;
513
514 Ok(Arc::new(new_meta))
515}
516
517fn set_twcs_options(
518 options: &mut TwcsOptions,
519 default_option: &TwcsOptions,
520 key: &str,
521 value: &str,
522 region_id: RegionId,
523) -> std::result::Result<(), MetadataError> {
524 match key {
525 mito_engine_options::TWCS_TRIGGER_FILE_NUM
526 | mito_engine_options::TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM => {
527 let files = parse_usize_with_default(
528 key,
529 value,
530 default_option.active_window_trigger_file_num,
531 )?;
532 log_option_update(
533 region_id,
534 key,
535 options.active_window_trigger_file_num,
536 files,
537 );
538 options.active_window_trigger_file_num = files;
539 }
540 mito_engine_options::TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER => {
541 let files = parse_usize_with_default(
542 key,
543 value,
544 default_option.active_window_l1_merge_trigger,
545 )?;
546 ensure!(
547 files >= 2,
548 InvalidSetRegionOptionRequestSnafu { key, value }
549 );
550 log_option_update(
551 region_id,
552 key,
553 options.active_window_l1_merge_trigger,
554 files,
555 );
556 options.active_window_l1_merge_trigger = files;
557 }
558 mito_engine_options::TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM => {
559 let files = parse_usize_with_default(
560 key,
561 value,
562 default_option.inactive_window_trigger_file_num,
563 )?;
564 log_option_update(
565 region_id,
566 key,
567 options.inactive_window_trigger_file_num,
568 files,
569 );
570 options.inactive_window_trigger_file_num = files;
571 }
572 mito_engine_options::TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER => {
573 let files = parse_usize_with_default(
574 key,
575 value,
576 default_option.inactive_window_l1_merge_trigger,
577 )?;
578 ensure!(
579 files >= 2,
580 InvalidSetRegionOptionRequestSnafu { key, value }
581 );
582 log_option_update(
583 region_id,
584 key,
585 options.inactive_window_l1_merge_trigger,
586 files,
587 );
588 options.inactive_window_l1_merge_trigger = files;
589 }
590 mito_engine_options::TWCS_MAX_OUTPUT_FILE_SIZE => {
591 let size = if value.is_empty() {
592 default_option.max_output_file_size
593 } else {
594 Some(
595 ReadableSize::from_str(value)
596 .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?,
597 )
598 };
599 log_option_update(region_id, key, options.max_output_file_size, size);
600 options.max_output_file_size = size;
601 }
602 mito_engine_options::TWCS_TIME_WINDOW => {
603 let window = if value.is_empty() {
604 default_option.time_window
605 } else {
606 Some(
607 humantime::parse_duration(value)
608 .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?,
609 )
610 };
611 log_option_update(region_id, key, options.time_window, window);
612 options.time_window = window;
613 }
614 _ => return InvalidSetRegionOptionRequestSnafu { key, value }.fail(),
615 }
616 Ok(())
617}
618
619fn parse_usize_with_default(
620 key: &str,
621 value: &str,
622 default: usize,
623) -> std::result::Result<usize, MetadataError> {
624 if value.is_empty() {
625 Ok(default)
626 } else {
627 value
628 .parse::<usize>()
629 .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())
630 }
631}
632
633fn log_option_update<T: std::fmt::Debug>(
634 region_id: RegionId,
635 option_name: &str,
636 prev_value: T,
637 cur_value: T,
638) {
639 info!(
640 "Update region {}: {}, previous: {:?}, new: {:?}",
641 option_name, region_id, prev_value, cur_value
642 );
643}
644
645fn log_time_index_widening_overflow(
650 region_id: RegionId,
651 version: &VersionRef,
652 request: &RegionAlterRequest,
653) {
654 let AlterKind::ModifyColumnTypes { columns } = &request.kind else {
655 return;
656 };
657 let time_index = version.metadata.time_index_column();
658 for column in columns {
659 if column.column_name != time_index.column_schema.name {
660 continue;
661 }
662 let Some(target_unit) = column.target_type.as_timestamp().map(|t| t.unit()) else {
665 continue;
666 };
667 for level in version.ssts.levels() {
668 for file in level.files.values() {
669 let (start, end) = file.time_range();
670 if start.convert_to(target_unit).is_none() || end.convert_to(target_unit).is_none()
671 {
672 error!(
673 "Time index widening for region {} overflows file {}: widening column \
674 '{}' to {:?}, but data spans [{}, {}] beyond the target unit's i64 \
675 range; overflowing values read back as NULL",
676 region_id,
677 file.file_id(),
678 column.column_name,
679 target_unit,
680 start.to_iso8601_string(),
681 end.to_iso8601_string(),
682 );
683 return;
684 }
685 }
686 }
687 }
688}
689
690fn need_change_index(kind: &AlterKind) -> bool {
692 match kind {
693 AlterKind::SetIndexes { options: _ } => true,
696 _ => false,
700 }
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 #[test]
708 fn test_set_twcs_window_trigger_options() {
709 let mut options = TwcsOptions::default();
710 let defaults = options.clone();
711 let region_id = RegionId::new(1, 1);
712
713 set_twcs_options(
714 &mut options,
715 &defaults,
716 "compaction.twcs.active_window.trigger_file_num",
717 "8",
718 region_id,
719 )
720 .unwrap();
721 set_twcs_options(
722 &mut options,
723 &defaults,
724 "compaction.twcs.active_window.l1_merge_trigger",
725 "16",
726 region_id,
727 )
728 .unwrap();
729 set_twcs_options(
730 &mut options,
731 &defaults,
732 "compaction.twcs.inactive_window.trigger_file_num",
733 "3",
734 region_id,
735 )
736 .unwrap();
737 set_twcs_options(
738 &mut options,
739 &defaults,
740 "compaction.twcs.inactive_window.l1_merge_trigger",
741 "12",
742 region_id,
743 )
744 .unwrap();
745 assert_eq!(8, options.active_window_trigger_file_num);
746 assert_eq!(16, options.active_window_l1_merge_trigger);
747 assert_eq!(3, options.inactive_window_trigger_file_num);
748 assert_eq!(12, options.inactive_window_l1_merge_trigger);
749
750 set_twcs_options(
751 &mut options,
752 &defaults,
753 "compaction.twcs.active_window.trigger_file_num",
754 "",
755 region_id,
756 )
757 .unwrap();
758 set_twcs_options(
759 &mut options,
760 &defaults,
761 "compaction.twcs.active_window.l1_merge_trigger",
762 "",
763 region_id,
764 )
765 .unwrap();
766 set_twcs_options(
767 &mut options,
768 &defaults,
769 "compaction.twcs.inactive_window.trigger_file_num",
770 "",
771 region_id,
772 )
773 .unwrap();
774 set_twcs_options(
775 &mut options,
776 &defaults,
777 "compaction.twcs.inactive_window.l1_merge_trigger",
778 "",
779 region_id,
780 )
781 .unwrap();
782 assert_eq!(defaults, options);
783 }
784
785 #[test]
786 fn test_set_twcs_window_trigger_accepts_one() {
787 let defaults = TwcsOptions::default();
788 for key in [
789 "compaction.twcs.trigger_file_num",
790 "compaction.twcs.active_window.trigger_file_num",
791 "compaction.twcs.inactive_window.trigger_file_num",
792 ] {
793 let mut options = defaults.clone();
794 assert!(
795 set_twcs_options(&mut options, &defaults, key, "1", RegionId::new(1, 1)).is_ok(),
796 "{key}"
797 );
798 }
799 }
800
801 #[test]
802 fn test_set_twcs_l1_merge_triggers_reject_one() {
803 let defaults = TwcsOptions::default();
804 for key in [
805 "compaction.twcs.active_window.l1_merge_trigger",
806 "compaction.twcs.inactive_window.l1_merge_trigger",
807 ] {
808 let mut options = TwcsOptions::default();
809 assert!(
810 set_twcs_options(&mut options, &defaults, key, "1", RegionId::new(1, 1)).is_err(),
811 "{key}"
812 );
813 }
814 }
815
816 #[test]
817 fn test_new_region_options_with_idempotent_append_mode() {
818 let current_options = RegionOptions::default();
819 let kind = AlterKind::SetRegionOptions {
820 options: vec![
821 SetRegionOption::AppendMode(false),
822 SetRegionOption::MaxRowGroupRowCount(Some(1024)),
823 ],
824 };
825
826 let new_options = new_region_options_on_empty_memtable(¤t_options, &kind).unwrap();
827 assert!(!new_options.append_mode);
828 assert_eq!(Some(1024), new_options.max_row_group_row_count);
829 }
830}