mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-19 10:22:31 +00:00
* feat: allow re-enabling WAL after disabling Signed-off-by: dhruvxvaishnav <dhruvvaishnav687@gmail.com> * test: regenerate skip_wal sqlness result The previous commit changed tests/cases/standalone/common/skip_wal.sql without regenerating the matching .result, so every Sqlness suite failed on the mismatch. tests/cases/distributed/common is a symlink to standalone/common, so the single stale file accounted for all five failing variants. Regenerated from a real run. The recorded output now covers the cases the .sql added: * A table created with skip_wal = 'true' has no real WAL provider, so enabling WAL is refused with Unsupported rather than the previous InvalidArguments. * A RaftEngine-backed table accepts the true -> false transition, and repeating it is a no-op. * SHOW CREATE TABLE reports skip_wal = 'false' after the transition, including after a restart. * A row written while WAL was skipped is absent after restart, while a row written after WAL was restored survives. Verified locally with nothing else running on the machine: sqlness skip_wal passes in both the standalone and distributed environments, and the regenerated file is byte-identical to the output of an independent earlier run. Signed-off-by: dhruvxvaishnav <dhruvvaishnav687@gmail.com> * test: strengthen skip_wal provider coverage Signed-off-by: dhruvxvaishnav <dhruvvaishnav687@gmail.com> * Fix WAL provider handling during re-enable Signed-off-by: dhruvxvaishnav <dhruvvaishnav687@gmail.com> * Refactor WAL provider check Signed-off-by: dhruvxvaishnav <dhruvvaishnav687@gmail.com> --------- Signed-off-by: dhruvxvaishnav <dhruvvaishnav687@gmail.com>
772 lines
29 KiB
Rust
772 lines
29 KiB
Rust
// Copyright 2023 Greptime Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Handling alter related requests.
|
|
|
|
use std::str::FromStr;
|
|
use std::sync::Arc;
|
|
|
|
use common_base::readable_size::ReadableSize;
|
|
use common_telemetry::tracing::warn;
|
|
use common_telemetry::{error, info};
|
|
use humantime_serde::re::humantime;
|
|
use snafu::{ResultExt, ensure};
|
|
use store_api::logstore::LogStore;
|
|
use store_api::logstore::provider::Provider;
|
|
use store_api::metadata::{
|
|
InvalidSetRegionOptionRequestSnafu, MetadataError, RegionMetadata, RegionMetadataBuilder,
|
|
RegionMetadataRef,
|
|
};
|
|
use store_api::mito_engine_options;
|
|
use store_api::mito_engine_options::MAX_ROW_GROUP_ROW_COUNT_LIMIT;
|
|
use store_api::region_request::{AlterKind, RegionAlterRequest, SetRegionOption};
|
|
use store_api::storage::RegionId;
|
|
|
|
use crate::error::{InvalidMetadataSnafu, InvalidRegionRequestSnafu, Result};
|
|
use crate::flush::FlushReason;
|
|
use crate::manifest::action::RegionChange;
|
|
use crate::region::MitoRegionRef;
|
|
use crate::region::options::CompactionOptions::Twcs;
|
|
use crate::region::options::{RegionOptions, TwcsOptions};
|
|
use crate::region::version::VersionRef;
|
|
use crate::request::{DdlRequest, OptionOutputTx, SenderDdlRequest};
|
|
use crate::sst::FormatType;
|
|
use crate::worker::RegionWorkerLoop;
|
|
|
|
impl<S: LogStore> RegionWorkerLoop<S> {
|
|
pub(crate) async fn handle_alter_request(
|
|
&mut self,
|
|
region_id: RegionId,
|
|
request: RegionAlterRequest,
|
|
sender: OptionOutputTx,
|
|
) {
|
|
let requested_skip_wal = skip_wal_value(&request.kind);
|
|
let (region, follower_skip_wal) = match self.regions.writable_non_staging_region(region_id)
|
|
{
|
|
Ok(region) => (region, None),
|
|
Err(_) if requested_skip_wal.is_some() => {
|
|
match self.regions.follower_region(region_id) {
|
|
Ok(region) => (region, requested_skip_wal),
|
|
Err(e) => {
|
|
sender.send(Err(e));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
sender.send(Err(e));
|
|
return;
|
|
}
|
|
};
|
|
|
|
info!("Try to alter region: {}, request: {:?}", region_id, request);
|
|
|
|
// Followers only accept skip-WAL changes, which are in-memory option changes and must
|
|
// never enter the leader path that may flush memtables.
|
|
if let Some(skip_wal) = follower_skip_wal {
|
|
if let Err(e) = validate_skip_wal_change(®ion, skip_wal) {
|
|
sender.send(Err(e).context(InvalidMetadataSnafu));
|
|
return;
|
|
}
|
|
let mut options = region.version().options.clone();
|
|
if options.skip_wal != skip_wal {
|
|
info!(
|
|
"Set skip_wal for follower region: {}, previous: {} new: {}",
|
|
region_id, options.skip_wal, skip_wal
|
|
);
|
|
options.skip_wal = skip_wal;
|
|
region.version_control.alter_options(options);
|
|
}
|
|
sender.send(Ok(0));
|
|
return;
|
|
}
|
|
|
|
// Gets the version before alter.
|
|
let version = region.version();
|
|
|
|
// fast path for memory state changes like options.
|
|
let set_options = match &request.kind {
|
|
AlterKind::SetRegionOptions { options } => options.clone(),
|
|
AlterKind::UnsetRegionOptions { keys } => {
|
|
// Converts the keys to SetRegionOption.
|
|
//
|
|
// It passes an empty string to achieve the purpose of unset
|
|
keys.iter().map(Into::into).collect()
|
|
}
|
|
_ => Vec::new(),
|
|
};
|
|
let mut new_options = None;
|
|
if !set_options.is_empty() {
|
|
match self.handle_alter_region_options_fast(®ion, version.clone(), set_options) {
|
|
Ok(staged_options) => {
|
|
let Some(staged_options) = staged_options else {
|
|
// We don't have options to alter after flush.
|
|
sender.send(Ok(0));
|
|
return;
|
|
};
|
|
new_options = Some(staged_options);
|
|
}
|
|
Err(e) => {
|
|
sender.send(Err(e).context(InvalidMetadataSnafu));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validates request.
|
|
if let Err(e) = request.validate(&version.metadata) {
|
|
// Invalid request.
|
|
sender.send(Err(e).context(InvalidRegionRequestSnafu));
|
|
return;
|
|
}
|
|
|
|
// Checks whether we need to alter the region.
|
|
if !request.need_alter(&version.metadata) {
|
|
warn!(
|
|
"Ignores alter request as it alters nothing, region_id: {}, request: {:?}",
|
|
region_id, request
|
|
);
|
|
sender.send(Ok(0));
|
|
return;
|
|
}
|
|
|
|
// Checks whether we can alter the region directly.
|
|
if !version.memtables.is_empty() {
|
|
// If memtable is not empty, we can't alter it directly and need to flush
|
|
// all memtables first.
|
|
info!("Flush region: {} before alteration", region_id);
|
|
|
|
// Try to submit a flush task.
|
|
let task = self.new_flush_task(®ion, FlushReason::Alter, None, self.config.clone());
|
|
if let Err(e) =
|
|
self.flush_scheduler
|
|
.schedule_flush(region.region_id, ®ion.version_control, task)
|
|
{
|
|
// Unable to flush the region, send error to waiter.
|
|
sender.send(Err(e));
|
|
return;
|
|
}
|
|
|
|
// Safety: We have requested flush.
|
|
self.flush_scheduler
|
|
.add_ddl_request_to_pending(SenderDdlRequest {
|
|
region_id,
|
|
sender,
|
|
request: DdlRequest::Alter(request),
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
info!(
|
|
"Try to alter region {}, version.metadata: {:?}, version.options: {:?}, request: {:?}",
|
|
region_id, version.metadata, version.options, request,
|
|
);
|
|
log_time_index_widening_overflow(region.region_id, &version, &request);
|
|
self.handle_alter_region_with_empty_memtable(region, version, request, new_options, sender);
|
|
}
|
|
|
|
// TODO(yingwen): Optional new options and sst format.
|
|
/// Handles region metadata and format changes when the region memtable is empty.
|
|
fn handle_alter_region_with_empty_memtable(
|
|
&mut self,
|
|
region: MitoRegionRef,
|
|
version: VersionRef,
|
|
request: RegionAlterRequest,
|
|
new_options: Option<RegionOptions>,
|
|
sender: OptionOutputTx,
|
|
) {
|
|
let need_index = need_change_index(&request.kind);
|
|
let new_meta = match metadata_after_alteration(&version.metadata, request) {
|
|
Ok(new_meta) => new_meta,
|
|
Err(e) => {
|
|
sender.send(Err(e));
|
|
return;
|
|
}
|
|
};
|
|
// Persist the metadata to region's manifest.
|
|
let options = new_options.as_ref().unwrap_or(&version.options);
|
|
let change = RegionChange {
|
|
metadata: new_meta,
|
|
sst_format: options.sst_format.unwrap_or_default(),
|
|
append_mode: Some(options.append_mode),
|
|
};
|
|
self.handle_manifest_region_change(region, change, need_index, new_options, sender);
|
|
}
|
|
|
|
/// Handles requests that changes region options, like TTL. It only affects memory state
|
|
/// since changes are persisted in the `DatanodeTableValue` in metasrv.
|
|
///
|
|
/// Options that can be applied without an empty memtable (e.g. TTL,
|
|
/// `preserve_row_sequence`) are applied directly through the fast path.
|
|
/// If another option in the same ALTER requires an empty memtable (e.g.
|
|
/// `append_mode`), only the complete final options are staged and the
|
|
/// existing flush path is followed.
|
|
///
|
|
/// Returns the staged options if they need further alteration.
|
|
fn handle_alter_region_options_fast(
|
|
&mut self,
|
|
region: &MitoRegionRef,
|
|
version: VersionRef,
|
|
options: Vec<SetRegionOption>,
|
|
) -> std::result::Result<Option<RegionOptions>, MetadataError> {
|
|
assert!(!options.is_empty());
|
|
|
|
let mut all_options_altered = true;
|
|
let mut current_options = version.options.clone();
|
|
for option in options.iter().cloned() {
|
|
match option {
|
|
SetRegionOption::WriteBufferSize(new_write_buffer_size) => {
|
|
info!(
|
|
"Update region write_buffer_size: {}, previous: {:?} new: {:?}",
|
|
region.region_id, current_options.write_buffer_size, new_write_buffer_size
|
|
);
|
|
current_options.write_buffer_size = new_write_buffer_size;
|
|
current_options.validate().map_err(|e| {
|
|
store_api::metadata::InvalidRegionRequestSnafu {
|
|
region_id: region.region_id,
|
|
err: e.to_string(),
|
|
}
|
|
.build()
|
|
})?;
|
|
}
|
|
SetRegionOption::Ttl(new_ttl) => {
|
|
info!(
|
|
"Update region ttl: {}, previous: {:?} new: {:?}",
|
|
region.region_id, current_options.ttl, new_ttl
|
|
);
|
|
current_options.ttl = new_ttl;
|
|
}
|
|
SetRegionOption::Twsc(key, value) => {
|
|
let Twcs(options) = &mut current_options.compaction;
|
|
set_twcs_options(
|
|
options,
|
|
&TwcsOptions::default(),
|
|
&key,
|
|
&value,
|
|
region.region_id,
|
|
)?;
|
|
if !value.is_empty() {
|
|
current_options.compaction_override = true;
|
|
}
|
|
}
|
|
SetRegionOption::Format(format_str) => {
|
|
let new_format = format_str.parse::<FormatType>().map_err(|_| {
|
|
store_api::metadata::InvalidRegionRequestSnafu {
|
|
region_id: region.region_id,
|
|
err: format!("Invalid format type: {}", format_str),
|
|
}
|
|
.build()
|
|
})?;
|
|
// If the format is unchanged, we also consider the option is altered.
|
|
if new_format != current_options.sst_format.unwrap_or_default() {
|
|
all_options_altered = false;
|
|
}
|
|
}
|
|
SetRegionOption::AppendMode(new_append_mode) => {
|
|
// If the append mode is unchanged, we consider the option is altered.
|
|
if new_append_mode != current_options.append_mode {
|
|
// Validates: only allow changing from false to true.
|
|
ensure!(
|
|
!current_options.append_mode && new_append_mode,
|
|
store_api::metadata::InvalidRegionRequestSnafu {
|
|
region_id: region.region_id,
|
|
err: "Only allow changing append_mode from false to true",
|
|
}
|
|
);
|
|
// Clear merge_mode since it's incompatible with append_mode.
|
|
current_options.merge_mode = None;
|
|
all_options_altered = false;
|
|
}
|
|
}
|
|
SetRegionOption::AutoFlushInterval(new_interval) => {
|
|
// The flush logic reads the effective interval from the region's
|
|
// current version each cycle, so the change takes effect on the
|
|
// next flush without a memtable flush.
|
|
if new_interval != current_options.auto_flush_interval {
|
|
info!(
|
|
"Update region auto_flush_interval: {}, previous: {:?} new: {:?}",
|
|
region.region_id, current_options.auto_flush_interval, new_interval
|
|
);
|
|
current_options.auto_flush_interval = new_interval;
|
|
}
|
|
}
|
|
SetRegionOption::MaxRowGroupRowCount(new_row_count) => {
|
|
if let Some(row_count) = new_row_count {
|
|
ensure!(
|
|
row_count > 0 && row_count <= MAX_ROW_GROUP_ROW_COUNT_LIMIT,
|
|
store_api::metadata::InvalidRegionRequestSnafu {
|
|
region_id: region.region_id,
|
|
err: format!(
|
|
"max_row_group_row_count must be in (0, \
|
|
{MAX_ROW_GROUP_ROW_COUNT_LIMIT}], got {row_count}"
|
|
),
|
|
}
|
|
);
|
|
}
|
|
if new_row_count != current_options.max_row_group_row_count {
|
|
all_options_altered = false;
|
|
}
|
|
}
|
|
SetRegionOption::PreserveRowSequence(new_preserve) => {
|
|
if new_preserve != current_options.preserve_row_sequence {
|
|
info!(
|
|
"Update region preserve_row_sequence: {}, previous: {:?} new: {:?}",
|
|
region.region_id, current_options.preserve_row_sequence, new_preserve
|
|
);
|
|
current_options.preserve_row_sequence = new_preserve;
|
|
}
|
|
}
|
|
SetRegionOption::SkipWal(skip_wal) => {
|
|
validate_skip_wal_change(region, skip_wal)?;
|
|
if current_options.skip_wal != skip_wal {
|
|
info!(
|
|
"Set skip_wal for region: {}, previous: {} new: {}",
|
|
region.region_id, current_options.skip_wal, skip_wal
|
|
);
|
|
current_options.skip_wal = skip_wal;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let kind = AlterKind::SetRegionOptions { options };
|
|
// Validate the complete final options. The loop above validates
|
|
// per-option, but a combined ALTER may flip append_mode and toggle
|
|
// preserve_row_sequence in the same request, which is only valid when
|
|
// viewed together (order-independent). Validating the staged outcome
|
|
// also guarantees that a preserve-only toggle applied via the fast path
|
|
// below cannot create an invalid state (preserve requires append_mode).
|
|
let candidate = new_region_options_on_empty_memtable(¤t_options, &kind)
|
|
.unwrap_or_else(|| current_options.clone());
|
|
candidate.validate().map_err(|e| {
|
|
store_api::metadata::InvalidRegionRequestSnafu {
|
|
region_id: region.region_id,
|
|
err: e.to_string(),
|
|
}
|
|
.build()
|
|
})?;
|
|
if all_options_altered {
|
|
region.version_control.alter_options(candidate);
|
|
Ok(None)
|
|
} else {
|
|
// Some options require an empty memtable (e.g. append_mode,
|
|
// sst_format, or max_row_group_row_count).
|
|
Ok(Some(candidate))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn skip_wal_value(kind: &AlterKind) -> Option<bool> {
|
|
let AlterKind::SetRegionOptions { options } = kind else {
|
|
return None;
|
|
};
|
|
let [SetRegionOption::SkipWal(skip_wal)] = options.as_slice() else {
|
|
return None;
|
|
};
|
|
Some(*skip_wal)
|
|
}
|
|
|
|
fn validate_skip_wal_change(
|
|
region: &MitoRegionRef,
|
|
skip_wal: bool,
|
|
) -> std::result::Result<(), MetadataError> {
|
|
ensure!(
|
|
skip_wal || !matches!(®ion.provider, Provider::Noop),
|
|
store_api::metadata::InvalidRegionRequestSnafu {
|
|
region_id: region.region_id,
|
|
err: "cannot enable WAL because the region uses the Noop WAL provider".to_string(),
|
|
}
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns the new region options if there are updates to the options.
|
|
fn new_region_options_on_empty_memtable(
|
|
current_options: &RegionOptions,
|
|
kind: &AlterKind,
|
|
) -> Option<RegionOptions> {
|
|
let options = match kind {
|
|
AlterKind::SetRegionOptions { options } => options.clone(),
|
|
AlterKind::UnsetRegionOptions { keys } => keys.iter().map(Into::into).collect(),
|
|
_ => return None,
|
|
};
|
|
|
|
if options.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
let mut current_options = current_options.clone();
|
|
for option in &options {
|
|
match option {
|
|
SetRegionOption::WriteBufferSize(_)
|
|
| SetRegionOption::Ttl(_)
|
|
| SetRegionOption::Twsc(_, _)
|
|
| SetRegionOption::AutoFlushInterval(_)
|
|
| SetRegionOption::SkipWal(_) => (),
|
|
SetRegionOption::Format(format_str) => {
|
|
// Safety: handle_alter_region_options_fast() has validated this.
|
|
let new_format = format_str.parse::<FormatType>().unwrap();
|
|
current_options.sst_format = Some(new_format);
|
|
}
|
|
SetRegionOption::AppendMode(new_append_mode) => {
|
|
if *new_append_mode != current_options.append_mode {
|
|
// Safety: handle_alter_region_options_fast() has validated that the only
|
|
// supported transition is from false to true.
|
|
current_options.append_mode = *new_append_mode;
|
|
current_options.merge_mode = None;
|
|
}
|
|
}
|
|
SetRegionOption::MaxRowGroupRowCount(new_row_count) => {
|
|
current_options.max_row_group_row_count = *new_row_count;
|
|
}
|
|
SetRegionOption::PreserveRowSequence(new_preserve) => {
|
|
current_options.preserve_row_sequence = *new_preserve;
|
|
}
|
|
}
|
|
}
|
|
Some(current_options)
|
|
}
|
|
|
|
/// Creates a metadata after applying the alter `request` to the old `metadata`.
|
|
///
|
|
/// Returns an error if the `request` is invalid.
|
|
fn metadata_after_alteration(
|
|
metadata: &RegionMetadata,
|
|
request: RegionAlterRequest,
|
|
) -> Result<RegionMetadataRef> {
|
|
let mut builder = RegionMetadataBuilder::from_existing(metadata.clone());
|
|
builder
|
|
.alter(request.kind)
|
|
.context(InvalidRegionRequestSnafu)?
|
|
.bump_version();
|
|
let new_meta = builder.build().context(InvalidMetadataSnafu)?;
|
|
|
|
Ok(Arc::new(new_meta))
|
|
}
|
|
|
|
fn set_twcs_options(
|
|
options: &mut TwcsOptions,
|
|
default_option: &TwcsOptions,
|
|
key: &str,
|
|
value: &str,
|
|
region_id: RegionId,
|
|
) -> std::result::Result<(), MetadataError> {
|
|
match key {
|
|
mito_engine_options::TWCS_TRIGGER_FILE_NUM
|
|
| mito_engine_options::TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM => {
|
|
let files = parse_usize_with_default(
|
|
key,
|
|
value,
|
|
default_option.active_window_trigger_file_num,
|
|
)?;
|
|
log_option_update(
|
|
region_id,
|
|
key,
|
|
options.active_window_trigger_file_num,
|
|
files,
|
|
);
|
|
options.active_window_trigger_file_num = files;
|
|
}
|
|
mito_engine_options::TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER => {
|
|
let files = parse_usize_with_default(
|
|
key,
|
|
value,
|
|
default_option.active_window_l1_merge_trigger,
|
|
)?;
|
|
ensure!(
|
|
files >= 2,
|
|
InvalidSetRegionOptionRequestSnafu { key, value }
|
|
);
|
|
log_option_update(
|
|
region_id,
|
|
key,
|
|
options.active_window_l1_merge_trigger,
|
|
files,
|
|
);
|
|
options.active_window_l1_merge_trigger = files;
|
|
}
|
|
mito_engine_options::TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM => {
|
|
let files = parse_usize_with_default(
|
|
key,
|
|
value,
|
|
default_option.inactive_window_trigger_file_num,
|
|
)?;
|
|
log_option_update(
|
|
region_id,
|
|
key,
|
|
options.inactive_window_trigger_file_num,
|
|
files,
|
|
);
|
|
options.inactive_window_trigger_file_num = files;
|
|
}
|
|
mito_engine_options::TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER => {
|
|
let files = parse_usize_with_default(
|
|
key,
|
|
value,
|
|
default_option.inactive_window_l1_merge_trigger,
|
|
)?;
|
|
ensure!(
|
|
files >= 2,
|
|
InvalidSetRegionOptionRequestSnafu { key, value }
|
|
);
|
|
log_option_update(
|
|
region_id,
|
|
key,
|
|
options.inactive_window_l1_merge_trigger,
|
|
files,
|
|
);
|
|
options.inactive_window_l1_merge_trigger = files;
|
|
}
|
|
mito_engine_options::TWCS_MAX_OUTPUT_FILE_SIZE => {
|
|
let size = if value.is_empty() {
|
|
default_option.max_output_file_size
|
|
} else {
|
|
Some(
|
|
ReadableSize::from_str(value)
|
|
.map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?,
|
|
)
|
|
};
|
|
log_option_update(region_id, key, options.max_output_file_size, size);
|
|
options.max_output_file_size = size;
|
|
}
|
|
mito_engine_options::TWCS_TIME_WINDOW => {
|
|
let window = if value.is_empty() {
|
|
default_option.time_window
|
|
} else {
|
|
Some(
|
|
humantime::parse_duration(value)
|
|
.map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?,
|
|
)
|
|
};
|
|
log_option_update(region_id, key, options.time_window, window);
|
|
options.time_window = window;
|
|
}
|
|
_ => return InvalidSetRegionOptionRequestSnafu { key, value }.fail(),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_usize_with_default(
|
|
key: &str,
|
|
value: &str,
|
|
default: usize,
|
|
) -> std::result::Result<usize, MetadataError> {
|
|
if value.is_empty() {
|
|
Ok(default)
|
|
} else {
|
|
value
|
|
.parse::<usize>()
|
|
.map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())
|
|
}
|
|
}
|
|
|
|
fn log_option_update<T: std::fmt::Debug>(
|
|
region_id: RegionId,
|
|
option_name: &str,
|
|
prev_value: T,
|
|
cur_value: T,
|
|
) {
|
|
info!(
|
|
"Update region {}: {}, previous: {:?}, new: {:?}",
|
|
option_name, region_id, prev_value, cur_value
|
|
);
|
|
}
|
|
|
|
/// Logs a time index unit widening whose rescaled values would overflow the
|
|
/// target unit's `i64` range in existing data. Memtables are flushed before
|
|
/// an alter, so each SST's time range bounds its values; overflowing values
|
|
/// cast to NULL on read, which is deemed acceptable.
|
|
fn log_time_index_widening_overflow(
|
|
region_id: RegionId,
|
|
version: &VersionRef,
|
|
request: &RegionAlterRequest,
|
|
) {
|
|
let AlterKind::ModifyColumnTypes { columns } = &request.kind else {
|
|
return;
|
|
};
|
|
let time_index = version.metadata.time_index_column();
|
|
for column in columns {
|
|
if column.column_name != time_index.column_schema.name {
|
|
continue;
|
|
}
|
|
// Non-timestamp targets and narrowing changes are rejected by
|
|
// `RegionAlterRequest::validate` before the request reaches here.
|
|
let Some(target_unit) = column.target_type.as_timestamp().map(|t| t.unit()) else {
|
|
continue;
|
|
};
|
|
for level in version.ssts.levels() {
|
|
for file in level.files.values() {
|
|
let (start, end) = file.time_range();
|
|
if start.convert_to(target_unit).is_none() || end.convert_to(target_unit).is_none()
|
|
{
|
|
error!(
|
|
"Time index widening for region {} overflows file {}: widening column \
|
|
'{}' to {:?}, but data spans [{}, {}] beyond the target unit's i64 \
|
|
range; overflowing values read back as NULL",
|
|
region_id,
|
|
file.file_id(),
|
|
column.column_name,
|
|
target_unit,
|
|
start.to_iso8601_string(),
|
|
end.to_iso8601_string(),
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Used to determine whether we can build index directly after schema change.
|
|
fn need_change_index(kind: &AlterKind) -> bool {
|
|
match kind {
|
|
// `SetIndexes` is a fast-path operation because it can build indexes for existing SSTs
|
|
// in the background, without needing to wait for a flush or compaction cycle.
|
|
AlterKind::SetIndexes { options: _ } => true,
|
|
// For AddColumns, DropColumns, UnsetIndexes and ModifyColumnTypes, we don't treat them as index changes.
|
|
// Index files still need to be rebuilt after schema changes,
|
|
// but this will happen automatically during flush or compaction.
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_set_twcs_window_trigger_options() {
|
|
let mut options = TwcsOptions::default();
|
|
let defaults = options.clone();
|
|
let region_id = RegionId::new(1, 1);
|
|
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.active_window.trigger_file_num",
|
|
"8",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.active_window.l1_merge_trigger",
|
|
"16",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.inactive_window.trigger_file_num",
|
|
"3",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.inactive_window.l1_merge_trigger",
|
|
"12",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(8, options.active_window_trigger_file_num);
|
|
assert_eq!(16, options.active_window_l1_merge_trigger);
|
|
assert_eq!(3, options.inactive_window_trigger_file_num);
|
|
assert_eq!(12, options.inactive_window_l1_merge_trigger);
|
|
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.active_window.trigger_file_num",
|
|
"",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.active_window.l1_merge_trigger",
|
|
"",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.inactive_window.trigger_file_num",
|
|
"",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
set_twcs_options(
|
|
&mut options,
|
|
&defaults,
|
|
"compaction.twcs.inactive_window.l1_merge_trigger",
|
|
"",
|
|
region_id,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(defaults, options);
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_twcs_window_trigger_accepts_one() {
|
|
let defaults = TwcsOptions::default();
|
|
for key in [
|
|
"compaction.twcs.trigger_file_num",
|
|
"compaction.twcs.active_window.trigger_file_num",
|
|
"compaction.twcs.inactive_window.trigger_file_num",
|
|
] {
|
|
let mut options = defaults.clone();
|
|
assert!(
|
|
set_twcs_options(&mut options, &defaults, key, "1", RegionId::new(1, 1)).is_ok(),
|
|
"{key}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_set_twcs_l1_merge_triggers_reject_one() {
|
|
let defaults = TwcsOptions::default();
|
|
for key in [
|
|
"compaction.twcs.active_window.l1_merge_trigger",
|
|
"compaction.twcs.inactive_window.l1_merge_trigger",
|
|
] {
|
|
let mut options = TwcsOptions::default();
|
|
assert!(
|
|
set_twcs_options(&mut options, &defaults, key, "1", RegionId::new(1, 1)).is_err(),
|
|
"{key}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_new_region_options_with_idempotent_append_mode() {
|
|
let current_options = RegionOptions::default();
|
|
let kind = AlterKind::SetRegionOptions {
|
|
options: vec![
|
|
SetRegionOption::AppendMode(false),
|
|
SetRegionOption::MaxRowGroupRowCount(Some(1024)),
|
|
],
|
|
};
|
|
|
|
let new_options = new_region_options_on_empty_memtable(¤t_options, &kind).unwrap();
|
|
assert!(!new_options.append_mode);
|
|
assert_eq!(Some(1024), new_options.max_row_group_row_count);
|
|
}
|
|
}
|