Repository navigation
Fix flaky Java tests #40298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Fix flaky Java tests #40298
Changes from all commits
dbedcbe
48b0e15
deae9c3
c65cb93
e10ec7f
e655aa0
d687e01
50935e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,10 +18,10 @@ | |
| package org.apache.beam.runners.prism; | ||
|
|
||
| import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument; | ||
| import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.ServerSocket; | ||
| import java.net.Socket; | ||
| import java.util.Arrays; | ||
| import org.apache.beam.runners.portability.PortableRunner; | ||
| import org.apache.beam.sdk.Pipeline; | ||
|
|
@@ -46,6 +46,7 @@ | |
| public class PrismRunner extends PipelineRunner<PipelineResult> { | ||
|
|
||
| private static final Logger LOG = LoggerFactory.getLogger(PrismRunner.class); | ||
| private static final int MAX_START_ATTEMPTS = 5; | ||
|
|
||
| private final PrismPipelineOptions prismPipelineOptions; | ||
|
|
||
|
|
@@ -82,19 +83,37 @@ public PipelineResult run(Pipeline pipeline) { | |
| prismPipelineOptions.getDefaultEnvironmentType(), | ||
| prismPipelineOptions.getJobEndpoint()); | ||
|
|
||
| try { | ||
| PrismExecutor executor = startPrism(); | ||
| PortableRunner delegate = PortableRunner.fromOptions(prismPipelineOptions); | ||
| return new PrismPipelineResult(delegate.run(pipeline), executor::stop); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException(e); | ||
| for (int attempt = 1; ; attempt++) { | ||
| PrismExecutor executor; | ||
| try { | ||
| executor = startPrism(); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| try { | ||
| PortableRunner delegate = PortableRunner.fromOptions(prismPipelineOptions); | ||
| return new PrismPipelineResult(delegate.run(pipeline), executor::stop); | ||
| } catch (RuntimeException e) { | ||
| boolean alive = executor.isAlive(); | ||
| executor.stop(); | ||
| if (attempt < MAX_START_ATTEMPTS | ||
| && (!alive | ||
| || (e.getMessage() != null && e.getMessage().contains("JobService/Prepare")))) { | ||
| LOG.warn( | ||
| "Prism job submission failed on attempt {} (alive={}), retrying on a new port...", | ||
| attempt, | ||
| alive, | ||
| e); | ||
| continue; | ||
| } | ||
| throw e; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| PrismExecutor startPrism() throws IOException { | ||
| PrismLocator locator = new PrismLocator(prismPipelineOptions); | ||
| int port = findAvailablePort(); | ||
| String portFlag = String.format(PrismExecutor.JOB_PORT_FLAG_TEMPLATE, port); | ||
| String command = locator.resolve(); | ||
| String serveHttpFlag = | ||
| String.format( | ||
| PrismExecutor.SERVE_HTTP_FLAG_TEMPLATE, prismPipelineOptions.getEnableWebUI()); | ||
|
|
@@ -104,18 +123,52 @@ PrismExecutor startPrism() throws IOException { | |
| String logLevelFlag = | ||
| String.format( | ||
| PrismExecutor.LOG_LEVEL_FLAG_TEMPLATE, prismPipelineOptions.getPrismLogLevel()); | ||
| String endpoint = "localhost:" + port; | ||
| prismPipelineOptions.setJobEndpoint(endpoint); | ||
| String command = locator.resolve(); | ||
| PrismExecutor executor = | ||
| PrismExecutor.builder() | ||
| .setCommand(command) | ||
| .setArguments( | ||
| Arrays.asList(portFlag, serveHttpFlag, idleShutdownTimeoutFlag, logLevelFlag)) | ||
| .build(); | ||
| executor.execute(); | ||
| checkState(executor.isAlive()); | ||
| return executor; | ||
|
|
||
| for (int attempt = 1; ; attempt++) { | ||
| int port = findAvailablePort(); | ||
| String portFlag = String.format(PrismExecutor.JOB_PORT_FLAG_TEMPLATE, port); | ||
| String endpoint = "localhost:" + port; | ||
| prismPipelineOptions.setJobEndpoint(endpoint); | ||
| PrismExecutor executor = | ||
| PrismExecutor.builder() | ||
| .setCommand(command) | ||
| .setArguments( | ||
| Arrays.asList(portFlag, serveHttpFlag, idleShutdownTimeoutFlag, logLevelFlag)) | ||
| .build(); | ||
| executor.execute(); | ||
| if (waitForPort(executor, port)) { | ||
| return executor; | ||
| } | ||
| executor.stop(); | ||
| String message = | ||
| String.format( | ||
| "Prism failed to start listening on port %d (attempt %d/%d)", | ||
| port, attempt, MAX_START_ATTEMPTS); | ||
| LOG.warn("{}", message); | ||
| if (attempt >= MAX_START_ATTEMPTS) { | ||
| throw new IOException(message); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static boolean waitForPort(PrismExecutor executor, int port) { | ||
| long deadlineMillis = System.currentTimeMillis() + 10_000L; | ||
| while (System.currentTimeMillis() < deadlineMillis) { | ||
| if (!executor.isAlive()) { | ||
| return false; | ||
| } | ||
| try (Socket socket = new Socket("localhost", port)) { | ||
| return executor.isAlive(); | ||
| } catch (IOException ignored) { | ||
| try { | ||
| Thread.sleep(10L); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
| return executor.isAlive(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this return |
||
| } | ||
|
|
||
| private static void assignDefaultsIfNeeded(PrismPipelineOptions prismPipelineOptions) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,8 +177,13 @@ public SparkStructuredStreamingPipelineResult run(final Pipeline pipeline) { | |
| ctxRef.set(ctx); | ||
| if (!cancelRequested.get()) { | ||
| ctx.evaluate(); | ||
| } else { | ||
| cancelSparkJobs.run(); | ||
| } | ||
| } finally { | ||
| if (cancelRequested.get()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sounds like related to #40101, which has been fixed recently
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW pr/40103 (fix for 40101) actually introduced some of the flaky tests that this PR tries to address, so the flakes seen by this PR postdate 40103. |
||
| cancelSparkJobs.run(); | ||
| } | ||
| if (releaseSession) { | ||
| SparkSessionFactory.release(sparkSession); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retry can reduce flakiness in general. However on genuine failure, keep retrying only delays the program execution. We should only retry transient failures (e.g. port conflict)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we expect genuine failures on starting the Prism runner?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC Java's prism runner wrapper downloads a prism binary if not already exist in
.cache/apache_beam. If downloads fail (no access to github.com; platform not have pre-build binary; run on a dev version and the binary not built separately; blocked by Stanta; etc) it could fail