Skip to content

Commit 758f026

Browse files
committed
chunk arrow files for conversion
1 parent 6a5ae51 commit 758f026

1 file changed

Lines changed: 84 additions & 10 deletions

File tree

src/parseable/streams.rs

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,71 @@ static DISK_WRITE_BATCH_ROWS: Lazy<usize> = Lazy::new(|| {
8686
});
8787

8888
const INPROCESS_DIR_PREFIX: &str = "processing_";
89-
const METRIC_ROW_GROUP_PREP_IN_FLIGHT: usize = 1;
89+
const METRIC_ROW_GROUP_PREP_IN_FLIGHT_VAR: &str = "METRIC_ROW_GROUP_PREP_IN_FLIGHT";
90+
/// Caps how many arrow files feed a single parquet conversion group. A
91+
/// minute with heavy schema-key churn can stage thousands of small arrow
92+
/// files; converting them as one group means one kmerge holding an open
93+
/// reader (and a decoded batch) per file. Chunking bounds that memory
94+
/// while still collapsing thousands of files into a handful of parquets.
95+
static METRIC_ROW_GROUP_PREP_IN_FLIGHT: Lazy<usize> = Lazy::new(|| {
96+
if let Ok(var) = std::env::var(METRIC_ROW_GROUP_PREP_IN_FLIGHT_VAR)
97+
&& let Ok(var) = var.parse::<usize>()
98+
&& var > 0
99+
{
100+
var
101+
} else {
102+
1
103+
}
104+
});
105+
106+
const MAX_ARROW_FILES_PER_PARQUET_VAR: &str = "MAX_ARROW_FILES_PER_PARQUET";
107+
/// Caps how many arrow files feed a single parquet conversion group. A
108+
/// minute with heavy schema-key churn can stage thousands of small arrow
109+
/// files; converting them as one group means one kmerge holding an open
110+
/// reader (and a decoded batch) per file. Chunking bounds that memory
111+
/// while still collapsing thousands of files into a handful of parquets.
112+
static MAX_ARROW_FILES_PER_PARQUET: Lazy<usize> = Lazy::new(|| {
113+
if let Ok(var) = std::env::var(MAX_ARROW_FILES_PER_PARQUET_VAR)
114+
&& let Ok(var) = var.parse::<usize>()
115+
&& var > 0
116+
{
117+
var
118+
} else {
119+
512
120+
}
121+
});
122+
123+
/// Splits any conversion group holding more than MAX_ARROW_FILES_PER_PARQUET
124+
/// arrow files into chunks, giving chunk 1.. a "-{i}" suffix on the parquet
125+
/// file stem so each chunk converts to its own parquet file.
126+
fn chunk_arrow_file_groups(
127+
grouped: HashMap<PathBuf, Vec<PathBuf>>,
128+
) -> HashMap<PathBuf, Vec<PathBuf>> {
129+
let max_files = *MAX_ARROW_FILES_PER_PARQUET;
130+
let mut chunked = HashMap::with_capacity(grouped.len());
131+
for (parquet_path, arrow_files) in grouped {
132+
if arrow_files.len() <= max_files {
133+
chunked.insert(parquet_path, arrow_files);
134+
continue;
135+
}
136+
for (i, chunk) in arrow_files.chunks(max_files).enumerate() {
137+
let chunk_path = if i == 0 {
138+
parquet_path.clone()
139+
} else {
140+
let mut path = parquet_path.clone();
141+
if let (Some(stem), Some(ext)) = (
142+
parquet_path.file_stem().and_then(|s| s.to_str()),
143+
parquet_path.extension().and_then(|e| e.to_str()),
144+
) {
145+
path.set_file_name(format!("{stem}-{i}.{ext}"));
146+
}
147+
path
148+
};
149+
chunked.insert(chunk_path, chunk.to_vec());
150+
}
151+
}
152+
chunked
153+
}
90154

91155
struct PreparedMetricRowGroup {
92156
batch: RecordBatch,
@@ -100,10 +164,20 @@ fn arrow_path_to_parquet(
100164
) -> Option<PathBuf> {
101165
let filename = path.file_stem()?.to_str()?;
102166
let (_, front) = filename.split_once('.')?;
103-
if !front.contains('.') {
104-
warn!("Skipping unexpected arrow file without `.`: {}", filename);
167+
// Writers may suffix the filename with a per-file ULID after the
168+
// ".data" marker (ONE_PARQUET_PER_ARROW). Truncate at the marker so
169+
// the parquet grouping key stays per-minute: with high schema-key
170+
// churn, keying on the full name made every arrow file its own
171+
// conversion group (thousands per minute), which exploded conversion
172+
// parallelism/memory and the object-store file count.
173+
let Some(idx) = front.rfind(".data") else {
174+
warn!(
175+
"Skipping unexpected arrow file without `.data`: {}",
176+
filename
177+
);
105178
return None;
106-
}
179+
};
180+
let front = &front[..idx + ".data".len()];
107181
let filename_with_random_number = format!("{front}.{random_string}.parquet");
108182
let mut parquet_path = stream_staging_path.to_owned();
109183
parquet_path.push(filename_with_random_number);
@@ -352,7 +426,7 @@ impl Stream {
352426
}
353427
}
354428
}
355-
grouped
429+
chunk_arrow_file_groups(grouped)
356430
}
357431

358432
/// Returns a mapping for inprocess arrow files (init_signal=true).
@@ -370,7 +444,7 @@ impl Stream {
370444
warn!("Unexpected arrow file: {}", inprocess_file.display());
371445
}
372446
}
373-
grouped
447+
chunk_arrow_file_groups(grouped)
374448
}
375449

376450
/// Returns arrow files for conversion, filtering by time and removing invalid files.
@@ -856,7 +930,7 @@ impl Stream {
856930
let merged_schema = record_reader.merged_schema();
857931
let props =
858932
self.parquet_writer_props(&merged_schema, time_partition, custom_partition);
859-
// schemas.push(merged_schema.clone());
933+
860934
let schema = Arc::new(merged_schema.clone());
861935

862936
let part_path = parquet_path.with_extension("part");
@@ -938,7 +1012,7 @@ impl Stream {
9381012
// each page actually carries.
9391013
let target = self.options.row_group_size;
9401014
let buffer_capacity = record_reader.readers.len();
941-
let mut pending_row_groups = VecDeque::with_capacity(METRIC_ROW_GROUP_PREP_IN_FLIGHT);
1015+
let mut pending_row_groups = VecDeque::with_capacity(*METRIC_ROW_GROUP_PREP_IN_FLIGHT);
9421016
let mut buffer: Vec<RecordBatch> = Vec::with_capacity(buffer_capacity);
9431017
let mut buffered_rows: usize = 0;
9441018
let mut merged_iter =
@@ -959,7 +1033,7 @@ impl Stream {
9591033
time_partition_field.clone(),
9601034
);
9611035
pending_row_groups.push_back(next_row_group);
962-
if pending_row_groups.len() > METRIC_ROW_GROUP_PREP_IN_FLIGHT
1036+
if pending_row_groups.len() > *METRIC_ROW_GROUP_PREP_IN_FLIGHT
9631037
&& let Some(rx) = pending_row_groups.pop_front()
9641038
{
9651039
let prepared = Self::receive_prepared_metric_row_group(rx)?;
@@ -976,7 +1050,7 @@ impl Stream {
9761050
time_partition_field.clone(),
9771051
);
9781052
pending_row_groups.push_back(next_row_group);
979-
if pending_row_groups.len() > METRIC_ROW_GROUP_PREP_IN_FLIGHT
1053+
if pending_row_groups.len() > *METRIC_ROW_GROUP_PREP_IN_FLIGHT
9801054
&& let Some(rx) = pending_row_groups.pop_front()
9811055
{
9821056
let prepared = Self::receive_prepared_metric_row_group(rx)?;

0 commit comments

Comments
 (0)