[Spark][#36841] Translate stateless streaming pipelines on the Spark 4 runner - #40090
[Spark][#36841] Translate stateless streaming pipelines on the Spark 4 runner#40090tkaymak wants to merge 1 commit into
Conversation
Makes the DataSourceV2 unbounded source from apache#39971 reachable. The Spark 4 module overrides PipelineTranslatorFactory and dispatches streaming pipelines to PipelineTranslatorStreaming, which translates unbounded reads and reuses the batch translators for stateless single output ParDo, Window.Assign, Flatten and Reshuffle. GroupByKey, Combine.perKey, stateful ParDo, ParDo with side inputs or additional outputs, Impulse and bounded reads fail at translation, the batch translators for them persist or collect the Dataset, which Spark rejects on a streaming plan. StreamingEvaluationContext runs one noop sink query per leaf, checkpoints under checkpointDir/<leaf index>, stops siblings when a query fails and stops a query after streamingStopAfterIdleBatches triggers without input. The test source of BeamMicroBatchSourceTest moves to TestUnboundedSource so the translator tests share it.
|
Assigning reviewers: R: @chamikaramj 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). |
|
Waiting for #40093 to be merged, then the Spark Precommit can run here |
|
Thanks. Close and reopen PR to triggger tests |
Abacn
left a comment
There was a problem hiding this comment.
Thanks, had a few comments.
| return super.getTransformTranslator(transform); | ||
| } | ||
|
|
||
| private static UnsupportedOperationException unsupported(String what) { |
There was a problem hiding this comment.
This is a thin wrapper. In convention exceptions should be created at the exact moment an error occurs.
|
|
||
| /** Returns a {@link TransformTranslator} for the given {@link PTransform} if known. */ | ||
| @Override | ||
| @SuppressWarnings({"rawtypes", "unchecked"}) |
There was a problem hiding this comment.
Please clean up SuppressWarnings
| LOG.warn( | ||
| "Error while stopping streaming query {}: {}", | ||
| query.id(), | ||
| String.valueOf(e.getMessage())); |
There was a problem hiding this comment.
redundant String.valueOf(String)
| assertEquals(PipelineResult.State.CANCELLED, cancelledState); | ||
| assertEquals(PipelineResult.State.CANCELLED, result.getState()); | ||
|
|
||
| long deadline = System.currentTimeMillis() + QUERY_START_TIMEOUT_MILLIS; |
There was a problem hiding this comment.
This almost duplicates awaitQueryStarted() except the loop condition. Consider generalize awaitQueryStarted -> awaitQuery(...)
| assertEquals(PipelineResult.State.FAILED, result.getState()); | ||
|
|
||
| long deadline = System.currentTimeMillis() + QUERY_START_TIMEOUT_MILLIS; | ||
| while (SESSION.getSession().streams().active().length > 0) { |
There was a problem hiding this comment.
same here (awaitQueryStarted)
| public void tearDown() { | ||
| TestUnboundedSource.forget("lifecycle-done"); | ||
| TestUnboundedSource.forget("lifecycle-cancel"); | ||
| TestUnboundedSource.forget("lifecycle-healthy"); |
There was a problem hiding this comment.
This is unusual. Generic teardown that runs after every test seems to clear some test specific resources.
|
|
||
| /** Returns a snapshot of everything collected so far under {@code collectorId}. */ | ||
| @SuppressWarnings("unchecked") | ||
| public static <T> List<T> getCollected(String collectorId) { |
There was a problem hiding this comment.
getCollected defensively new an ArrayList, while it's callers also redundantly new an ArrayList / Set.
| * for them persist or collect the Dataset, which Spark rejects for streaming plans. | ||
| */ | ||
| @Internal | ||
| public class PipelineTranslatorStreaming extends PipelineTranslatorBatch { |
There was a problem hiding this comment.
While it works and resuses some code, future change in PipelineTranslatorBatch may result in surprises on streaming path. For example, new translations meant only for batch now silently port to streaming. On the other hand, refactoring it sounds an risky choice either. At minimum can we rename "PipelineTranslatorBatch" to "PipelineTranslatorCommon" and create a thin subclass "PipelineTranslatorBatch" to it?
| toAwait = new ArrayList<>(queries); | ||
| } | ||
| awaitTermination(toAwait); | ||
| } finally { |
There was a problem hiding this comment.
Is there a risk of leak query on exception thrown in the try block? should we call stopQuery for all remaining queries in a catch?
| .writeStream() | ||
| .format("noop") | ||
| .outputMode("append") | ||
| .option("checkpointLocation", checkpointBaseDir + "/" + leafIndex) |
There was a problem hiding this comment.
consider use builtin path join method to handle trailing paths in checkpointBaseDir
Part of #36841, follows #39971 (DataSourceV2 unbounded source). This makes that source reachable: the Spark 4 runner now translates and runs stateless streaming pipelines.
Scope
Supported in streaming mode:
Read.from(UnboundedSource), stateless single outputParDowithout side inputs,Window.Assign,Flatten,Reshuffle. The last four reuse the batch translators unchanged. Everything else fails at translation with anUnsupportedOperationExceptionpointing at #36841:GroupByKey,Combine.perKey, statefulParDo,ParDowith side inputs or additional outputs,Impulseand bounded reads (soCreateandPAssert). The rejections are explicit because the batch translators for those primitives persist or collect the Dataset, which Spark refuses on a streaming plan with a rawAnalysisException.GroupByKeyand statefulParDoarrive with thetransformWithStatebridge in the next PR.Main code, four files under
runners/spark/4translation/PipelineTranslatorFactory.javashadows the shared base file that throws for streaming today. The Spark 4 module compiles the override tree with later wins, so only Spark 4 gets the streaming dispatch.translation/PipelineTranslatorStreaming.javaroutesPrimitiveUnboundedReadto the new translator, rejects the unsupported primitives, and falls back to the batch registry for the rest. Without the rejectionsGroupByKeywould silently run the batch translator against a streaming Dataset.translation/StreamingEvaluationContext.javastarts onenoopsink query per leaf with a processing time trigger ofmaxBatchDurationMillis, checkpoints undercheckpointDir/<leaf index>, blocks until every query terminates, stops siblings when one fails, and stops a query afterstreamingStopAfterIdleBatchestriggers without input when that option is set. Idle triggers arrive as zero row progress events while the source offset moves and asQueryIdleEventotherwise, the listener counts both.checkpointDirmust be set, the shared default is/tmp/<jobName>.translation/streaming/ReadUnboundedTranslator.javabuilds the Dataset throughUnboundedSourceDataset.ofand decodes the payload column with the full windowed value coder.Tests, all live
StreamingQueryrunsTestUnboundedSourceis the source thatBeamMicroBatchSourceTestused as a nested class in [Spark][#36841] Add the DataSourceV2 unbounded source for the Spark 4 streaming runner #39971, extracted so the translator tests share it. No second synthetic source.StatelessParDoStreamingTest: pass through, andFlattenof two unbounded reads.StreamingPipelineLifecycleTest: RUNNING to DONE on idle, cancel, a failing leaf fails the pipeline and stops its healthy sibling.StreamingCheckpointRestartTest: two runs against one checkpoint location with the reader cache wiped in between, the second run recreates readers from the durable marks and re-emits nothing the first run committed. The file layout itself is covered byBeamMicroBatchSourceTest, here only the wiring of the checkpoint location is asserted.PipelineTranslatorStreamingTest: the rejections surface fromrun()with the Beam message, not a Spark one.Delivery is at least once, as documented on
BeamReaderCachein #39971. NoCHANGES.mdentry yet, that comes when the runner can execute a windowedGroupByKey.R: @Abacn