@@ -86,7 +86,71 @@ static DISK_WRITE_BATCH_ROWS: Lazy<usize> = Lazy::new(|| {
8686} ) ;
8787
8888const 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
91155struct 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