@@ -54,8 +54,13 @@ use futures::{FutureExt as _, Stream};
5454struct SpillReaderStream {
5555 schema : SchemaRef ,
5656 state : SpillReaderStreamState ,
57+ /// how much memory the largest memory batch is taking
58+ pub max_record_batch_memory : Option < usize > ,
5759}
5860
61+ // Small margin allowed to accommodate slight memory accounting variation
62+ const MEMORY_MARGIN : usize = 4096 ;
63+
5964/// When we poll for the next batch, we will get back both the batch and the reader,
6065/// so we can call `next` again.
6166type NextRecordBatchResult = Result < ( StreamReader < BufReader < File > > , Option < RecordBatch > ) > ;
@@ -76,10 +81,15 @@ enum SpillReaderStreamState {
7681}
7782
7883impl SpillReaderStream {
79- fn new ( schema : SchemaRef , spill_file : RefCountedTempFile ) -> Self {
84+ fn new (
85+ schema : SchemaRef ,
86+ spill_file : RefCountedTempFile ,
87+ max_record_batch_memory : Option < usize > ,
88+ ) -> Self {
8089 Self {
8190 schema,
8291 state : SpillReaderStreamState :: Uninitialized ( spill_file) ,
92+ max_record_batch_memory,
8393 }
8494 }
8595
@@ -125,6 +135,27 @@ impl SpillReaderStream {
125135 Ok ( ( reader, batch) ) => {
126136 match batch {
127137 Some ( batch) => {
138+ if let Some ( max_record_batch_memory) =
139+ self . max_record_batch_memory
140+ {
141+ let actual_size =
142+ get_record_batch_memory_size ( & batch) ;
143+ if actual_size
144+ > max_record_batch_memory + MEMORY_MARGIN
145+ {
146+ return Poll :: Ready ( Some ( Err (
147+ DataFusionError :: ResourcesExhausted (
148+ format ! (
149+ "Record batch memory usage ({actual_size} bytes) exceeds the expected limit ({max_record_batch_memory} bytes)\n
150+ by more than the allowed tolerance ({MEMORY_MARGIN} bytes).\n
151+ This likely indicates a bug in memory accounting during spilling.\n
152+ Please report this issue" ,
153+ )
154+ . to_owned ( ) ,
155+ ) ,
156+ ) ) ) ;
157+ }
158+ }
128159 self . state = SpillReaderStreamState :: Waiting ( reader) ;
129160
130161 Poll :: Ready ( Some ( Ok ( batch) ) )
@@ -417,7 +448,7 @@ mod tests {
417448 let spilled_rows = spill_manager. metrics . spilled_rows . value ( ) ;
418449 assert_eq ! ( spilled_rows, num_rows) ;
419450
420- let stream = spill_manager. read_spill_as_stream ( spill_file) ?;
451+ let stream = spill_manager. read_spill_as_stream ( spill_file, None ) ?;
421452 assert_eq ! ( stream. schema( ) , schema) ;
422453
423454 let batches = collect ( stream) . await ?;
@@ -481,7 +512,7 @@ mod tests {
481512 let spilled_rows = spill_manager. metrics . spilled_rows . value ( ) ;
482513 assert_eq ! ( spilled_rows, num_rows) ;
483514
484- let stream = spill_manager. read_spill_as_stream ( spill_file) ?;
515+ let stream = spill_manager. read_spill_as_stream ( spill_file, None ) ?;
485516 assert_eq ! ( stream. schema( ) , dict_schema) ;
486517 let batches = collect ( stream) . await ?;
487518 assert_eq ! ( batches. len( ) , 2 ) ;
@@ -512,7 +543,7 @@ mod tests {
512543 assert ! ( spill_file. path( ) . exists( ) ) ;
513544 assert ! ( max_batch_mem > 0 ) ;
514545
515- let stream = spill_manager. read_spill_as_stream ( spill_file) ?;
546+ let stream = spill_manager. read_spill_as_stream ( spill_file, None ) ?;
516547 assert_eq ! ( stream. schema( ) , schema) ;
517548
518549 let batches = collect ( stream) . await ?;
@@ -547,7 +578,7 @@ mod tests {
547578 let spilled_rows = spill_manager. metrics . spilled_rows . value ( ) ;
548579 assert_eq ! ( spilled_rows, num_rows) ;
549580
550- let stream = spill_manager. read_spill_as_stream ( spill_file) ?;
581+ let stream = spill_manager. read_spill_as_stream ( spill_file, None ) ?;
551582 assert_eq ! ( stream. schema( ) , schema) ;
552583
553584 let batches = collect ( stream) . await ?;
@@ -931,8 +962,10 @@ mod tests {
931962 . spill_record_batch_and_finish ( & batches, "Test2" ) ?
932963 . unwrap ( ) ;
933964
934- let mut stream_1 = spill_manager. read_spill_as_stream ( spill_file_1) ?;
935- let mut stream_2 = spill_manager. read_spill_as_stream ( spill_file_2) ?;
965+ let mut stream_1 =
966+ spill_manager. read_spill_as_stream ( spill_file_1, None ) ?;
967+ let mut stream_2 =
968+ spill_manager. read_spill_as_stream ( spill_file_2, None ) ?;
936969 stream_1. next ( ) . await ;
937970 stream_2. next ( ) . await ;
938971
0 commit comments