Skip to content

[FLINK-40172][table-runtime] Support processing-time early fire on a row-time interval join - #28953

Draft
weiqingy wants to merge 2 commits into
apache:masterfrom
weiqingy:FLINK-36953-pr5-proctime
Draft

[FLINK-40172][table-runtime] Support processing-time early fire on a row-time interval join#28953
weiqingy wants to merge 2 commits into
apache:masterfrom
weiqingy:FLINK-36953-pr5-proctime

Conversation

@weiqingy

Copy link
Copy Markdown
Contributor

Part of the FLIP-497 implementation stack under umbrella FLINK-36953. Landing order:

Step Sub-task Scope
PR-1a FLINK-40167 EARLY_FIRE hint surface + option validation (#28353, merged)
PR-1b FLINK-40168 Thread the hint into the interval join (#28796, merged)
PR-2 FLINK-40169 target option (#28827, merged)
PR-3 FLINK-40170 Update-producing changelog mode + insert-only guard (#28877, merged)
PR-4 FLINK-40171 Runtime early-fire emit + retraction (#28952, in review)
PR-5 (this PR) FLINK-40172 Processing-time early fire on an event-time join
PR-6 FLINK-40173 State restore coverage
PR-7 FLINK-40174 User-facing documentation

Opened as a draft because it is stacked on #28952, which is in review. Until that merges, the commit list and diff here also carry PR-4's commit. Once #28952 merges I will rebase onto master, leaving only this PR's change, and take it out of draft.

What is the purpose of the change

Allows a processing-time early-fire delay on an event-time interval join. That pairing was previously rejected at planning with a "not yet supported" error. The join still matches on event time; only the early-fire trigger runs on processing time, so a speculative row can be emitted on a wall-clock delay even when watermarks are sparse or stalled.

Brief change log

  • Drop the planning guard that rejected a processing-time delay on an event-time join.
  • When the hint asks for it, register the early-fire timer on the processing-time timer service, computing the firing time as currentProcessingTime() + delay. The join's matching and its state cleanup stay on event time.
  • Track the row's event time as a bucket key in new per-side schedule state, since a processing-time firing timestamp cannot be mapped back to an event-time bucket.
  • A single onTimer serves both domains and dispatches on ctx.timeDomain().
  • StreamExecIntervalJoin passes the resolved time mode through to the operator.

Verifying this change

This change added tests and can be verified as follows:

  • EarlyFireJoinHintTest.testEarlyFireProcTimeOnRowTimeJoin flips from asserting the old rejection to a golden plan, which records earlyFireTimeMode=[PROCTIME] on a join whose bounds are isRowTime=true. The opposite pairing stays rejected, covered by testEarlyFireRowTimeOnProcTimeJoin.
  • Harness tests in RowTimeIntervalJoinTest drive the two clocks independently. One case fires the pad on a wall-clock advance with the watermark untouched. Together they cover the speculative emit, its -U/+U correction, an inner join correctly ignoring the hint, and snapshot/restore on both sides of the firing.

Does this pull request potentially affect one of the following parts:

  • Dependencies (does it add or upgrade a dependency): no
  • The public API, i.e., is any changed class annotated with @Public(Evolving): no
  • The serializers: no
  • The runtime per-record code paths (performance sensitive): yes, the interval join record path. The behavior is gated on the hint and off by default.
  • Anything that affects deployment or recovery: yes. This adds per-side early-fire schedule state alongside the bookkeeping MapState introduced in PR-4. Restore is covered by tests taking a snapshot both before and after the speculative row fires.
  • The S3 file system connector: no

Documentation

  • Does this pull request introduce a new feature? no (extends the FLIP-497 hint to a time-domain pairing that was previously rejected)
  • If yes, how is the feature documented? not applicable

Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Generated-by: Claude Code (Anthropic)

…he interval join operator

Wire the EARLY_FIRE delay into the interval join operator so an outer join
speculatively emits its padded unmatched row after the delay and corrects it
when a real match arrives. Covers the natural timer pairings: a row-time join
fires on event time, a processing-time join fires on processing time.
Processing-time triggering on a row-time join stays rejected at planning.

When an unmatched outer row is cached, the operator registers an early-fire
timer at rowTime + delay. On that timer it emits the padded row as an INSERT
and records that it fired. When the row later matches, it retracts the padded
row as UPDATE_BEFORE and emits the matched row as UPDATE_AFTER, matching the
update-producing changelog mode inferred for the node. The retraction is tied
to the one-time matched-and-emitted flip, so a row that matches several times
emits a single correction followed by ordinary inserts.

The already-fired marker is a new per-side MapState<Long, List<Boolean>> kept
positionally aligned with the existing row cache, rather than widening the
cache tuple, so the cache serializer is unchanged and old savepoints restore
the new state empty. The marker is the single gate that keeps a row padded
exactly once when the delay is at or beyond the window span. All early-fire
work is gated on the hint being set, an outer join, and a non-negative window,
so a plain interval join is unchanged and allocates nothing new.

EmitAwareCollector carries the changelog stamping so IntervalJoinFunction stays
changelog-agnostic, and every padded or matched emit stamps its RowKind
explicitly to avoid leaking a kind onto a reused row.
…row-time interval join

Add the cross-domain timer combination the previous commit left out: an
event-time interval join with EARLY_FIRE('time_mode'='proctime') now fires its
speculative pads on the wall clock while keeping its event-time cleanup. The
temporary "not yet supported" rejection in the planner rule is removed; the
row-time-on-processing-time rejection is retained.

onTimer distinguishes the two timer kinds by OnTimerContext.timeDomain(): in
the cross-domain case early-fire timers are processing-time and cleanup timers
are event-time, so a processing-time firing runs early fire and returns while
an event-time firing runs cleanup only. The discrimination is gated on a new
cross-domain flag, so the natural pairings keep the previous timestamp - delay
recovery where early fire and cleanup share a domain.

A processing-time firing timestamp cannot be mapped back to an event-time cache
bucket arithmetically, so a per-side MapState<Long, List<Long>> keyed by firing
processing-time records the event-time bucket keys due to fire then. It is
allocated only in the cross-domain case and reuses the existing per-bucket emit
and positional fired bit, so the retract-and-correct path is shared. Every
scheduled firing time fires and removes its own entry, and a bucket already
cleaned by event-time expiry makes the firing a no-op, so nothing accumulates.

The schedule is value-typed and order-preserving and processing-time timers are
checkpointed, so a timer pending at snapshot fires after restore against the
restored schedule and fired bits and emits at most the not-yet-emitted pad.
Harness tests cover the wall-clock trigger without watermark advance, a snapshot
before the timer fires, and a snapshot after the pad is emitted.
@flinkbot

flinkbot commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands The @flinkbot bot supports the following commands:
  • @flinkbot run azure re-run the last Azure build

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants