[FLINK-40171][table-runtime] Emit and retract early-fire results in the interval join operator - #28952
[FLINK-40171][table-runtime] Emit and retract early-fire results in the interval join operator#28952weiqingy wants to merge 3 commits into
Conversation
…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.
|
Hi @RocMarshal, this is the runtime slice of the FLIP-497 stack, now that #28877 is merged. It makes the interval join emit the speculative padded row when the early-fire delay elapses, then retract and correct it if a real match arrives later. Two things worth your eye:
PTAL when you have time. Thanks! |
There was a problem hiding this comment.
Pull request overview
Implements runtime support for the EARLY_FIRE hint in stream interval joins by emitting speculative null-padded outer rows after a configured delay and retracting/replacing them when a later match arrives, with coverage for both row-time and processing-time joins.
Changes:
- Added
earlyFireDelayplumbing into interval join operators and enabled early-fire only for eligible outer joins. - Introduced fired-bit bookkeeping state and timer-driven early-fire emission + later-match retraction/correction logic.
- Added harness tests asserting the exact
+I -> -U -> +Uchangelog sequences across join types and time domains.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/TimeIntervalJoin.java | Core early-fire implementation: fired bookkeeping state, timer-based speculative pads, and match-time retraction/correction gating. |
| flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/EmitAwareCollector.java | Collector enhancements to turn a single match emit into -U/+U when retracting a previously emitted speculative pad. |
| flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/RowTimeIntervalJoin.java | Threads earlyFireDelay into the row-time interval join operator constructor. |
| flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/join/interval/ProcTimeIntervalJoin.java | Threads earlyFireDelay into the proc-time interval join operator constructor. |
| flink-table/flink-table-planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecIntervalJoin.java | Unboxes/passes the planned early-fire delay to the runtime operator (defaulting to disabled when absent). |
| flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/RowTimeIntervalJoinTest.java | Adds row-time harness tests asserting speculative pad emission and -U/+U correction sequences and edge cases. |
| flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/join/interval/ProcTimeIntervalJoinTest.java | Adds proc-time harness tests for early-fire + correction and delay edge cases. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /** | ||
| * Collector to wrap a [[org.apache.flink.table.dataformat.RowData]] and to track whether a row has | ||
| * been emitted by the inner collector. |
There was a problem hiding this comment.
Fixed in ad2fdc1. Now {@link RowData}. The old link also pointed at org.apache.flink.table.dataformat.RowData, which does not exist.
| List<Tuple2<RowData, Boolean>> rightRows = rightEntry.getValue(); | ||
| List<Boolean> rightFired = | ||
| earlyFireEnabled | ||
| ? firedBits(rightFiredState, rightTime, rightRows) | ||
| : null; |
There was a problem hiding this comment.
Gated in 7912a13. It is worse than an extra read: on a LEFT join nothing ever writes rightFiredState, so the list was always all-false.
Used isRightOuter() rather than an equality test, since that returns true for FULL as well. Adding the gate showed the suite could not tell the two apart, so I extended testRowTimeFullOuterEarlyFireOneMatches to drive the right-side retraction too. With the gate written as == FlinkJoinType.RIGHT that test now fails; before the change the whole suite stayed green.
| List<Tuple2<RowData, Boolean>> leftRows = leftEntry.getValue(); | ||
| List<Boolean> leftFired = | ||
| earlyFireEnabled ? firedBits(leftFiredState, leftTime, leftRows) : null; |
There was a problem hiding this comment.
Same fix in 7912a13, gated on isLeftOuter().
| * join function knowing about changelogs. When not armed, collected rows are forwarded with their | ||
| * existing {@link RowKind}. |
There was a problem hiding this comment.
You are right, the doc was wrong and the code is deliberate. Reworded in ad2fdc1 to say the row is stamped INSERT, and why: the join function reuses one row instance whose kind may have been left at UPDATE_AFTER by an earlier correction. testRowTimeEarlyFireRowKindIsolation covers it.
… side The early-fire fired bits are only consulted when retracting a speculative pad, which can only happen on an outer side. Gate each per-side read on that side being outer, so a one-sided outer join stops reading state it can never use on the matching path. Extend the full-outer test to also drive the right-side retraction, which the suite did not exercise before.
…adoc The doc used a Scala-style link to a class that does not exist, and stated that an unarmed collector forwards the existing row kind. It stamps INSERT instead, so a reused row instance cannot leak an UPDATE_AFTER left by an earlier correction.
|
Copilot's four comments are resolved in 7912a13 and ad2fdc1: the per-side fired-state read is now gated on that side actually being outer, and the Gating that read turned up a gap: the suite could not tell the correct gate from an equality test that would break FULL joins, so cc @RocMarshal, this one is ready for review when you have time. Thanks! |
Part of the FLIP-497 implementation stack under umbrella FLINK-36953. Landing order:
targetoption (#28827, merged)What is the purpose of the change
Implements the runtime behavior for the
EARLY_FIREhint on an interval join. An unmatched outer row is emitted speculatively with a null-padded counterpart after the configured delay; if a real match later arrives within the window, the speculative row is retracted and corrected. This covers the natural time-domain pairings: an event-time join with an event-time delay, and a processing-time join with a processing-time delay.Brief change log
earlyFireDelay, and a bookkeepingMapStatetracking whether a row has already early-fired.StreamExecIntervalJoinunboxes the delay and passes it to the operator.Verifying this change
This change added tests and can be verified as follows:
RowTimeIntervalJoinTestandProcTimeIntervalJoinTestpin the full+Ithen-Uthen+Usequence for left, right and full outer joins. The assertor compares positionally, so the ordering is enforced rather than incidental.testRowTimeEarlyFireRowKindIsolationcovers the negative direction: a pad emitted after a retraction must be a plain+I, not a leakedUPDATE_BEFORE.testRowTimeLeftOuterEarlyFireMultiMatchcovers that repeated matches of one early-fired row produce exactly one retraction.Does this pull request potentially affect one of the following parts:
@Public(Evolving): noMapState. A savepoint taken before this change restores it empty, which is safe: a row is then treated as not yet early-fired, so the only effect is a possible duplicate speculative row, never a swallowed retraction.Documentation
Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Anthropic)