@@ -76,24 +76,17 @@ use std::collections::{HashMap, HashSet};
7676use std:: fmt:: Debug ;
7777use std:: fs:: File ;
7878use std:: path:: Path ;
79- use std:: sync:: Arc ;
8079use std:: time:: Instant ;
81- use tokio:: sync:: Semaphore ;
8280use tracing:: { debug, error, warn} ;
8381
8482pub const DATASET_STATS_STREAM_NAME : & str = "pstats" ;
8583const DATASET_STATS_CUSTOM_PARTITION : & str = "dataset_name" ;
86- const MAX_CONCURRENT_FIELD_STATS : usize = 4 ;
87- const PARALLEL_FIELD_STATS_MIN_FIELDS : usize = 16 ;
8884const MIN_TRACKED_DISTINCT_VALUES_PER_FIELD : usize = 1024 ;
8985const MAX_TRACKED_DISTINCT_VALUES_PER_FIELD : usize = 10_000 ;
9086const HLL_PRECISION_BITS : u32 = 12 ;
9187const HLL_REGISTER_COUNT : usize = 1 << HLL_PRECISION_BITS ;
92- static FIELD_STATS_QUERY_SEMAPHORE : Lazy < Arc < Semaphore > > =
93- Lazy :: new ( || Arc :: new ( Semaphore :: new ( MAX_CONCURRENT_FIELD_STATS ) ) ) ;
9488static FIELD_STATS_RAYON_POOL : Lazy < ThreadPool > = Lazy :: new ( || {
9589 ThreadPoolBuilder :: new ( )
96- . num_threads ( MAX_CONCURRENT_FIELD_STATS )
9790 . thread_name ( |index| format ! ( "field-stats-{index}" ) )
9891 . build ( )
9992 . expect ( "field stats rayon pool should initialize" )
@@ -139,7 +132,7 @@ pub async fn calculate_field_stats(
139132 tenant_id,
140133 )
141134 . await ;
142- log_stats_calculation_time ( stream_name, parquet_path, started_at, result. is_ok ( ) ) ;
135+ log_stats_calculation_time ( stream_name, parquet_path, started_at, & result) ;
143136
144137 result
145138}
@@ -218,20 +211,21 @@ fn log_stats_calculation_time(
218211 stream_name : & str ,
219212 parquet_path : & Path ,
220213 started_at : Instant ,
221- success : bool ,
214+ result : & Result < bool , PostError > ,
222215) {
223216 let parquet_file = parquet_path
224217 . file_name ( )
225218 . and_then ( |filename| filename. to_str ( ) )
226219 . unwrap_or ( "<unknown>" ) ;
227220 let elapsed_ms = started_at. elapsed ( ) . as_millis ( ) ;
228- if success {
221+ if result . is_ok ( ) {
229222 debug ! (
230223 "Field stats calculation completed for parquet file {parquet_file} in {elapsed_ms} ms. stream={stream_name}"
231224 ) ;
232225 } else {
226+ let err = result. as_ref ( ) . expect_err ( "result checked as error" ) ;
233227 warn ! (
234- "Field stats calculation failed for parquet file {parquet_file} after {elapsed_ms} ms. stream={stream_name}"
228+ "Field stats calculation failed for parquet file {parquet_file} after {elapsed_ms} ms. stream={stream_name}: {err} "
235229 ) ;
236230 }
237231}
@@ -260,22 +254,11 @@ async fn collect_all_field_stats_from_parquet(
260254 schema : & Schema ,
261255 max_field_statistics : usize ,
262256) -> Result < Vec < FieldStat > , PostError > {
263- let permit = FIELD_STATS_QUERY_SEMAPHORE
264- . clone ( )
265- . acquire_owned ( )
266- . await
267- . map_err ( |e| {
268- PostError :: Invalid ( anyhow:: anyhow!(
269- "Failed to acquire field stats query permit: {}" ,
270- e
271- ) )
272- } ) ?;
273257 let stream_name = stream_name. to_string ( ) ;
274258 let parquet_path = parquet_path. to_path_buf ( ) ;
275259 let schema = schema. clone ( ) ;
276260
277261 tokio:: task:: spawn_blocking ( move || {
278- let _permit = permit;
279262 collect_all_field_stats_from_parquet_blocking (
280263 & stream_name,
281264 & parquet_path,
@@ -335,14 +318,15 @@ fn collect_all_field_stats_from_parquet_blocking(
335318 }
336319 } ;
337320
338- let batch_counts = if field_names. len ( ) >= PARALLEL_FIELD_STATS_MIN_FIELDS {
339- collect_batch_field_counts_parallel ( & batch, & field_names)
340- } else {
341- collect_batch_field_counts_serial ( & batch, & field_names)
342- } ;
321+ let batch_field_counts = collect_batch_field_counts_parallel (
322+ stream_name,
323+ & batch,
324+ & field_names,
325+ max_field_statistics,
326+ ) ;
343327
344- for ( field_name, counts ) in batch_counts {
345- merge_field_counts ( & mut field_counts, & field_name, counts ) ;
328+ for ( field_name, batch_field_count ) in batch_field_counts {
329+ merge_field_counts ( & mut field_counts, & field_name, batch_field_count ) ;
346330 }
347331 }
348332
@@ -364,53 +348,53 @@ fn collect_all_field_stats_from_parquet_blocking(
364348 . collect ( ) )
365349}
366350
367- fn collect_batch_field_counts_serial (
368- batch : & arrow_array:: RecordBatch ,
369- field_names : & [ String ] ,
370- ) -> Vec < ( String , HashMap < String , i64 > ) > {
371- field_names
372- . iter ( )
373- . filter_map ( |field_name| collect_field_counts ( batch, field_name) )
374- . collect ( )
375- }
376-
377351fn collect_batch_field_counts_parallel (
352+ stream_name : & str ,
378353 batch : & arrow_array:: RecordBatch ,
379354 field_names : & [ String ] ,
380- ) -> Vec < ( String , HashMap < String , i64 > ) > {
355+ max_field_statistics : usize ,
356+ ) -> Vec < ( String , FieldCountState ) > {
381357 FIELD_STATS_RAYON_POOL . install ( || {
382358 field_names
383359 . par_iter ( )
384- . filter_map ( |field_name| collect_field_counts ( batch, field_name) )
360+ . filter_map ( |field_name| {
361+ collect_field_counts ( stream_name, batch, field_name, max_field_statistics)
362+ } )
385363 . collect ( )
386364 } )
387365}
388366
389367fn collect_field_counts (
368+ stream_name : & str ,
390369 batch : & arrow_array:: RecordBatch ,
391370 field_name : & str ,
392- ) -> Option < ( String , HashMap < String , i64 > ) > {
371+ max_field_statistics : usize ,
372+ ) -> Option < ( String , FieldCountState ) > {
393373 let array = batch. column_by_name ( field_name) ?;
394- let mut counts = HashMap :: new ( ) ;
374+ let mut field_count = FieldCountState :: new_for_batch (
375+ stream_name. to_string ( ) ,
376+ field_name. to_string ( ) ,
377+ max_field_statistics,
378+ ) ;
395379
396380 for row_index in 0 ..array. len ( ) {
397381 let value = format_arrow_value ( array. as_ref ( ) , row_index) ;
398- * counts . entry ( value) . or_default ( ) += 1 ;
382+ field_count . record_value ( value) ;
399383 }
400384
401- Some ( ( field_name. to_string ( ) , counts ) )
385+ Some ( ( field_name. to_string ( ) , field_count ) )
402386}
403387
404388fn merge_field_counts (
405389 field_counts : & mut HashMap < String , FieldCountState > ,
406390 field_name : & str ,
407- counts : HashMap < String , i64 > ,
391+ batch_field_count : FieldCountState ,
408392) {
409393 let field_total = field_counts
410394 . get_mut ( field_name)
411395 . expect ( "field_counts initialized for each field" ) ;
412396
413- field_total. merge_counts ( counts ) ;
397+ field_total. merge_state ( batch_field_count ) ;
414398}
415399
416400struct FieldCountState {
@@ -421,10 +405,24 @@ struct FieldCountState {
421405 hll : HyperLogLog ,
422406 max_tracked_values : usize ,
423407 approximate : bool ,
408+ log_cardinality_cap : bool ,
424409}
425410
426411impl FieldCountState {
427412 fn new ( stream_name : String , field_name : String , max_field_statistics : usize ) -> Self {
413+ Self :: new_with_logging ( stream_name, field_name, max_field_statistics, true )
414+ }
415+
416+ fn new_for_batch ( stream_name : String , field_name : String , max_field_statistics : usize ) -> Self {
417+ Self :: new_with_logging ( stream_name, field_name, max_field_statistics, false )
418+ }
419+
420+ fn new_with_logging (
421+ stream_name : String ,
422+ field_name : String ,
423+ max_field_statistics : usize ,
424+ log_cardinality_cap : bool ,
425+ ) -> Self {
428426 Self {
429427 stream_name,
430428 field_name,
@@ -433,38 +431,68 @@ impl FieldCountState {
433431 hll : HyperLogLog :: new ( ) ,
434432 max_tracked_values : tracked_distinct_value_limit ( max_field_statistics) ,
435433 approximate : false ,
434+ log_cardinality_cap,
436435 }
437436 }
438437
438+ fn record_value ( & mut self , value : String ) {
439+ self . hll . add ( & value) ;
440+ self . total_count += 1 ;
441+ self . track_value ( value, 1 ) ;
442+ }
443+
444+ #[ cfg( test) ]
439445 fn merge_counts ( & mut self , counts : HashMap < String , i64 > ) {
440446 for ( value, count) in counts {
441447 self . hll . add ( & value) ;
442448 self . total_count += count;
449+ self . track_value ( value, count) ;
450+ }
451+ }
443452
444- if let Some ( existing_count) = self . counts . get_mut ( & value) {
445- * existing_count += count;
446- continue ;
447- }
453+ fn merge_state ( & mut self , state : FieldCountState ) {
454+ self . hll . merge ( & state. hll ) ;
455+ self . total_count += state. total_count ;
448456
449- if self . counts . len ( ) < self . max_tracked_values {
450- self . counts . insert ( value, count) ;
451- continue ;
452- }
457+ if state. approximate {
458+ self . mark_approximate ( ) ;
459+ }
453460
454- if !self . approximate {
455- self . approximate = true ;
461+ for ( value, count) in state. counts {
462+ self . track_value ( value, count) ;
463+ }
464+ }
465+
466+ fn track_value ( & mut self , value : String , count : i64 ) {
467+ if let Some ( existing_count) = self . counts . get_mut ( & value) {
468+ * existing_count += count;
469+ return ;
470+ }
471+
472+ if self . counts . len ( ) < self . max_tracked_values {
473+ self . counts . insert ( value, count) ;
474+ return ;
475+ }
476+
477+ self . mark_approximate ( ) ;
478+
479+ if let Some ( ( min_value, min_count) ) = self . current_min_value ( )
480+ && count > min_count
481+ {
482+ self . counts . remove ( & min_value) ;
483+ self . counts . insert ( value, count) ;
484+ }
485+ }
486+
487+ fn mark_approximate ( & mut self ) {
488+ if !self . approximate {
489+ self . approximate = true ;
490+ if self . log_cardinality_cap {
456491 debug ! (
457492 "Field stats cardinality cap reached for stream {} field {}. Tracking bounded top-value candidates with max_tracked_values={}" ,
458493 self . stream_name, self . field_name, self . max_tracked_values
459494 ) ;
460495 }
461-
462- if let Some ( ( min_value, min_count) ) = self . current_min_value ( )
463- && count > min_count
464- {
465- self . counts . remove ( & min_value) ;
466- self . counts . insert ( value, count) ;
467- }
468496 }
469497 }
470498
@@ -533,6 +561,12 @@ impl HyperLogLog {
533561 self . registers [ register_index] = self . registers [ register_index] . max ( rank) ;
534562 }
535563
564+ fn merge ( & mut self , other : & Self ) {
565+ for ( register, other_register) in self . registers . iter_mut ( ) . zip ( other. registers . iter ( ) ) {
566+ * register = ( * register) . max ( * other_register) ;
567+ }
568+ }
569+
536570 fn estimate ( & self ) -> f64 {
537571 let register_count = HLL_REGISTER_COUNT as f64 ;
538572 let zero_registers = self
0 commit comments