[Spark][#36841] Add the DataSourceV2 unbounded source for the Spark 4 streaming runner - #39971
[Spark][#36841] Add the DataSourceV2 unbounded source for the Spark 4 streaming runner#39971tkaymak wants to merge 1 commit into
Conversation
Exposes any Beam UnboundedSource as a Spark 4 DataSourceV2 streaming table with a fixed two column schema, encoded payload plus event timestamp. Offsets are opaque, strictly increasing epoch counters, so Spark keeps scheduling micro-batches and termination stays with the lifecycle owner. Recovery is durable under the query's checkpoint location: the source id derives deterministically from the read transform's full name, the first run pins its split list (Beam sources do not guarantee deterministic splitting), and every split persists its CheckpointMark per epoch with a retention of two, written atomically via temp file and rename. Executors cache live readers between micro-batches and fall back to the newest durable mark at or before the replayed epoch after a restart. Semantics are at least once, a crash between finishing a read and Spark's commit replays the last micro-batch. The batch cutoff honors maxRecordsPerBatch, values below 1, including the default, mean no limit and the batch ends on the duration deadline.
|
Assigning reviewers: R: @tvalentyn added as fallback since no labels match configuration Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
| * | ||
| * @param <T> the element type of the wrapped source | ||
| */ | ||
| @SuppressWarnings({ |
There was a problem hiding this comment.
Avoid SuppressWarnings in new codes. If there is a compelling reason (I see there are comments here), put this in specific chunk, not whole class. This applies to the other few places.
| /** Name of the timestamp column holding the Beam event timestamp. */ | ||
| public static final String COL_EVENT_TS = "eventTimestamp"; | ||
|
|
||
| /** Upper bound on the number of splits requested from a source, keeps the POC predictable. */ |
There was a problem hiding this comment.
We need to clean up PoC hardcodes when checking in them into master branch
Consider make it in alignment with the Bounded source:
which uses session.sparkContext().defaultParallelism() or from pipeline options
| @@ -0,0 +1,236 @@ | |||
| /* | |||
There was a problem hiding this comment.
In #39576 it was noted to support streaming with TransformWithState API, which only exists in Spark 4. However, it appears current change does not yet involve TransformWithState API.
Put it in spark/4/ sounds fine as we only aim to support streaming for Spark 4. However, it may be more straightforward to re-use existing code if we work inside spark/src as long as it doesn't involve TransformWithState, as this addition-only change suggests there may be duplicated codes that should be shared with common/batch paths, see below.
| public static void writeMark( | ||
| String checkpointLocation, String sourceId, int splitId, long endEpoch, CheckpointMark mark) | ||
| throws IOException { | ||
| if (!(mark instanceof Serializable)) { |
There was a problem hiding this comment.
This restriction doesn't sounds right as Beam CheckpointMark doesn't require Serializable. We should use provided coders via source.getCheckpointMarkCoder() to serialize Beam checkpoints
| InputPartition[] partitions = new InputPartition[splits.size()]; | ||
| for (int i = 0; i < splits.size(); i++) { | ||
| partitions[i] = | ||
| new BeamInputPartition( |
There was a problem hiding this comment.
maxRecordsPerBatch is passed to every BeamInputPartition, results in each micro-batch actually pulling (maxRecordsPerBatch * splits) records, inconsistent with SparkStructuredStreamingPipelineOptions.getMaxRecordsPerBatch() ("Max records per micro-batch"). definition.
In
, splitNumRecords(maxNumRecords, numSplits) evenly partitions the record quota across splits. BeamMicroBatchStream.planInputPartitions should use the same distribution logic.| } | ||
|
|
||
| private static FileSystem fileSystem(Path path) throws IOException { | ||
| return path.getFileSystem(new Configuration()); |
There was a problem hiding this comment.
If we decide to stay with hadoop-file-based checkpointing, a recommendation is to use CheckpointFileManager:
Spark uses this for its own /offsets and /commits.
Currently a default new Configuration() is subject to fail on a cloud based file system.
| private static final String TMP_SUFFIX = ".tmp"; | ||
|
|
||
| /** Number of most recent mark files retained per split. */ | ||
| private static final int RETAINED_MARKS = 2; |
There was a problem hiding this comment.
We should revisit this hard coded number and throughout the PR.
RETAINED_MARKS = 2 is dangerous because Spark's offset log keeps 100 batches by default (spark.sql.streaming.minBatchesToRetain). If a restarted query rewinds 3 batches, the mark is missing and the stream replays from scratch. Keep at least minBatchesToRetain marks, or delete old marks only upon MicroBatchStream.commit(Offset).
| .build(); | ||
|
|
||
| /** Last known checkpoint mark per key, used when a reader has to be recreated. */ | ||
| private static final ConcurrentMap<String, CheckpointMark> MARKS = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Reference leak possible as MARKS holds references to CheckpointMark indefinitely, unless invalidateAll() gets called
|
|
||
| /** Parses the form produced by {@link #json()}, a bare number is also accepted. */ | ||
| public static BeamOffset fromJson(String json) { | ||
| Matcher matcher = EPOCH_PATTERN.matcher(json); |
There was a problem hiding this comment.
It may work by coincidence, doesn't sounds semantically correct way to extract an offset
| current = toRow(); | ||
| return true; | ||
| } | ||
| Uninterruptibles.sleepUninterruptibly( |
There was a problem hiding this comment.
What is the consideration of sleepUninterruptibly? Consider using Beam's FluentBackoff
|
Thank you for the review @Abacn! |
Third slice of the Spark 4 Structured Streaming work split out of #39576, following the dispatch seam (#39906) and the Kryo registrations (#39939). Addresses #36841.
This adds the DataSourceV2 micro-batch source that exposes any Beam UnboundedSource as a Spark 4 streaming table. All 12 files are new, nothing existing changes.
Design notes:
Tests cover element delivery, watermark tracking through typed maps, the epoch offset round trip, the unlimited default, the checkpoint file layout with retention, epoch fast forward and the deterministic source id. The end of stream sentinel used by PAssert arrives with the translators slice, its hooks are deliberately absent here.
Remaining slices: the state and timer bridge on transformWithState, then the translators with the end to end tests. End to end evidence remains in draft #39576.
R: @Abacn