mirror of
https://github.com/GreptimeTeam/greptimedb.git
synced 2026-10-19 18:32:33 +00:00
feat(mito2): introduce TWCS active window compaction (#9011)
* feat(mito2): support independent TWCS trigger_file_num for active and inactive windows Split the single TWCS trigger_file_num into per-window-state thresholds: the active window keeps the existing trigger (default 4, legacy compaction.twcs.trigger_file_num stays a compatible alias), while inactive windows use a new trigger (default 2). Inactive windows additionally fall back from balanced L0-only/L1-only candidates to a progress-making unbalanced mixed candidate so historical windows can converge; the active window retains the row/byte balance guards. Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): bound inactive TWCS window convergence by rewrite budget Inactive windows that cannot compact within one level previously either stayed stuck (a threshold-qualified but unbalanced level returned no candidate without trying any fallback) or fell back to a mixed merge with no balance checks at all, which could rewrite a huge compacted file to absorb tiny fresh files. Inactive windows now converge progressively: threshold-qualified balanced picks, sub-threshold balanced single-level picks, a mixed merge whose total rewrite must fit in the output file budget, and finally an L0-only merge without balance checks. Windows that qualify for none of these are left uncompacted, bounding write amplification. Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * test(mito2): cover TWCS window trigger options in alter_table_options sqlness case Exercise SET/UNSET of compaction.twcs.active_window.trigger_file_num and compaction.twcs.inactive_window.trigger_file_num end to end, including that setting the canonical active key removes the legacy compaction.twcs.trigger_file_num alias from the table options. Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): derive TWCS active window from the max-sequence file The active window was determined by the max event-time window among level-0 files. Between an L0 compaction removing its inputs and the next flush landing, level 0 is empty, so the active window transiently became None and every window fell back to the inactive rules - triggering full-window convergence merges during ongoing ingestion whose outputs are then superseded by new data. Flush and compaction outputs both inherit the max input sequence, so the file with the highest sequence across all levels always tracks the most recent write. Use its window as the active window, falling back to the previous L0-based rule when no file carries a sequence (legacy files). The new helper deliberately computes window keys with the assign_to_windows convention (truncate to seconds, then align up), because the result is compared against window keys produced there; the older ceil-based helper is kept unchanged for the legacy fallback path. Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * feat(mito2): add active-window L1 compaction safety trigger Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * test(compat): cover TWCS active window options Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): align TWCS window trigger validation Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): resolve database TWCS trigger aliases Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): prioritize newer compaction windows Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): repick serial compaction outputs Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * feat(mito2): configure inactive-window L1 trigger Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * feat(mito2): prioritize TWCS compaction candidates Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(meta): preserve TWCS trigger downgrade compatibility Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(sql): distinguish invalid database option values Separate database option key and value validation so recognized keys report the invalid value and its constraint. Add parser coverage for invalid, valid, and unknown options. Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * docs(meta): explain TWCS legacy key compatibility Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * refactor(mito2): clarify active window trigger field Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(options): normalize TWCS trigger aliases Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): ignore ineligible files for active window Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): mark explicit TWCS options as overrides Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(mito2): normalize zero compaction output size Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * fix(options): validate database TWCS trigger values Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> * style(store-api): collapse TWCS alias conflict condition Signed-off-by: Lei, HUANG <ratuthomm@gmail.com> --------- Signed-off-by: Lei, HUANG <ratuthomm@gmail.com>
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
//! Option keys for the mito engine.
|
||||
//! We define them in this mod so the create parser can use it to validate table options.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Option key for all WAL options.
|
||||
pub use common_wal::options::WAL_OPTIONS_KEY;
|
||||
/// Option key for append mode.
|
||||
@@ -37,6 +39,18 @@ pub const COMPACTION_OVERRIDE: &str = "compaction.override";
|
||||
pub const COMPACTION_TYPE_TWCS: &str = "twcs";
|
||||
/// Option key for twcs min file num to trigger a compaction.
|
||||
pub const TWCS_TRIGGER_FILE_NUM: &str = "compaction.twcs.trigger_file_num";
|
||||
/// Option key for twcs min file num to trigger compaction in the active window.
|
||||
pub const TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM: &str =
|
||||
"compaction.twcs.active_window.trigger_file_num";
|
||||
/// Option key for the active-window L1 safety compaction threshold.
|
||||
pub const TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER: &str =
|
||||
"compaction.twcs.active_window.l1_merge_trigger";
|
||||
/// Option key for twcs min file num to trigger compaction in an inactive window.
|
||||
pub const TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM: &str =
|
||||
"compaction.twcs.inactive_window.trigger_file_num";
|
||||
/// Option key for the inactive-window L1 compaction threshold.
|
||||
pub const TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER: &str =
|
||||
"compaction.twcs.inactive_window.l1_merge_trigger";
|
||||
/// Option key for twcs max output file size.
|
||||
pub const TWCS_MAX_OUTPUT_FILE_SIZE: &str = "compaction.twcs.max_output_file_size";
|
||||
/// Option key for twcs time window.
|
||||
@@ -76,6 +90,36 @@ pub const MAX_ROW_GROUP_ROW_COUNT_LIMIT: usize = 10 * 1024 * 1024;
|
||||
pub const PRESERVE_ROW_SEQUENCE: &str = "preserve_row_sequence";
|
||||
// Note: Adding new options here should also check if this option should be removed in [metric_engine::engine::create::region_options_for_metadata_region].
|
||||
|
||||
/// Conflicting values supplied through the legacy and canonical TWCS trigger options.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct TwcsTriggerOptionConflict {
|
||||
/// Value supplied under [`TWCS_TRIGGER_FILE_NUM`].
|
||||
pub legacy_value: String,
|
||||
/// Value supplied under [`TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM`].
|
||||
pub canonical_value: String,
|
||||
}
|
||||
|
||||
/// Normalizes the active-window TWCS trigger option to its legacy storage key.
|
||||
pub fn normalize_twcs_trigger_options(
|
||||
options: &mut HashMap<String, String>,
|
||||
) -> Result<(), TwcsTriggerOptionConflict> {
|
||||
let Some(canonical_value) = options.get(TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM).cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(legacy_value) = options.get(TWCS_TRIGGER_FILE_NUM)
|
||||
&& legacy_value != &canonical_value
|
||||
{
|
||||
return Err(TwcsTriggerOptionConflict {
|
||||
legacy_value: legacy_value.clone(),
|
||||
canonical_value,
|
||||
});
|
||||
}
|
||||
|
||||
options.remove(TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM);
|
||||
options.insert(TWCS_TRIGGER_FILE_NUM.to_string(), canonical_value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns true if the `key` is a valid option key for the mito engine.
|
||||
pub fn is_mito_engine_option_key(key: &str) -> bool {
|
||||
[
|
||||
@@ -85,6 +129,10 @@ pub fn is_mito_engine_option_key(key: &str) -> bool {
|
||||
COMPACTION_TYPE,
|
||||
COMPACTION_OVERRIDE,
|
||||
TWCS_TRIGGER_FILE_NUM,
|
||||
TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
|
||||
TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER,
|
||||
TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM,
|
||||
TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER,
|
||||
TWCS_MAX_OUTPUT_FILE_SIZE,
|
||||
TWCS_TIME_WINDOW,
|
||||
TWCS_REMOTE_COMPACTION,
|
||||
@@ -113,6 +161,8 @@ pub fn is_mito_engine_option_key(key: &str) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -125,6 +175,18 @@ mod tests {
|
||||
assert!(is_mito_engine_option_key(
|
||||
"compaction.twcs.trigger_file_num"
|
||||
));
|
||||
assert!(is_mito_engine_option_key(
|
||||
"compaction.twcs.active_window.trigger_file_num"
|
||||
));
|
||||
assert!(is_mito_engine_option_key(
|
||||
"compaction.twcs.active_window.l1_merge_trigger"
|
||||
));
|
||||
assert!(is_mito_engine_option_key(
|
||||
"compaction.twcs.inactive_window.trigger_file_num"
|
||||
));
|
||||
assert!(is_mito_engine_option_key(
|
||||
"compaction.twcs.inactive_window.l1_merge_trigger"
|
||||
));
|
||||
assert!(is_mito_engine_option_key("compaction.twcs.time_window"));
|
||||
assert!(is_mito_engine_option_key("storage"));
|
||||
assert!(is_mito_engine_option_key(
|
||||
@@ -157,4 +219,44 @@ mod tests {
|
||||
assert!(is_mito_engine_option_key("preserve_row_sequence"));
|
||||
assert!(!is_mito_engine_option_key("foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_twcs_trigger_aliases_to_legacy_key() {
|
||||
let expected = HashMap::from([(TWCS_TRIGGER_FILE_NUM.to_string(), "4".to_string())]);
|
||||
for mut options in [
|
||||
HashMap::from([(TWCS_TRIGGER_FILE_NUM.to_string(), "4".to_string())]),
|
||||
HashMap::from([(
|
||||
TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM.to_string(),
|
||||
"4".to_string(),
|
||||
)]),
|
||||
HashMap::from([
|
||||
(TWCS_TRIGGER_FILE_NUM.to_string(), "4".to_string()),
|
||||
(
|
||||
TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM.to_string(),
|
||||
"4".to_string(),
|
||||
),
|
||||
]),
|
||||
] {
|
||||
normalize_twcs_trigger_options(&mut options).unwrap();
|
||||
assert_eq!(expected, options);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_twcs_trigger_conflicting_aliases() {
|
||||
let mut options = HashMap::from([
|
||||
(TWCS_TRIGGER_FILE_NUM.to_string(), "4".to_string()),
|
||||
(
|
||||
TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM.to_string(),
|
||||
"8".to_string(),
|
||||
),
|
||||
]);
|
||||
let original = options.clone();
|
||||
|
||||
let error = normalize_twcs_trigger_options(&mut options).unwrap_err();
|
||||
|
||||
assert_eq!("4", error.legacy_value);
|
||||
assert_eq!("8", error.canonical_value);
|
||||
assert_eq!(original, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ use crate::metrics;
|
||||
use crate::mito_engine_options::{
|
||||
APPEND_MODE_KEY, AUTO_FLUSH_INTERVAL_KEY, MAX_ROW_GROUP_ROW_COUNT,
|
||||
MAX_ROW_GROUP_ROW_COUNT_LIMIT, PRESERVE_ROW_SEQUENCE, SKIP_WAL_KEY, SST_FORMAT_KEY, TTL_KEY,
|
||||
TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER, TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
|
||||
TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER, TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM,
|
||||
TWCS_MAX_OUTPUT_FILE_SIZE, TWCS_TIME_WINDOW, TWCS_TRIGGER_FILE_NUM, WRITE_BUFFER_SIZE_KEY,
|
||||
};
|
||||
use crate::path_utils::table_dir;
|
||||
@@ -1517,9 +1519,13 @@ impl TryFrom<&PbOption> for SetRegionOption {
|
||||
|
||||
Ok(Self::Ttl(Some(ttl)))
|
||||
}
|
||||
TWCS_TRIGGER_FILE_NUM | TWCS_MAX_OUTPUT_FILE_SIZE | TWCS_TIME_WINDOW => {
|
||||
Ok(Self::Twsc(key.clone(), value.clone()))
|
||||
}
|
||||
TWCS_TRIGGER_FILE_NUM
|
||||
| TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM
|
||||
| TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER
|
||||
| TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM
|
||||
| TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER
|
||||
| TWCS_MAX_OUTPUT_FILE_SIZE
|
||||
| TWCS_TIME_WINDOW => Ok(Self::Twsc(key.clone(), value.clone())),
|
||||
SST_FORMAT_KEY => Ok(Self::Format(value.clone())),
|
||||
APPEND_MODE_KEY => {
|
||||
let append_mode = value
|
||||
@@ -1572,6 +1578,18 @@ impl From<&UnsetRegionOption> for SetRegionOption {
|
||||
UnsetRegionOption::TwcsTriggerFileNum => {
|
||||
SetRegionOption::Twsc(unset_option.to_string(), String::new())
|
||||
}
|
||||
UnsetRegionOption::TwcsActiveWindowTriggerFileNum => {
|
||||
SetRegionOption::Twsc(unset_option.to_string(), String::new())
|
||||
}
|
||||
UnsetRegionOption::TwcsActiveWindowL1MergeTrigger => {
|
||||
SetRegionOption::Twsc(unset_option.to_string(), String::new())
|
||||
}
|
||||
UnsetRegionOption::TwcsInactiveWindowTriggerFileNum => {
|
||||
SetRegionOption::Twsc(unset_option.to_string(), String::new())
|
||||
}
|
||||
UnsetRegionOption::TwcsInactiveWindowL1MergeTrigger => {
|
||||
SetRegionOption::Twsc(unset_option.to_string(), String::new())
|
||||
}
|
||||
UnsetRegionOption::TwcsMaxOutputFileSize => {
|
||||
SetRegionOption::Twsc(unset_option.to_string(), String::new())
|
||||
}
|
||||
@@ -1594,6 +1612,10 @@ impl TryFrom<&str> for UnsetRegionOption {
|
||||
TTL_KEY => Ok(Self::Ttl),
|
||||
WRITE_BUFFER_SIZE_KEY => Ok(Self::WriteBufferSize),
|
||||
TWCS_TRIGGER_FILE_NUM => Ok(Self::TwcsTriggerFileNum),
|
||||
TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM => Ok(Self::TwcsActiveWindowTriggerFileNum),
|
||||
TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER => Ok(Self::TwcsActiveWindowL1MergeTrigger),
|
||||
TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM => Ok(Self::TwcsInactiveWindowTriggerFileNum),
|
||||
TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER => Ok(Self::TwcsInactiveWindowL1MergeTrigger),
|
||||
TWCS_MAX_OUTPUT_FILE_SIZE => Ok(Self::TwcsMaxOutputFileSize),
|
||||
TWCS_TIME_WINDOW => Ok(Self::TwcsTimeWindow),
|
||||
MAX_ROW_GROUP_ROW_COUNT => Ok(Self::MaxRowGroupRowCount),
|
||||
@@ -1606,12 +1628,16 @@ impl TryFrom<&str> for UnsetRegionOption {
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum UnsetRegionOption {
|
||||
TwcsTriggerFileNum,
|
||||
TwcsActiveWindowTriggerFileNum,
|
||||
TwcsInactiveWindowTriggerFileNum,
|
||||
TwcsInactiveWindowL1MergeTrigger,
|
||||
TwcsMaxOutputFileSize,
|
||||
TwcsTimeWindow,
|
||||
Ttl,
|
||||
MaxRowGroupRowCount,
|
||||
WriteBufferSize,
|
||||
PreserveRowSequence,
|
||||
TwcsActiveWindowL1MergeTrigger,
|
||||
}
|
||||
|
||||
impl UnsetRegionOption {
|
||||
@@ -1620,6 +1646,10 @@ impl UnsetRegionOption {
|
||||
Self::Ttl => TTL_KEY,
|
||||
Self::WriteBufferSize => WRITE_BUFFER_SIZE_KEY,
|
||||
Self::TwcsTriggerFileNum => TWCS_TRIGGER_FILE_NUM,
|
||||
Self::TwcsActiveWindowTriggerFileNum => TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
|
||||
Self::TwcsActiveWindowL1MergeTrigger => TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER,
|
||||
Self::TwcsInactiveWindowTriggerFileNum => TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM,
|
||||
Self::TwcsInactiveWindowL1MergeTrigger => TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER,
|
||||
Self::TwcsMaxOutputFileSize => TWCS_MAX_OUTPUT_FILE_SIZE,
|
||||
Self::TwcsTimeWindow => TWCS_TIME_WINDOW,
|
||||
Self::MaxRowGroupRowCount => MAX_ROW_GROUP_ROW_COUNT,
|
||||
@@ -2063,6 +2093,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_twcs_window_trigger_options_try_from() {
|
||||
for key in [
|
||||
"compaction.twcs.active_window.trigger_file_num",
|
||||
"compaction.twcs.active_window.l1_merge_trigger",
|
||||
"compaction.twcs.inactive_window.trigger_file_num",
|
||||
"compaction.twcs.inactive_window.l1_merge_trigger",
|
||||
] {
|
||||
let option = PbOption {
|
||||
key: key.to_string(),
|
||||
value: "8".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
SetRegionOption::Twsc(key.to_string(), "8".to_string()),
|
||||
SetRegionOption::try_from(&option).unwrap(),
|
||||
"{key}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unset_twcs_window_trigger_options_try_from() {
|
||||
for (key, expected) in [
|
||||
(
|
||||
"compaction.twcs.active_window.trigger_file_num",
|
||||
UnsetRegionOption::TwcsActiveWindowTriggerFileNum,
|
||||
),
|
||||
(
|
||||
"compaction.twcs.active_window.l1_merge_trigger",
|
||||
UnsetRegionOption::TwcsActiveWindowL1MergeTrigger,
|
||||
),
|
||||
(
|
||||
"compaction.twcs.inactive_window.trigger_file_num",
|
||||
UnsetRegionOption::TwcsInactiveWindowTriggerFileNum,
|
||||
),
|
||||
] {
|
||||
assert_eq!(expected, UnsetRegionOption::try_from(key).unwrap());
|
||||
assert_eq!(
|
||||
SetRegionOption::Twsc(key.to_string(), String::new()),
|
||||
SetRegionOption::from(&expected)
|
||||
);
|
||||
}
|
||||
|
||||
let key = "compaction.twcs.inactive_window.l1_merge_trigger";
|
||||
let option = UnsetRegionOption::try_from(key).unwrap();
|
||||
assert_eq!(key, option.to_string());
|
||||
assert_eq!(
|
||||
SetRegionOption::Twsc(key.to_string(), String::new()),
|
||||
SetRegionOption::from(&option)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_proto_alter_request() {
|
||||
RegionAlterRequest::try_from(AlterRequest {
|
||||
|
||||
Reference in New Issue
Block a user