Skip to content

[Flink] Fix BuildPipeline.execStep NPE on steps that return null - #4486

Merged
wolfboys merged 1 commit into
apache:devfrom
88fantasy:fix/build-pipeline-null-step-result
Aug 13, 2026
Merged

[Flink] Fix BuildPipeline.execStep NPE on steps that return null#4486
wolfboys merged 1 commit into
apache:devfrom
88fantasy:fix/build-pipeline-null-step-result

Conversation

@88fantasy

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request

Issue Number: close #4481

Fixes BuildPipeline#execStep() throwing NullPointerException whenever a pipeline step's Callable legitimately returns null — which is the normal case for steps that only perform a side effect (e.g. "create/clean the build workspace") and have nothing meaningful to hand to the next step. This breaks the build/release pipeline for a Flink SQL application (and, per the ~20 affected call sites, potentially every deploy mode) at its very first step.

Brief change log

  • BuildPipeline#execStep(): return R directly instead of Optional<R>, and throw pipelineException() directly from the catch block on failure, instead of returning Optional.empty() for the caller to detect via .orElseThrow(...). This removes the null ambiguity entirely — Java Optional cannot distinguish "empty because the step failed" from "empty because it succeeded with a null value," which is exactly what made a one-line Optional.ofOptional.ofNullable swap insufficient (it would just make .orElseThrow() mis-fire on every successful null-returning step instead of crashing at Optional.of).
  • Drop the now-redundant .orElseThrow(() -> pipelineException()) / .orElseThrow(this::pipelineException) at all ~20 call sites: BuildPipeline#runYarnSqlBuildSteps, FlinkRemoteBuildPipeline, FlinkK8sSessionBuildPipeline, AbstractK8sApplicationBuildPipeline, FlinkK8sApplicationBuildPipeline, SparkK8sApplicationBuildPipeline.

No caller had extra logic in its orElseThrow lambda beyond throw pipelineException(); — every occurrence checked and removed identically.

Verifying this change

Manually verified against a real Flink 2.2.1 standalone cluster:

  • Before this change: POST /flink/pipe/build for a Flink SQL application always fails at step 1/2 ("Create building workspace") with NullPointerException at Optional.of(null), even though the workspace directory is genuinely created on disk. The failure is invisible via the REST API (hasError: false, errorSummary: null) and only shows up in raw stdout (streampark.out), not the console's own error log — because the exception is thrown from a background thread pool task before GlobalExceptionHandler is ever involved.
  • After this change: POST /flink/pipe/build for the same application completes with pipeStatus: SUCCESS for both steps, and produces a real 6.2 MB shaded jar at workspace/workspace/<appId>/streampark-flinkjob_<jobName>.jar.

./mvnw -pl streampark-flink/streampark-flink-packer,streampark-console/streampark-console-service -am clean compile checkstyle:check spotless:check passes with 0 violations across all touched modules.

Does this pull request potentially affect one of the following parts

  • Dependencies (does it add or upgrade a dependency): no
  • Anything that affects deployment: no
  • The persistence of application state: no
  • The direction of network connections: no
  • Anything that affects any api: no (internal BuildPipeline contract used only within streampark-flink-packer and its console caller; execStep/runYarnSqlBuildSteps are protected, not part of any public/external API)

… in BuildPipeline

execStep() wrapped every successful step result in Optional.of(result), which
throws NullPointerException whenever a step's Callable legitimately returns
null -- and returning null is the normal case for steps that only perform a
side effect (e.g. "create/clean the build workspace") and have nothing
meaningful to hand to the next step. This is a textbook Scala-to-Java
migration bug: the original Scala code almost certainly used Option(x), which
tolerates null, not the strict Optional.of(x).

Every one of BuildPipeline's ~20 call sites already followed the same
execStep(seq, ...).orElseThrow(() -> pipelineException()) pattern purely to
turn a failed step into a thrown exception -- Optional was being used as a
success/failure signal, not as a genuine "maybe absent" value, so it could
never actually represent "succeeded, but there's no value" once any step
returned null. FlinkRemoteBuildPipeline's step 1 hit exactly that: it
succeeded (the workspace really was created), but execStep threw
NullPointerException at Optional.of(null) before the SUCCESS status update
even had a chance to matter, and the console reported the pipeline as failed
building the app jar for every Flink SQL application.

Change execStep() to return R directly and throw pipelineException() itself
on failure -- the exact exception every caller was already constructing via
orElseThrow(). This removes the null ambiguity entirely: a normal return means
success (whatever the value, including null), and failure is always signaled
by an exception, matching how the callers actually use it. Drop the now
redundant .orElseThrow(...) at all 20 call sites across
BuildPipeline#runYarnSqlBuildSteps, FlinkRemoteBuildPipeline,
FlinkK8sSessionBuildPipeline, AbstractK8sApplicationBuildPipeline,
FlinkK8sApplicationBuildPipeline, and SparkK8sApplicationBuildPipeline.

Reproduced via POST /flink/pipe/build against a real Flink 2.2.1 standalone
cluster: the build pipeline for a Flink SQL application always failed at step
1/2 with this exact NullPointerException, confirmed via the stdout log
(streampark.out; this exception is thrown before the console's own
GlobalExceptionHandler ever sees a request, so it never reaches the ERROR
log file or the persisted ApplicationBuildPipeline.errorSummary).
@github-actions github-actions Bot added the FLINK label Aug 12, 2026
@sonarqubecloud

Copy link
Copy Markdown

@wolfboys wolfboys left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@wolfboys
wolfboys merged commit 41f88c2 into apache:dev Aug 13, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] BuildPipeline.execStep throws NullPointerException for any step that legitimately returns null

2 participants