Skip to content

Commit df4b5ac

Browse files
committed
Address all review comments
1 parent 5d9ee9a commit df4b5ac

3 files changed

Lines changed: 46 additions & 43 deletions

File tree

quickwit/quickwit-cli/src/tool.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,8 @@ pub fn build_tool_command() -> Command {
169169
.display_order(10)
170170
.about("Merges mature splits across all indexes and nodes.")
171171
.long_about(
172-
"Scans all indexes for mature Published splits, groups them by day \
173-
and partition, and merges groups with more than 5 small splits. \
174-
Runs once and exits."
172+
"Scans indexes for merge opportunities in mature Published splits. Considers \
173+
opportunities across all origin nodes and sources. Runs once and exits."
175174
)
176175
.args(&[
177176
arg!(--"dry-run"
@@ -493,6 +492,13 @@ impl ToolCliCommand {
493492
.map(|s| s.split(',').map(|p| p.trim().to_string()).collect())
494493
.unwrap_or(defaults.index_id_patterns);
495494
let serve_metrics = matches.get_flag("metrics");
495+
496+
if max_concurrent_merges == 0 {
497+
bail!("`max-concurrent-merges` must be greater than or equal to 1.");
498+
}
499+
if index_parallelism == 0 {
500+
bail!("`index-parallelism` must be greater than or equal to 1.");
501+
}
496502
Ok(Self::MatureMerge(MatureMergeArgs {
497503
config_uri,
498504
serve_metrics,

quickwit/quickwit-indexing/src/mature_merge.rs

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
//! Mature merge: merge fully-mature splits across all nodes of an index.
16-
//!
17-
//! Unlike the standard `MergePipeline`, this module:
18-
//! - Considers only *mature* splits (i.e., splits that are past their maturation period).
19-
//! - Has no node-id restriction — it can merge splits originally created on different nodes.
20-
//! - Is driven by a simple one-shot batch run, not a reactive actor loop.
21-
//!
22-
//! Entry point: [`merge_mature_all_indexes`].
23-
2415
use std::sync::Arc;
2516

2617
use anyhow::Context;
@@ -49,7 +40,7 @@ use crate::actors::{
4940
MergeExecutor, MergePermit, MergeSplitDownloader, Packager, Publisher, PublisherType, Uploader,
5041
UploaderType,
5142
};
52-
use crate::mature_merge_plan::plan_merge_operations_for_index;
43+
use crate::mature_merge_plan::{MATURITY_BUFFER, plan_merge_operations_for_index};
5344
use crate::merge_policy::{MergeOperation, MergeTask, NopMergePolicy};
5445
use crate::split_store::{IndexingSplitCache, IndexingSplitStore};
5546

@@ -106,17 +97,18 @@ struct IndexMergeSummary {
10697
outcome: IndexMergeOutcome,
10798
}
10899

109-
/// Fetches all published splits for the given index from the metastore (no node-id filter,
110-
/// no immature filter) and calls [`plan_merge_operations_for_index`].
100+
/// Fetches all published splits for the given index from the metastore (no
101+
/// node-id filter) and calls [`plan_merge_operations_for_index`].
111102
async fn fetch_splits_and_plan(
112103
index_metadata: &IndexMetadata,
113104
metastore: &MetastoreServiceClient,
114105
now: OffsetDateTime,
115106
config: &MatureMergeConfig,
116107
) -> anyhow::Result<Vec<MergeOperation>> {
117108
let index_uid = index_metadata.index_uid.clone();
118-
let list_splits_query =
119-
ListSplitsQuery::for_index(index_uid).with_split_state(SplitState::Published);
109+
let list_splits_query = ListSplitsQuery::for_index(index_uid)
110+
.with_split_state(SplitState::Published)
111+
.retain_mature(now - MATURITY_BUFFER);
120112
let list_splits_request = ListSplitsRequest::try_from_list_splits_query(&list_splits_query)?;
121113
let splits_stream = metastore.list_splits(list_splits_request).await?;
122114
let splits = splits_stream.collect_splits_metadata().await?;
@@ -297,8 +289,8 @@ async fn merge_mature_single_index(
297289
data_dir_path: &std::path::Path,
298290
config: &MatureMergeConfig,
299291
node_id: NodeId,
300-
now: OffsetDateTime,
301292
) -> anyhow::Result<IndexMergeSummary> {
293+
let now = OffsetDateTime::now_utc();
302294
let index_id = index_metadata.index_config.index_id.clone();
303295
let operations = fetch_splits_and_plan(&index_metadata, metastore, now, config).await?;
304296
let num_merges_planned = operations.len();
@@ -467,8 +459,6 @@ pub async fn merge_mature_all_indexes(
467459
config: MatureMergeConfig,
468460
node_id: NodeId,
469461
) -> anyhow::Result<()> {
470-
let now = OffsetDateTime::now_utc();
471-
472462
let indexes_metadata = metastore
473463
.list_indexes_metadata(ListIndexesMetadataRequest {
474464
index_id_patterns: config.index_id_patterns.clone(),
@@ -502,7 +492,6 @@ pub async fn merge_mature_all_indexes(
502492
data_dir_path,
503493
config_ref,
504494
node_id,
505-
now,
506495
)
507496
.await
508497
}

quickwit/quickwit-indexing/src/mature_merge_plan.rs

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
//! Planning logic for mature-merge: pure, synchronous functions that decide
16-
//! which splits to merge and how to group them. No I/O, no actors.
17-
//!
18-
//! The main entry point is [`plan_merge_operations_for_index`].
19-
2015
use std::collections::HashMap;
2116
use std::time::Duration;
2217

@@ -27,6 +22,12 @@ use time::OffsetDateTime;
2722
use crate::mature_merge::MatureMergeConfig;
2823
use crate::merge_policy::MergeOperation;
2924

25+
pub const SECS_PER_DAY: i64 = 60 * 60 * 24;
26+
27+
/// Wait a couple of hours after the split got mature to be extra sure no merge
28+
/// process is still running on it.
29+
pub const MATURITY_BUFFER: Duration = Duration::from_hours(6);
30+
3031
/// Computes the earliest UTC-day midnight (seconds since epoch) that is safe to merge,
3132
/// given the index's retention policy and the current time.
3233
fn retention_safety_cutoff_secs(
@@ -43,7 +44,7 @@ fn retention_safety_cutoff_secs(
4344
}
4445
let cutoff_raw = now_secs - period.as_secs() as i64 + retention_safety_buffer.as_secs() as i64;
4546
// Round up to the next day boundary so we never partially exclude a day bucket.
46-
Some((cutoff_raw / 86400 + 1) * 86400)
47+
Some((cutoff_raw / SECS_PER_DAY + 1) * SECS_PER_DAY)
4748
}
4849

4950
/// Converts a single day-bucket group of eligible splits into one or more balanced
@@ -55,12 +56,6 @@ fn plan_operations_for_group(
5556
if group_splits.len() < config.min_merge_group_size {
5657
return Vec::new();
5758
}
58-
if !group_splits
59-
.iter()
60-
.all(|s| s.num_docs < config.input_split_max_num_docs)
61-
{
62-
return Vec::new();
63-
}
6459
// Sort ascending by end time so each sub-operation covers the most compact range.
6560
group_splits.sort_by_key(|s| s.time_range.as_ref().map(|r| *r.end()).unwrap_or(0));
6661

@@ -92,6 +87,11 @@ fn plan_operations_for_group(
9287
/// fall on the same UTC day (i.e., the split does not span midnight).
9388
/// - Immature splits are excluded.
9489
/// - Splits whose `time_range.end()` falls within the retention safety buffer are excluded.
90+
///
91+
/// Important: This plan merges splits accross sources. It can be problematic if
92+
/// the IndexingSettings are different (e.g different maturation period), which
93+
/// was made possible on Kafka sources by specifying an override in the
94+
/// client_params.
9595
pub fn plan_merge_operations_for_index(
9696
index_config: &IndexConfig,
9797
splits: Vec<SplitMetadata>,
@@ -107,7 +107,12 @@ pub fn plan_merge_operations_for_index(
107107

108108
for split in splits {
109109
// Only splits that have been mature for a while
110-
if !split.is_mature(now - Duration::from_hours(6)) {
110+
if !split.is_mature(now - MATURITY_BUFFER) {
111+
continue;
112+
}
113+
114+
// Enforce the max size for splits to be considered for merging.
115+
if split.num_docs > config.input_split_max_num_docs {
111116
continue;
112117
}
113118

@@ -116,8 +121,8 @@ pub fn plan_merge_operations_for_index(
116121
continue;
117122
};
118123

119-
let start_day = time_range.start() / 86400;
120-
let end_day = time_range.end() / 86400;
124+
let start_day = time_range.start() / SECS_PER_DAY;
125+
let end_day = time_range.end() / SECS_PER_DAY;
121126

122127
// Both endpoints must fall on the same UTC day.
123128
if start_day != end_day {
@@ -131,7 +136,7 @@ pub fn plan_merge_operations_for_index(
131136
continue;
132137
}
133138

134-
let day_bucket = start_day * 86400;
139+
let day_bucket = start_day * SECS_PER_DAY;
135140
let key = (
136141
split.partition_id,
137142
split.doc_mapping_uid.to_string(),
@@ -161,7 +166,7 @@ mod tests {
161166
/// Builds a mature [`SplitMetadata`] for use in tests.
162167
///
163168
/// - `day_bucket`: UTC day expressed as seconds-since-epoch (midnight). For example `day_bucket
164-
/// = 0` means 1970-01-01, `day_bucket = 86400` means 1970-01-02.
169+
/// = 0` means 1970-01-01, `day_bucket = SECS_PER_DAY` means 1970-01-02.
165170
fn mature_split_for_test(
166171
split_id: &str,
167172
index_uid: &IndexUid,
@@ -199,11 +204,11 @@ mod tests {
199204

200205
// UTC day 0 = 1970-01-01. Use a recent-ish day to avoid the retention buffer.
201206
// We use day 20000 (approx 2024-10) so splits are "recent" relative to a "now" we control.
202-
const RECENT_DAY: i64 = 20_000 * 86400;
207+
const RECENT_DAY: i64 = 20_000 * SECS_PER_DAY;
203208

204209
fn now_well_after_recent_day() -> OffsetDateTime {
205210
// 1 day after the splits' day — they are mature but not in a retention buffer.
206-
OffsetDateTime::from_unix_timestamp(RECENT_DAY + 86400 + 1).unwrap()
211+
OffsetDateTime::from_unix_timestamp(RECENT_DAY + SECS_PER_DAY + 1).unwrap()
207212
}
208213

209214
#[test]
@@ -238,7 +243,7 @@ mod tests {
238243
fn test_plan_below_threshold() {
239244
let index_uid = IndexUid::for_test("test-index", 0);
240245
let doc_mapping_uid = DocMappingUid::random();
241-
// Only 4 splits — below MIN_MERGE_GROUP_SIZE (6).
246+
// Only 4 splits — below the min_merge_group_size (5).
242247
let splits: Vec<SplitMetadata> = (0..4)
243248
.map(|i| {
244249
mature_split_for_test(
@@ -256,7 +261,10 @@ mod tests {
256261
&index_config_no_retention(),
257262
splits,
258263
now_well_after_recent_day(),
259-
&MatureMergeConfig::default(),
264+
&MatureMergeConfig {
265+
min_merge_group_size: 5,
266+
..Default::default()
267+
},
260268
);
261269

262270
assert!(operations.is_empty(), "expected no operations for 4 splits");
@@ -342,7 +350,7 @@ mod tests {
342350
// Then: cutoff_raw = (RECENT_DAY + 91d) - 90d + 30d = RECENT_DAY + 31d
343351
// cutoff = RECENT_DAY + 32d (rounded up to next day boundary)
344352
// Because RECENT_DAY + 3600 < cutoff, splits should be excluded.
345-
let now_ts = RECENT_DAY + 91 * 86400;
353+
let now_ts = RECENT_DAY + 91 * SECS_PER_DAY;
346354
let now = OffsetDateTime::from_unix_timestamp(now_ts).unwrap();
347355

348356
let splits: Vec<SplitMetadata> = (0..10)

0 commit comments

Comments
 (0)