Skip to content

Commit b52c222

Browse files
test: cover later-chunk errors and ragged json splits
1 parent bb3127c commit b52c222

2 files changed

Lines changed: 93 additions & 2 deletions

File tree

default-engine/src/json.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ impl<E: TaskExecutor> DefaultJsonHandler<E> {
9090

9191
/// Number of ordered file chunks to parse concurrently in [`Self::read_json_files`].
9292
/// `None` (the default) means no parallelism, no chunking.
93+
///
94+
/// Chunk tasks are spawned on the current Tokio runtime. Real speedup needs a
95+
/// multi-thread executor ([`TokioMultiThreadExecutor`]). The default
96+
/// [`TokioBackgroundExecutor`] is single-threaded, so chunks share one thread.
9397
pub fn with_parallel_chunks(mut self, parallel_chunks: Option<NonZero<usize>>) -> Self {
9498
self.parallel_chunks = parallel_chunks;
9599
self
@@ -1253,6 +1257,89 @@ mod tests {
12531257
assert!(result.is_err(), "missing file must produce an error");
12541258
}
12551259

1260+
#[tokio::test(flavor = "multi_thread")]
1261+
async fn test_read_json_files_parallel_later_chunk_errors_after_data() {
1262+
// 4 files / 2 chunks -> [0,1] then [missing, 3]. Chunk 0 yields data;
1263+
// chunk 1 errors. try_collect would hide that ordering.
1264+
let store = Arc::new(InMemory::new());
1265+
for i in [0, 1, 3] {
1266+
store
1267+
.put(
1268+
&Path::from(format!("test/{i}")),
1269+
Bytes::from(format!("{{\"val\": {i}}}")).into(),
1270+
)
1271+
.await
1272+
.unwrap();
1273+
}
1274+
let files: Vec<FileMeta> = (0..4)
1275+
.map(|i| FileMeta {
1276+
location: Url::parse(&format!("memory:///test/{i}")).unwrap(),
1277+
last_modified: 0,
1278+
size: 12,
1279+
})
1280+
.collect();
1281+
let handler = DefaultJsonHandler::new(
1282+
store,
1283+
Arc::new(TokioMultiThreadExecutor::new(
1284+
tokio::runtime::Handle::current(),
1285+
)),
1286+
)
1287+
.with_parallel_chunks(NonZero::new(2));
1288+
let physical_schema = schema_ref! { nullable "val": INTEGER };
1289+
let mut iter = handler
1290+
.read_json_files(&files, physical_schema, None)
1291+
.unwrap();
1292+
let first = iter.next().expect("chunk 0 must yield a batch");
1293+
assert!(first.is_ok(), "chunk 0 must succeed before chunk 1 errors");
1294+
let rest: DeltaResult<Vec<_>> = iter.try_collect();
1295+
assert!(rest.is_err(), "missing file in a later chunk must error");
1296+
}
1297+
1298+
#[tokio::test(flavor = "multi_thread")]
1299+
async fn test_read_json_files_parallel_ragged_chunk_sizes() {
1300+
// 7 files / 3 chunks -> sizes 3, 3, 1 (not an even split).
1301+
const N: i32 = 7;
1302+
let store = Arc::new(InMemory::new());
1303+
for i in 0..N {
1304+
store
1305+
.put(
1306+
&Path::from(format!("test/{i}")),
1307+
Bytes::from(format!("{{\"val\": {i}}}")).into(),
1308+
)
1309+
.await
1310+
.unwrap();
1311+
}
1312+
let files: Vec<FileMeta> = (0..N)
1313+
.map(|i| FileMeta {
1314+
location: Url::parse(&format!("memory:///test/{i}")).unwrap(),
1315+
last_modified: 0,
1316+
size: 12,
1317+
})
1318+
.collect();
1319+
let handler = DefaultJsonHandler::new(
1320+
store,
1321+
Arc::new(TokioMultiThreadExecutor::new(
1322+
tokio::runtime::Handle::current(),
1323+
)),
1324+
)
1325+
.with_parallel_chunks(NonZero::new(3));
1326+
let physical_schema = schema_ref! { nullable "val": INTEGER };
1327+
let data: Vec<RecordBatch> = handler
1328+
.read_json_files(&files, physical_schema, None)
1329+
.unwrap()
1330+
.map_ok(into_record_batch)
1331+
.try_collect()
1332+
.unwrap();
1333+
let all_values: Vec<i32> = data
1334+
.iter()
1335+
.flat_map(|batch| {
1336+
let val_col: &Int32Array = batch.column(0).as_primitive();
1337+
(0..val_col.len()).map(|i| val_col.value(i)).collect_vec()
1338+
})
1339+
.collect();
1340+
assert_eq!(all_values, (0..N).collect_vec());
1341+
}
1342+
12561343
#[tokio::test(flavor = "multi_thread")]
12571344
async fn test_read_json_files_parallel_with_cancelled_token() {
12581345
let store = Arc::new(InMemory::new());

default-engine/src/lib.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,12 @@ impl<E> DefaultEngineBuilder<E> {
295295
/// Set the number of ordered JSON file chunks to parse concurrently.
296296
///
297297
/// `None` (the default) means no parallelism. `Some(n)` splits the
298-
/// file list into `n` chunks, parses them concurrently, and concatenates
299-
/// results in the proper order.
298+
/// file list into up to n chunks (fewer when there are fewer files than n),
299+
/// parses them concurrently, and concatenates results in the proper order.
300+
///
301+
/// Chunk tasks are spawned on the current Tokio runtime. Real speedup needs a
302+
/// multi-thread executor ([`TokioMultiThreadExecutor`]). The default
303+
/// [`TokioBackgroundExecutor`] is single-threaded, so chunks share one thread.
300304
pub fn with_parallel_chunks(mut self, parallel_chunks: Option<NonZero<usize>>) -> Self {
301305
self.io_config.parallel_chunks = parallel_chunks;
302306
self

0 commit comments

Comments
 (0)