Skip to content

Commit 8dbd959

Browse files
fix: abort json chunk tasks when the stream is dropped
1 parent 02c897c commit 8dbd959

1 file changed

Lines changed: 93 additions & 14 deletions

File tree

default-engine/src/json.rs

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use delta_kernel::{
2626
use futures::stream::{self, BoxStream};
2727
use futures::{ready, FutureExt, StreamExt, TryStreamExt};
2828
use tokio::sync::mpsc;
29+
use tokio::task::{JoinError, JoinHandle};
2930
use url::Url;
3031

3132
use 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.
219246
fn 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

Comments
 (0)