@@ -26,6 +26,7 @@ use delta_kernel::{
2626use futures:: stream:: { self , BoxStream } ;
2727use futures:: { ready, FutureExt , StreamExt , TryStreamExt } ;
2828use tokio:: sync:: mpsc;
29+ use tokio:: task:: { JoinError , JoinHandle } ;
2930use url:: Url ;
3031
3132use crate :: executor:: TaskExecutor ;
@@ -166,21 +167,17 @@ async fn read_json_files_parallel_impl(
166167 let num_chunks = chunks. len ( ) ;
167168 let per_chunk_buffer = ( buffer_size / num_chunks) . max ( 1 ) ;
168169
169- // Spawn each chunk as a tokio task. Each task streams batches through a
170- // channel as they are produced. We keep the JoinHandle so a panic becomes
171- // JoinFailure instead of a silent hole in the file list.
172- let channel_cap = per_chunk_buffer. saturating_mul ( 4 ) . max ( 1 ) ;
173170 let mut receivers = Vec :: new ( ) ;
174171 let mut handles = Vec :: new ( ) ;
175172 for chunk in chunks {
176- let ( tx, rx) = mpsc:: channel :: < DeltaResult < Box < dyn EngineData > > > ( channel_cap ) ;
173+ let ( tx, rx) = mpsc:: channel :: < DeltaResult < Box < dyn EngineData > > > ( per_chunk_buffer ) ;
177174 receivers. push ( rx) ;
178175
179176 let store = store. clone ( ) ;
180177 let physical_schema = physical_schema. clone ( ) ;
181178 let predicate = predicate. clone ( ) ;
182179
183- let handle = tokio:: spawn ( async move {
180+ let handle = AbortOnDropHandle :: new ( tokio:: spawn ( async move {
184181 let result = read_json_files_impl (
185182 store,
186183 chunk,
@@ -204,7 +201,7 @@ async fn read_json_files_parallel_impl(
204201 let _ = tx. send ( Err ( e) ) . await ;
205202 }
206203 }
207- } ) ;
204+ } ) ) ;
208205 handles. push ( handle) ;
209206 }
210207
@@ -215,22 +212,51 @@ async fn read_json_files_parallel_impl(
215212 Ok ( result_stream. boxed ( ) )
216213}
217214
215+ /// `JoinHandle` that aborts the task if dropped without being joined.
216+ /// Dropping a raw `JoinHandle` detaches the task; aborting stops in-flight
217+ /// object-store GETs when the consumer cancels or stops early.
218+ struct AbortOnDropHandle {
219+ handle : Option < JoinHandle < ( ) > > ,
220+ }
221+
222+ impl AbortOnDropHandle {
223+ fn new ( handle : JoinHandle < ( ) > ) -> Self {
224+ Self {
225+ handle : Some ( handle) ,
226+ }
227+ }
228+
229+ async fn join ( mut self ) -> Result < ( ) , JoinError > {
230+ match self . handle . take ( ) {
231+ Some ( handle) => handle. await ,
232+ None => Ok ( ( ) ) ,
233+ }
234+ }
235+ }
236+
237+ impl Drop for AbortOnDropHandle {
238+ fn drop ( & mut self ) {
239+ if let Some ( handle) = self . handle . take ( ) {
240+ handle. abort ( ) ;
241+ }
242+ }
243+ }
244+
218245/// Drain `rx` then join `handle`. A panicked task is `Error::JoinFailure`, not EOF.
219246fn drain_chunk (
220247 rx : mpsc:: Receiver < DeltaResult < Box < dyn EngineData > > > ,
221- handle : tokio :: task :: JoinHandle < ( ) > ,
248+ handle : AbortOnDropHandle ,
222249) -> impl futures:: Stream < Item = DeltaResult < Box < dyn EngineData > > > {
223250 let batches = stream:: unfold ( rx, |mut rx| async {
224251 rx. recv ( ) . await . map ( |item| ( item, rx) )
225252 } ) ;
226- let join = stream:: once ( async move { handle. await . map_err ( Error :: join_failure) } ) . filter_map (
227- |result| async move {
253+ let join = stream:: once ( async move { handle. join ( ) . await . map_err ( Error :: join_failure) } )
254+ . filter_map ( |result| async move {
228255 match result {
229256 Ok ( ( ) ) => None ,
230257 Err ( e) => Some ( Err ( e) ) ,
231258 }
232- } ,
233- ) ;
259+ } ) ;
234260 batches. chain ( join)
235261}
236262
@@ -1112,9 +1138,9 @@ mod tests {
11121138 #[ tokio:: test( flavor = "multi_thread" ) ]
11131139 async fn test_drain_chunk_panicked_task_is_join_failure_not_eof ( ) {
11141140 let ( tx, rx) = tokio:: sync:: mpsc:: channel :: < DeltaResult < Box < dyn EngineData > > > ( 1 ) ;
1115- let handle = tokio:: spawn ( async {
1141+ let handle = AbortOnDropHandle :: new ( tokio:: spawn ( async {
11161142 panic ! ( "chunk task panicked" ) ;
1117- } ) ;
1143+ } ) ) ;
11181144 drop ( tx) ;
11191145
11201146 let items: Vec < _ > = drain_chunk ( rx, handle) . collect ( ) . await ;
@@ -1130,6 +1156,59 @@ mod tests {
11301156 }
11311157 }
11321158
1159+ #[ tokio:: test( flavor = "multi_thread" ) ]
1160+ async fn test_abort_on_drop_handle_aborts_unjoined_task ( ) {
1161+ let ( tx, rx) = tokio:: sync:: oneshot:: channel :: < ( ) > ( ) ;
1162+ let handle = AbortOnDropHandle :: new ( tokio:: spawn ( async move {
1163+ let _tx = tx;
1164+ std:: future:: pending :: < ( ) > ( ) . await ;
1165+ } ) ) ;
1166+ drop ( handle) ;
1167+ assert ! (
1168+ rx. await . is_err( ) ,
1169+ "dropping AbortOnDropHandle must abort the task"
1170+ ) ;
1171+ }
1172+
1173+ #[ tokio:: test( flavor = "multi_thread" ) ]
1174+ async fn test_abort_on_drop_handle_join_completed_task ( ) {
1175+ let handle = AbortOnDropHandle :: new ( tokio:: spawn ( async { } ) ) ;
1176+ handle
1177+ . join ( )
1178+ . await
1179+ . expect ( "joining a completed task must succeed" ) ;
1180+ }
1181+
1182+ #[ tokio:: test( flavor = "multi_thread" ) ]
1183+ async fn test_drain_chunk_successful_task_yields_no_join_item ( ) {
1184+ let ( tx, rx) = tokio:: sync:: mpsc:: channel :: < DeltaResult < Box < dyn EngineData > > > ( 1 ) ;
1185+ let handle = AbortOnDropHandle :: new ( tokio:: spawn ( async { } ) ) ;
1186+ drop ( tx) ;
1187+
1188+ let items: Vec < _ > = drain_chunk ( rx, handle) . collect ( ) . await ;
1189+ assert ! (
1190+ items. is_empty( ) ,
1191+ "successful chunk must yield no extra item after the channel closes"
1192+ ) ;
1193+ }
1194+
1195+ #[ tokio:: test( flavor = "multi_thread" ) ]
1196+ async fn test_drain_chunk_drop_aborts_unjoined_task ( ) {
1197+ let ( tx, rx) = tokio:: sync:: mpsc:: channel :: < DeltaResult < Box < dyn EngineData > > > ( 1 ) ;
1198+ let ( done_tx, done_rx) = tokio:: sync:: oneshot:: channel :: < ( ) > ( ) ;
1199+ let handle = AbortOnDropHandle :: new ( tokio:: spawn ( async move {
1200+ let _tx = tx;
1201+ let _done_tx = done_tx;
1202+ std:: future:: pending :: < ( ) > ( ) . await ;
1203+ } ) ) ;
1204+
1205+ drop ( drain_chunk ( rx, handle) ) ;
1206+ assert ! (
1207+ done_rx. await . is_err( ) ,
1208+ "dropping drain_chunk must abort the unjoined task"
1209+ ) ;
1210+ }
1211+
11331212 #[ tokio:: test( flavor = "multi_thread" ) ]
11341213 async fn test_read_json_files_parallel_empty_files ( ) {
11351214 let store = Arc :: new ( InMemory :: new ( ) ) ;
0 commit comments