@@ -1508,34 +1508,41 @@ impl BatchDecodeStream {
15081508
15091509 pub fn into_stream ( self ) -> BoxStream < ' static , ReadBatchTask > {
15101510 let stream = futures:: stream:: unfold ( self , |mut slf| async move {
1511- let next_task = slf. next_batch_task ( ) . await ;
1512- let next_task = next_task. transpose ( ) . map ( |next_task| {
1513- let num_rows = next_task. as_ref ( ) . map ( |t| t. num_rows ) . unwrap_or ( 0 ) ;
1514- let emitted_batch_size_warning = slf. emitted_batch_size_warning . clone ( ) ;
1515- let task = async move {
1516- let next_task = next_task?;
1517- // Real decode work happens inside into_batch, which can block the current
1518- // thread for a long time. By spawning it as a new task, we allow Tokio's
1519- // worker threads to keep making progress.
1520- let ( batch, _data_size) =
1521- tokio:: spawn (
1522- async move { next_task. into_batch ( emitted_batch_size_warning) } ,
1523- )
1511+ let next_task = match slf. next_batch_task ( ) . await {
1512+ Ok ( Some ( next_task) ) => next_task,
1513+ Ok ( None ) => return None ,
1514+ Err ( err) => {
1515+ slf. rows_remaining = 0 ;
1516+ return Some ( (
1517+ ReadBatchTask {
1518+ task : async move { Err ( err) } . boxed ( ) ,
1519+ num_rows : 0 ,
1520+ } ,
1521+ slf,
1522+ ) ) ;
1523+ }
1524+ } ;
1525+ let num_rows = next_task. num_rows ;
1526+ let emitted_batch_size_warning = slf. emitted_batch_size_warning . clone ( ) ;
1527+ let task = async move {
1528+ // Real decode work happens inside into_batch, which can block the current
1529+ // thread for a long time. By spawning it as a new task, we allow Tokio's
1530+ // worker threads to keep making progress.
1531+ let ( batch, _data_size) =
1532+ tokio:: spawn ( async move { next_task. into_batch ( emitted_batch_size_warning) } )
15241533 . await
15251534 . map_err ( |err| Error :: wrapped ( err. into ( ) ) ) ??;
1526- Ok ( batch)
1527- } ;
1528- ( task, num_rows)
1529- } ) ;
1530- next_task. map ( |( task, num_rows) | {
1531- // This should be true since batch size is u32
1532- debug_assert ! ( num_rows <= u32 :: MAX as u64 ) ;
1533- let next_task = ReadBatchTask {
1535+ Ok ( batch)
1536+ } ;
1537+ // This should be true since batch size is u32
1538+ debug_assert ! ( num_rows <= u32 :: MAX as u64 ) ;
1539+ Some ( (
1540+ ReadBatchTask {
15341541 task : task. boxed ( ) ,
15351542 num_rows : num_rows as u32 ,
1536- } ;
1537- ( next_task , slf)
1538- } )
1543+ } ,
1544+ slf,
1545+ ) )
15391546 } ) ;
15401547 stream. boxed ( )
15411548 }
@@ -1913,54 +1920,61 @@ impl StructuralBatchDecodeStream {
19131920
19141921 pub fn into_stream ( self ) -> BoxStream < ' static , ReadBatchTask > {
19151922 let stream = futures:: stream:: unfold ( self , |mut slf| async move {
1916- let next_task = slf. next_batch_task ( ) . await ;
1917- let next_task = next_task. transpose ( ) . map ( |next_task| {
1918- let num_rows = next_task. as_ref ( ) . map ( |t| t. num_rows ) . unwrap_or ( 0 ) ;
1919- let emitted_batch_size_warning = slf. emitted_batch_size_warning . clone ( ) ;
1920- let bytes_per_row_feedback = slf. bytes_per_row_feedback . clone ( ) ;
1921- // Capture the per-stream policy once so every emitted batch task follows the
1922- // same throughput-vs-overhead choice made by the scheduler.
1923- let spawn_batch_decode_tasks = slf. spawn_batch_decode_tasks ;
1924- let task = async move {
1925- let next_task = next_task?;
1926- let ( batch, data_size) = if spawn_batch_decode_tasks {
1927- tokio:: spawn (
1928- async move { next_task. into_batch ( emitted_batch_size_warning) } ,
1929- )
1923+ let next_task = match slf. next_batch_task ( ) . await {
1924+ Ok ( Some ( next_task) ) => next_task,
1925+ Ok ( None ) => return None ,
1926+ Err ( err) => {
1927+ slf. rows_remaining = 0 ;
1928+ return Some ( (
1929+ ReadBatchTask {
1930+ task : async move { Err ( err) } . boxed ( ) ,
1931+ num_rows : 0 ,
1932+ } ,
1933+ slf,
1934+ ) ) ;
1935+ }
1936+ } ;
1937+ let num_rows = next_task. num_rows ;
1938+ let emitted_batch_size_warning = slf. emitted_batch_size_warning . clone ( ) ;
1939+ let bytes_per_row_feedback = slf. bytes_per_row_feedback . clone ( ) ;
1940+ // Capture the per-stream policy once so every emitted batch task follows the
1941+ // same throughput-vs-overhead choice made by the scheduler.
1942+ let spawn_batch_decode_tasks = slf. spawn_batch_decode_tasks ;
1943+ let task = async move {
1944+ let ( batch, data_size) = if spawn_batch_decode_tasks {
1945+ tokio:: spawn ( async move { next_task. into_batch ( emitted_batch_size_warning) } )
19301946 . await
19311947 . map_err ( |err| Error :: wrapped ( err. into ( ) ) ) ??
1948+ } else {
1949+ next_task. into_batch ( emitted_batch_size_warning) ?
1950+ } ;
1951+ let num_rows = batch. num_rows ( ) as u64 ;
1952+ if num_rows > 0 {
1953+ let bpr = data_size / num_rows;
1954+ let prev = bytes_per_row_feedback. load ( Ordering :: Relaxed ) ;
1955+ let next = if prev == 0 || bpr >= prev {
1956+ // First batch or actual size is larger than estimate:
1957+ // adopt immediately to avoid OOM.
1958+ bpr
19321959 } else {
1933- next_task. into_batch ( emitted_batch_size_warning) ?
1960+ // Actual size is smaller: degrade gradually toward
1961+ // the true value to avoid over-correcting on a
1962+ // single anomalous batch.
1963+ ( prev + bpr) / 2
19341964 } ;
1935- let num_rows = batch. num_rows ( ) as u64 ;
1936- if num_rows > 0 {
1937- let bpr = data_size / num_rows;
1938- let prev = bytes_per_row_feedback. load ( Ordering :: Relaxed ) ;
1939- let next = if prev == 0 || bpr >= prev {
1940- // First batch or actual size is larger than estimate:
1941- // adopt immediately to avoid OOM.
1942- bpr
1943- } else {
1944- // Actual size is smaller: degrade gradually toward
1945- // the true value to avoid over-correcting on a
1946- // single anomalous batch.
1947- ( prev + bpr) / 2
1948- } ;
1949- bytes_per_row_feedback. store ( next. max ( 1 ) , Ordering :: Relaxed ) ;
1950- }
1951- Ok ( batch)
1952- } ;
1953- ( task, num_rows)
1954- } ) ;
1955- next_task. map ( |( task, num_rows) | {
1956- // This should be true since batch size is u32
1957- debug_assert ! ( num_rows <= u32 :: MAX as u64 ) ;
1958- let next_task = ReadBatchTask {
1965+ bytes_per_row_feedback. store ( next. max ( 1 ) , Ordering :: Relaxed ) ;
1966+ }
1967+ Ok ( batch)
1968+ } ;
1969+ // This should be true since batch size is u32
1970+ debug_assert ! ( num_rows <= u32 :: MAX as u64 ) ;
1971+ Some ( (
1972+ ReadBatchTask {
19591973 task : task. boxed ( ) ,
19601974 num_rows : num_rows as u32 ,
1961- } ;
1962- ( next_task , slf)
1963- } )
1975+ } ,
1976+ slf,
1977+ ) )
19641978 } ) ;
19651979 stream. boxed ( )
19661980 }
@@ -2915,6 +2929,60 @@ pub async fn decode_batch(
29152929// test coalesce indices to ranges
29162930mod tests {
29172931 use super :: * ;
2932+ use crate :: previous:: decoder:: { DecoderReady , LogicalPageDecoder } ;
2933+ use std:: collections:: VecDeque ;
2934+
2935+ #[ derive( Debug ) ]
2936+ struct FailingPageDecoder {
2937+ page_data_type : DataType ,
2938+ total_rows : u64 ,
2939+ load_error_message : & ' static str ,
2940+ }
2941+
2942+ impl FailingPageDecoder {
2943+ fn new (
2944+ page_data_type : DataType ,
2945+ total_rows : u64 ,
2946+ load_error_message : & ' static str ,
2947+ ) -> Self {
2948+ Self {
2949+ page_data_type,
2950+ total_rows,
2951+ load_error_message,
2952+ }
2953+ }
2954+ }
2955+
2956+ impl LogicalPageDecoder for FailingPageDecoder {
2957+ fn wait_for_loaded ( & ' _ mut self , _rows_needed : u64 ) -> BoxFuture < ' _ , Result < ( ) > > {
2958+ let load_error_message = self . load_error_message ;
2959+ async move { Err ( Error :: io ( load_error_message) ) } . boxed ( )
2960+ }
2961+
2962+ fn rows_loaded ( & self ) -> u64 {
2963+ 0
2964+ }
2965+
2966+ fn num_rows ( & self ) -> u64 {
2967+ self . total_rows
2968+ }
2969+
2970+ fn rows_drained ( & self ) -> u64 {
2971+ 0
2972+ }
2973+
2974+ fn drain ( & mut self , requested_rows : u64 ) -> Result < NextDecodeTask > {
2975+ Err ( Error :: internal ( format ! (
2976+ "failing page decoder should not be drained after load error \
2977+ (requested_rows={})",
2978+ requested_rows
2979+ ) ) )
2980+ }
2981+
2982+ fn data_type ( & self ) -> & DataType {
2983+ & self . page_data_type
2984+ }
2985+ }
29182986
29192987 #[ test]
29202988 fn test_read_zero_dimension_fsl_errors_instead_of_panicking ( ) {
@@ -2962,6 +3030,100 @@ mod tests {
29623030 ) ;
29633031 }
29643032
3033+ #[ tokio:: test]
3034+ async fn test_legacy_stream_stops_on_load_error ( ) {
3035+ use arrow_schema:: Field as ArrowField ;
3036+
3037+ let rows_per_batch = 1 ;
3038+ let total_rows = 2 ;
3039+ let scheduled_rows = 1 ;
3040+ let page_rows = 1 ;
3041+ let batch_readahead = 2 ;
3042+ let load_error_message = "simulated page load failure" ;
3043+ let fields = Fields :: from ( vec ! [ ArrowField :: new( "vector" , DataType :: Float32 , true ) ] ) ;
3044+ let root_decoder = SimpleStructDecoder :: new ( fields, total_rows) ;
3045+ let ( tx, rx) = unbounded_channel ( ) ;
3046+
3047+ tx. send ( Ok ( DecoderMessage {
3048+ scheduled_so_far : scheduled_rows,
3049+ decoders : vec ! [ MessageType :: DecoderReady ( DecoderReady {
3050+ decoder: Box :: new( FailingPageDecoder :: new(
3051+ DataType :: Float32 ,
3052+ page_rows,
3053+ load_error_message,
3054+ ) ) ,
3055+ path: VecDeque :: from( [ 0 ] ) ,
3056+ } ) ] ,
3057+ } ) )
3058+ . unwrap ( ) ;
3059+ drop ( tx) ;
3060+
3061+ let stream =
3062+ BatchDecodeStream :: new ( rx, rows_per_batch, total_rows, root_decoder) . into_stream ( ) ;
3063+ let mut batches = stream. map ( |task| task. task ) . buffered ( batch_readahead) ;
3064+
3065+ let err = batches
3066+ . next ( )
3067+ . await
3068+ . expect ( "stream should emit the legacy page-load error" )
3069+ . unwrap_err ( ) ;
3070+ assert ! (
3071+ err. to_string( ) . contains( load_error_message) ,
3072+ "unexpected error: {}" ,
3073+ err
3074+ ) ;
3075+ assert ! (
3076+ batches. next( ) . await . is_none( ) ,
3077+ "stream should stop after the legacy page-load error"
3078+ ) ;
3079+ }
3080+
3081+ #[ tokio:: test]
3082+ async fn test_structural_stream_stops_on_load_error ( ) {
3083+ let rows_per_batch = 1 ;
3084+ let total_rows = 2 ;
3085+ let scheduled_rows = 1 ;
3086+ let batch_readahead = 2 ;
3087+ let load_error_message = "simulated page load failure" ;
3088+ let fields = Fields :: from ( vec ! [ ArrowField :: new( "vector" , DataType :: Float32 , true ) ] ) ;
3089+ let root_decoder = StructuralStructDecoder :: new ( fields, false , /*is_root=*/ true ) . unwrap ( ) ;
3090+ let ( tx, rx) = unbounded_channel ( ) ;
3091+ let failed_page = async move { Err ( Error :: io ( load_error_message) ) } . boxed ( ) ;
3092+
3093+ tx. send ( Ok ( DecoderMessage {
3094+ scheduled_so_far : scheduled_rows,
3095+ decoders : vec ! [ MessageType :: UnloadedPage ( UnloadedPageShard ( failed_page) ) ] ,
3096+ } ) )
3097+ . unwrap ( ) ;
3098+ drop ( tx) ;
3099+
3100+ let stream = StructuralBatchDecodeStream :: new (
3101+ rx,
3102+ rows_per_batch,
3103+ total_rows,
3104+ root_decoder,
3105+ /*spawn_batch_decode_tasks=*/ true ,
3106+ None ,
3107+ )
3108+ . into_stream ( ) ;
3109+ let mut batches = stream. map ( |task| task. task ) . buffered ( batch_readahead) ;
3110+
3111+ let err = batches
3112+ . next ( )
3113+ . await
3114+ . expect ( "stream should emit the page-load error" )
3115+ . unwrap_err ( ) ;
3116+ assert ! (
3117+ err. to_string( ) . contains( load_error_message) ,
3118+ "unexpected error: {}" ,
3119+ err
3120+ ) ;
3121+ assert ! (
3122+ batches. next( ) . await . is_none( ) ,
3123+ "stream should stop after the page-load error"
3124+ ) ;
3125+ }
3126+
29653127 #[ test]
29663128 fn test_coalesce_indices_to_ranges_with_single_index ( ) {
29673129 let indices = vec ! [ 1 ] ;
0 commit comments