Class ParallelParsingCoordinator
Inspired by ClickHouse's ParallelParsingInputFormat. The approach:
- A segmentator divides the file into N byte-range segments at record boundaries
- Each segment is parsed independently on a separate executor thread
- The coordinator yields pages as they become ready via a
CloseableIterator
Row ordering. Cross-segment row order is intentionally not preserved:
an external scan has no row-order guarantee absent an explicit SORT, and the
read schema is bound up-front (see parallelRead(org.elasticsearch.xpack.esql.datasources.spi.SegmentableFormatReader, org.elasticsearch.xpack.esql.datasources.spi.StorageObject, java.util.List<java.lang.String>, int, int, java.util.concurrent.Executor)) so segment 0 has no obligation
to emit first. Holding pages back to reconstruct segment order is what created the bug
this design fixes: a parsed-but-not-yet-emitted segment kept its object-store socket open
and idle until the in-order cursor reached it; on S3 that idle socket exceeds the server
idle timeout and is reset, surfacing as an HTTP 500. Emitting as-ready lets each segment's
socket close as soon as its pages are consumed.
This coordinator only works with SegmentableFormatReader implementations
(line-oriented formats like CSV and NDJSON). Columnar formats have their own
row-group-level parallelism.
-
Method Summary
Modifier and TypeMethodDescriptionstatic List<long[]> computeSegments(SegmentableFormatReader reader, StorageObject storageObject, long fileLength, int parallelism, long minSegment) Computes byte-range segments for the file by probing record boundaries.static List<long[]> computeSegments(SegmentableFormatReader reader, StorageObject storageObject, long fileLength, int parallelism, long minSegment, int maxRecordBytes) Computes byte-range segments using a splitter capped bymaxRecordBytes.static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor) Creates a parallel-parsing iterator over a single storage object.static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy) Creates a parallel-parsing iterator with an explicit error policy.static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary) Convenience overload that forwardssplitIncludesFileLeader=true.static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader) static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, int maxConcurrentOpenSegments) Convenience overload that adds themax_concurrent_open_segmentscap without per-chunk source-stats capture (equivalent to passingcaptureSink == null).static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, int maxConcurrentOpenSegments, ConcurrentMap<String, List<Map<String, Object>>> captureSink) Full-control overload that takes both themax_concurrent_open_segmentscap and an explicitcaptureSinkfor per-chunk source-stats contributions.static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset) Forwards to the full-control overload with thedefaultopen-segment cap andcaptureSink=null(text-format readers' close hooks then publish into whateverExternalStatsCapturesink — if any — happens to be bound on the calling thread).static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset, int maxConcurrentOpenSegments, ConcurrentMap<String, List<Map<String, Object>>> captureSink, int maxRecordBytes, long statsStripeSize, StripeColumnScope statsColumnScope, boolean splitIsFileFinal) Full-control overload that also takes themax_record_sizecap used by record splitters, the file-global byte base offset, and the canonical-stripe grid for per-stripe stats attribution (<= 0disables; pure overlay).static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset, int maxConcurrentOpenSegments, ConcurrentMap<String, List<Map<String, Object>>> captureSink, int maxRecordBytes, long statsStripeSize, StripeColumnScope statsColumnScope, boolean splitIsFileFinal, ExternalSourceMetrics metrics) As thecaptureSink+maxRecordBytesoverload, plus a node telemetry sink used to record areader.pool.rejectedevent when a parser-segment submission is rejected (executor saturated / shutting down).static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset, int maxConcurrentOpenSegments, ConcurrentMap<String, List<Map<String, Object>>> captureSink, int maxRecordBytes, long statsStripeSize, StripeColumnScope statsColumnScope, boolean splitIsFileFinal, ExternalSourceMetrics metrics, Consumer<String> warningSink) As themetricsoverload, plus a relay for client-visible lenient-policy warnings raised while parsing a segment (seeSkipWarnings).
-
Method Details
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor) throws IOException Creates a parallel-parsing iterator over a single storage object.The file is divided into
parallelismsegments at record boundaries. Each segment is parsed independently and pages are yielded as they become ready (completion order, not segment order). If the file is too small for meaningful parallelism (below the reader'sSegmentableFormatReader.minimumSegmentSize()per segment), falls back to single-threaded reading.- Parameters:
reader- the segmentable format readerstorageObject- the file to readprojectedColumns- columns to projectbatchSize- rows per pageparallelism- number of parallel parser threadsexecutor- executor for parser threads- Returns:
- an iterator that yields pages as they become ready (cross-segment row order not preserved)
- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy) throws IOException Creates a parallel-parsing iterator with an explicit error policy.- Parameters:
errorPolicy- error handling policy for per-segment parsing, ornullfor defaults- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary) throws IOException Convenience overload that forwardssplitIncludesFileLeader=true. This assumes the storage object includes the file's leading bytes (header row). For non-leading macro-splits, use the nine-argument overload with explicitsplitIncludesFileLeader=false.- Parameters:
splitStartsAtRecordBoundary- whentrue,storageObjectis a byte range that already begins on a record boundary (e.g. newline-aligned macroFileSplit); single-threaded fallback reads must setFormatReadContext.recordAligned().- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader) throws IOException - Parameters:
splitStartsAtRecordBoundary- whentrue,storageObjectis a byte range that already begins on a record boundary (e.g. newline-aligned macroFileSplit); single-threaded fallback reads must setFormatReadContext.recordAligned().splitIncludesFileLeader- whether this split contains the file-leading bytes (and therefore file header for header-bearing formats). For whole-file reads this istrue; for non-leading macro-splits this isfalse.- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset) throws IOException Forwards to the full-control overload with thedefaultopen-segment cap andcaptureSink=null(text-format readers' close hooks then publish into whateverExternalStatsCapturesink — if any — happens to be bound on the calling thread). Callers that resolve themax_concurrent_open_segmentspragma or care about cross-thread stats capture use the 12-arg overload.- Parameters:
readSchema- planner-bound read schema, ornullfor per-file inferencebaseFileOffset- file-global byte offset ofstorageObject's first byte (i.e. the macro split'sFileSplit.offset()).0for whole-file reads. Added to each segment's split-relative offset so readers emit file-global, split-invariant record positions on the_rowPositionchannel.- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, int maxConcurrentOpenSegments) throws IOException Convenience overload that adds themax_concurrent_open_segmentscap without per-chunk source-stats capture (equivalent to passingcaptureSink == null).- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, int maxConcurrentOpenSegments, @Nullable ConcurrentMap<String, List<Map<String, throws IOExceptionObject>>> captureSink) Full-control overload that takes both themax_concurrent_open_segmentscap and an explicitcaptureSinkfor per-chunk source-stats contributions.maxConcurrentOpenSegmentsis the per-file limit on byte-range segments whose read streams are open at once. A sliding window dispatches at mostmaxConcurrentOpenSegmentssegments at a time; each completion opens the next. This caps the open-stream / buffer count independent of file count and length. SeeParallelParsingCoordinator.AsReadyParallelIterator.Each segment is parsed on a worker thread; the worker binds
captureSinkaround the per-segmentCloseable.close()so text-format readers' close hooks publish their_stats.*contribution into the same sink the consumer-thread wrapper sees. Passnullwhen no capture is desired (tests, benchmarks).- Parameters:
maxConcurrentOpenSegments- per-file cap on concurrently-open segment streams (>= 1)captureSink- consumer-owned per-file stats sink to bind on each parser worker, ornullto disable capture- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset, int maxConcurrentOpenSegments, @Nullable ConcurrentMap<String, List<Map<String, throws IOExceptionObject>>> captureSink, int maxRecordBytes, long statsStripeSize, StripeColumnScope statsColumnScope, boolean splitIsFileFinal) Full-control overload that also takes themax_record_sizecap used by record splitters, the file-global byte base offset, and the canonical-stripe grid for per-stripe stats attribution (<= 0disables; pure overlay).- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset, int maxConcurrentOpenSegments, @Nullable ConcurrentMap<String, List<Map<String, throws IOExceptionObject>>> captureSink, int maxRecordBytes, long statsStripeSize, StripeColumnScope statsColumnScope, boolean splitIsFileFinal, ExternalSourceMetrics metrics) As thecaptureSink+maxRecordBytesoverload, plus a node telemetry sink used to record areader.pool.rejectedevent when a parser-segment submission is rejected (executor saturated / shutting down). PassExternalSourceMetrics.NOOPto disable (all narrower overloads do).- Throws:
IOException
-
parallelRead
public static CloseableIterator<Page> parallelRead(SegmentableFormatReader reader, StorageObject storageObject, List<String> projectedColumns, int batchSize, int parallelism, Executor executor, ErrorPolicy errorPolicy, boolean splitStartsAtRecordBoundary, boolean splitIncludesFileLeader, List<Attribute> readSchema, long baseFileOffset, int maxConcurrentOpenSegments, @Nullable ConcurrentMap<String, List<Map<String, throws IOExceptionObject>>> captureSink, int maxRecordBytes, long statsStripeSize, StripeColumnScope statsColumnScope, boolean splitIsFileFinal, ExternalSourceMetrics metrics, @Nullable Consumer<String> warningSink) As themetricsoverload, plus a relay for client-visible lenient-policy warnings raised while parsing a segment (seeSkipWarnings). Segment parsing runs onexecutorworker threads, so a directHeaderWarningcall there would attach to the wrong thread's response headers and be dropped; production passesAsyncExternalSourceBuffer::recordInformationalWarningso the operator can re-emit it on the driver thread without flipping the response'sis_partialflag (these are per-record skip/null-fill warnings, not a truncation of the whole read — seeAsyncExternalSourceBuffer#recordWarningfor that case). Passnullto fall back to a directHeaderWarningcall on the parsing thread (tests, benchmarks).- Throws:
IOException
-
computeSegments
public static List<long[]> computeSegments(SegmentableFormatReader reader, StorageObject storageObject, long fileLength, int parallelism, long minSegment) throws IOException Computes byte-range segments for the file by probing record boundaries. Each segment is a[offset, length]pair. The first segment starts at offset 0; subsequent segments start at the record boundary found after the nominal split point.- Throws:
IOException
-
computeSegments
public static List<long[]> computeSegments(SegmentableFormatReader reader, StorageObject storageObject, long fileLength, int parallelism, long minSegment, int maxRecordBytes) throws IOException Computes byte-range segments using a splitter capped bymaxRecordBytes.- Throws:
IOException
-