Skip to content

Fix output bugs in FnApiDoFnRunner @OnTimer and @OnWindowExpiration - #40024

Open
kennknowles wants to merge 1 commit into
apache:masterfrom
kennknowles:FnApiDoFnRunner
Open

Fix output bugs in FnApiDoFnRunner @OnTimer and @OnWindowExpiration#40024
kennknowles wants to merge 1 commit into
apache:masterfrom
kennknowles:FnApiDoFnRunner

Conversation

@kennknowles

@kennknowles kennknowles commented Sep 4, 2026

Copy link
Copy Markdown
Member

The three argument providers each carry their own near-identical copy of the output-receiver plumbing, and the @OnTimer and @OnWindowExpiration copies had drifted from the working @ProcessElement one:

  • OnTimerContext.outputWindowedValue(tag, ...) had an empty body, so tagged windowed output from @OnTimer was silently dropped.
  • OnTimerContext's tagged receivers passed no tag to outputWindowedValue, so output to a side tag went to the main output.
  • The @OnTimer row receivers built from WindowedValues.builder( currentElement) or called withValue() on a fresh builder. Neither works during timer processing: currentElement is only set while processing an element, and withValue() reads timestamp/window/pane off the builder it is called on. Both now seed from currentTimer, matching the @OnWindowExpiration equivalents.
  • OnWindowExpirationContext read currentElement.getValueKind() while currentElement was null. Window expiration emits new records, so this is ValueKind.INSERT.
  • OnWindowExpirationContext.timeDomain() returned currentTimeDomain, which processOnWindowExpiration never sets. TimeDomain is not an allowed @OnWindowExpiration parameter, so drop the override and let BaseArgumentProvider reject it.
  • outputWindowedValue(tag, ...) skipped the unknown-tag check its siblings perform, so a bad tag produced an NPE.
  • processTimer's finally did not clear causedByDrain, unlike processOnWindowExpiration. Not observable today since every entry point sets it before invoking user code, but the asymmetry is a trap.

The two inner Context classes are renamed to TimerContext and WindowExpirationContext so the fields holding them can drop their raw types; the enclosing providers are generic in K, and errorprone's SameNameButDifferent rejects the bare Context name.

Please add a meaningful description for your change here


Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Mention the appropriate issue in your description (for example: addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, comment fixes #<ISSUE NUMBER> instead.
  • Update CHANGES.md with noteworthy changes.
  • If this contribution is large, please file an Apache Individual Contributor License Agreement.

See the Contributor Guide for more tips on how to make review process smoother.

To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md

GitHub Actions Tests Status (on master branch)

Build python source distribution and wheels
Python tests
Java tests
Go tests

See CI.md for more information about GitHub Actions CI or the workflows README to see a list of phrases to trigger workflows.

The three argument providers each carry their own near-identical copy of
the output-receiver plumbing, and the @ontimer and @OnWindowExpiration
copies had drifted from the working @ProcessElement one:

- OnTimerContext.outputWindowedValue(tag, ...) had an empty body, so
  tagged windowed output from @ontimer was silently dropped.
- OnTimerContext's tagged receivers passed no tag to
  outputWindowedValue, so output to a side tag went to the main output.
- The @ontimer row receivers built from WindowedValues.builder(
  currentElement) or called withValue() on a fresh builder. Neither
  works during timer processing: currentElement is only set while
  processing an element, and withValue() reads timestamp/window/pane
  off the builder it is called on. Both now seed from currentTimer,
  matching the @OnWindowExpiration equivalents.
- OnWindowExpirationContext read currentElement.getValueKind() while
  currentElement was null. Window expiration emits new records, so this
  is ValueKind.INSERT.
- OnWindowExpirationContext.timeDomain() returned currentTimeDomain,
  which processOnWindowExpiration never sets. TimeDomain is not an
  allowed @OnWindowExpiration parameter, so drop the override and let
  BaseArgumentProvider reject it.
- outputWindowedValue(tag, ...) skipped the unknown-tag check its
  siblings perform, so a bad tag produced an NPE.
- processTimer's finally did not clear causedByDrain, unlike
  processOnWindowExpiration. Not observable today since every entry
  point sets it before invoking user code, but the asymmetry is a trap.

The two inner Context classes are renamed to TimerContext and
WindowExpirationContext so the fields holding them can drop their raw
types; the enclosing providers are generic in K, and errorprone's
SameNameButDifferent rejects the bare Context name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the java label Sep 4, 2026
@kennknowles
kennknowles marked this pull request as ready for review September 4, 2026 19:30
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.

1 participant