@@ -44,7 +44,7 @@ use std::alloc::Layout;
4444use std:: ffi:: c_void;
4545use std:: sync:: { Arc , Mutex } ;
4646
47- use datafusion:: arrow:: array:: { Int32Array , RecordBatch } ;
47+ use datafusion:: arrow:: array:: { Int32Array , RecordBatch , StringArray } ;
4848use datafusion:: arrow:: datatypes:: { DataType , Field , Schema , SchemaRef } ;
4949use datafusion:: catalog:: memory:: DataSourceExec ;
5050use datafusion:: common:: runtime:: JoinSet ;
@@ -594,6 +594,42 @@ mod tests {
594594 ctx. register_table ( "t" , Arc :: new ( table) ) . unwrap ( ) ;
595595 }
596596
597+ fn wide_table_schema ( ) -> SchemaRef {
598+ Arc :: new ( Schema :: new ( vec ! [
599+ Field :: new( "val" , DataType :: Int32 , false ) ,
600+ Field :: new( "s" , DataType :: Utf8 , false ) ,
601+ ] ) )
602+ }
603+
604+ /// High-cardinality groups over ~1 KiB strings: each worker's partial-aggregate state runs
605+ /// to hundreds of KiB, so its emit slices balloon past a tiny ring through arrow-ipc.
606+ fn wide_table_partitions ( ) -> Vec < Vec < RecordBatch > > {
607+ let schema = wide_table_schema ( ) ;
608+ const ROWS_PER_PART : i32 = 700 ;
609+ ( 0 ..N_WORKERS as i32 )
610+ . map ( |p| {
611+ let vals =
612+ Int32Array :: from_iter_values ( ( 0 ..ROWS_PER_PART ) . map ( |i| p * ROWS_PER_PART + i) ) ;
613+ let strings = StringArray :: from_iter_values (
614+ ( 0 ..ROWS_PER_PART ) . map ( |i| format ! ( "{p:02}-{i:06}-{}" , "x" . repeat( 1024 ) ) ) ,
615+ ) ;
616+ let batch =
617+ RecordBatch :: try_new ( schema. clone ( ) , vec ! [ Arc :: new( vals) , Arc :: new( strings) ] )
618+ . unwrap ( ) ;
619+ vec ! [ batch]
620+ } )
621+ . collect ( )
622+ }
623+
624+ /// Swap the session's `t` for the wide-string variant. `build_session` registers the
625+ /// standard table; the swap only matters on the leader (fragments travel as shared plan
626+ /// Arcs, so worker sessions never re-resolve the table).
627+ fn register_wide_table ( ctx : & SessionContext ) {
628+ let _ = ctx. deregister_table ( "t" ) . unwrap ( ) ;
629+ let table = MemTable :: try_new ( wide_table_schema ( ) , wide_table_partitions ( ) ) . unwrap ( ) ;
630+ ctx. register_table ( "t" , Arc :: new ( table) ) . unwrap ( ) ;
631+ }
632+
597633 /// A worker is a consumer too (it reads shuffle inputs), so when one of its input streams drops
598634 /// early it has to cancel that stream's producer, not just the leader. This checks the wiring
599635 /// end-to-end: a worker proc's `cancel_stream` reaches the producing proc's inbox through the
@@ -846,15 +882,19 @@ mod tests {
846882 }
847883
848884 fn bootstrap_mesh ( n_procs : u32 ) -> Bootstrap {
849- let region_total = dsm_region_bytes ( n_procs, IN_PROCESS_QUEUE_BYTES , 0 ) . unwrap ( ) ;
885+ bootstrap_mesh_with_queue ( n_procs, IN_PROCESS_QUEUE_BYTES )
886+ }
887+
888+ fn bootstrap_mesh_with_queue ( n_procs : u32 , queue_bytes : usize ) -> Bootstrap {
889+ let region_total = dsm_region_bytes ( n_procs, queue_bytes, 0 ) . unwrap ( ) ;
850890 let region = HeapRegion :: new ( region_total) ;
851891 let base = SharedBase ( region. base ( ) ) ;
852892 let wakeup: Arc < dyn Wakeup > = Arc :: new ( NoopWakeup ) ;
853893 let leader_mesh = unsafe {
854894 leader_setup (
855895 base. 0 ,
856896 n_procs,
857- IN_PROCESS_QUEUE_BYTES ,
897+ queue_bytes ,
858898 & [ ] ,
859899 Arc :: clone ( & wakeup) ,
860900 receiver_token ( 0 ) ,
@@ -952,6 +992,69 @@ mod tests {
952992 ) ;
953993 }
954994
995+ /// An aggregate emits offset slices of its accumulated state, and arrow-ipc writes a
996+ /// sliced variable-length array's whole values buffer, so raw frames balloon to the
997+ /// state's size regardless of batch size. With rings far smaller than that state, the
998+ /// send path's compact-and-split has to carry the query; the result must still match the
999+ /// serial reference exactly.
1000+ #[ tokio:: test( flavor = "current_thread" ) ]
1001+ async fn oversized_aggregate_frames_split_across_tiny_rings ( ) {
1002+ let query = "SELECT val, max(s) AS m FROM t GROUP BY val ORDER BY val" ;
1003+
1004+ let serial_ctx = SessionContext :: new ( ) ;
1005+ register_wide_table ( & serial_ctx) ;
1006+ let expected = serial_ctx
1007+ . sql ( query)
1008+ . await
1009+ . unwrap ( )
1010+ . collect ( )
1011+ . await
1012+ . unwrap ( ) ;
1013+
1014+ // 64 KiB rings against ~700 KiB of per-worker aggregate state.
1015+ let boot = bootstrap_mesh_with_queue ( N_WORKERS + 1 , 64 * 1024 ) ;
1016+ let captured = new_captured_plans ( ) ;
1017+ let leader_ctx = build_session ( Arc :: clone ( & boot. leader_mesh ) , Some ( Arc :: clone ( & captured) ) ) ;
1018+ register_wide_table ( & leader_ctx) ;
1019+ let physical = leader_ctx
1020+ . sql ( query)
1021+ . await
1022+ . unwrap ( )
1023+ . create_physical_plan ( )
1024+ . await
1025+ . unwrap ( ) ;
1026+ let entries = collect_dispatched_stages ( & physical, N_WORKERS ) ;
1027+
1028+ let mut workers = JoinSet :: new ( ) ;
1029+ for ( proc_idx, mesh, outbound) in boot. workers {
1030+ let fragments = fragments_for_proc ( & entries, proc_idx, N_WORKERS ) ;
1031+ let session = build_session ( Arc :: clone ( & mesh) , None ) ;
1032+ workers. spawn ( run_worker_proc (
1033+ fragments,
1034+ outbound,
1035+ mesh,
1036+ session,
1037+ N_WORKERS ,
1038+ Arc :: clone ( & captured) ,
1039+ ) ) ;
1040+ }
1041+
1042+ let leader_task_ctx = leader_ctx. task_ctx ( ) ;
1043+ let stream = physical. execute ( 0 , leader_task_ctx) . unwrap ( ) ;
1044+ let got: Vec < RecordBatch > = stream. try_collect ( ) . await . unwrap ( ) ;
1045+
1046+ while let Some ( res) = workers. join_next ( ) . await {
1047+ res. expect ( "worker task panicked" ) . expect ( "worker proc" ) ;
1048+ }
1049+
1050+ use datafusion:: arrow:: util:: pretty:: pretty_format_batches;
1051+ assert_eq ! (
1052+ pretty_format_batches( & expected) . unwrap( ) . to_string( ) ,
1053+ pretty_format_batches( & got) . unwrap( ) . to_string( ) ,
1054+ "distributed wide aggregate != serial"
1055+ ) ;
1056+ }
1057+
9551058 /// A producer that attaches and then goes away without sending its EOFs must fail the
9561059 /// gather, not hang it: the drain fails the channels the dead receiver fed once the ring
9571060 /// detaches.
0 commit comments