Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.apache.flink.streaming.api.transformations.TwoInputTransformation;
import org.apache.flink.streaming.api.transformations.UnionTransformation;
import org.apache.flink.table.api.TableException;
import org.apache.flink.table.api.config.EarlyFireJoinHintOptions;
import org.apache.flink.table.api.config.ExecutionConfigOptions;
import org.apache.flink.table.data.RowData;
import org.apache.flink.table.planner.delegation.PlannerBase;
Expand Down Expand Up @@ -59,11 +60,14 @@

import org.apache.flink.shaded.guava33.com.google.common.collect.Lists;
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonInclude;
import org.apache.flink.shaded.jackson2.com.fasterxml.jackson.annotation.JsonProperty;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;

import java.util.List;

/** {@link StreamExecNode} for a time interval stream join. */
Expand Down Expand Up @@ -91,13 +95,27 @@ public class StreamExecIntervalJoin extends ExecNodeBase<RowData>
public static final String INTERVAL_JOIN_TRANSFORMATION = "interval-join";

public static final String FIELD_NAME_INTERVAL_JOIN_SPEC = "intervalJoinSpec";
public static final String FIELD_NAME_EARLY_FIRE_DELAY = "earlyFireDelay";
public static final String FIELD_NAME_EARLY_FIRE_TIME_MODE = "earlyFireTimeMode";

@JsonProperty(FIELD_NAME_INTERVAL_JOIN_SPEC)
private final IntervalJoinSpec intervalJoinSpec;

@Nullable
@JsonProperty(FIELD_NAME_EARLY_FIRE_DELAY)
@JsonInclude(JsonInclude.Include.NON_NULL)
private final Long earlyFireDelay;

@Nullable
@JsonProperty(FIELD_NAME_EARLY_FIRE_TIME_MODE)
@JsonInclude(JsonInclude.Include.NON_NULL)
private final EarlyFireJoinHintOptions.TimeMode earlyFireTimeMode;

public StreamExecIntervalJoin(
ReadableConfig tableConfig,
IntervalJoinSpec intervalJoinSpec,
@Nullable Long earlyFireDelay,
@Nullable EarlyFireJoinHintOptions.TimeMode earlyFireTimeMode,
InputProperty leftInputProperty,
InputProperty rightInputProperty,
RowType outputType,
Expand All @@ -107,6 +125,8 @@ public StreamExecIntervalJoin(
ExecNodeContext.newContext(StreamExecIntervalJoin.class),
ExecNodeContext.newPersistedConfig(StreamExecIntervalJoin.class, tableConfig),
intervalJoinSpec,
earlyFireDelay,
earlyFireTimeMode,
Lists.newArrayList(leftInputProperty, rightInputProperty),
outputType,
description);
Expand All @@ -118,12 +138,17 @@ public StreamExecIntervalJoin(
@JsonProperty(FIELD_NAME_TYPE) ExecNodeContext context,
@JsonProperty(FIELD_NAME_CONFIGURATION) ReadableConfig persistedConfig,
@JsonProperty(FIELD_NAME_INTERVAL_JOIN_SPEC) IntervalJoinSpec intervalJoinSpec,
@Nullable @JsonProperty(FIELD_NAME_EARLY_FIRE_DELAY) Long earlyFireDelay,
@Nullable @JsonProperty(FIELD_NAME_EARLY_FIRE_TIME_MODE)
EarlyFireJoinHintOptions.TimeMode earlyFireTimeMode,
@JsonProperty(FIELD_NAME_INPUT_PROPERTIES) List<InputProperty> inputProperties,
@JsonProperty(FIELD_NAME_OUTPUT_TYPE) RowType outputType,
@JsonProperty(FIELD_NAME_DESCRIPTION) String description) {
super(id, context, persistedConfig, inputProperties, outputType, description);
Preconditions.checkArgument(inputProperties.size() == 2);
this.intervalJoinSpec = Preconditions.checkNotNull(intervalJoinSpec);
this.earlyFireDelay = earlyFireDelay;
this.earlyFireTimeMode = earlyFireTimeMode;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
package org.apache.flink.table.planner.plan.rules.physical.stream;

import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.table.api.TableException;
import org.apache.flink.table.api.ValidationException;
import org.apache.flink.table.api.config.EarlyFireJoinHintOptions;
import org.apache.flink.table.api.config.EarlyFireJoinHintOptions.TimeMode;
import org.apache.flink.table.planner.calcite.FlinkTypeFactory;
import org.apache.flink.table.planner.hint.JoinStrategy;
import org.apache.flink.table.planner.plan.nodes.FlinkRelNode;
import org.apache.flink.table.planner.plan.nodes.exec.spec.IntervalJoinSpec;
import org.apache.flink.table.planner.plan.nodes.logical.FlinkLogicalJoin;
Expand All @@ -32,11 +36,16 @@
import org.apache.calcite.plan.RelOptRuleCall;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.hint.RelHint;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexNode;
import org.immutables.value.Value;

import javax.annotation.Nullable;

import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -133,6 +142,8 @@ public FlinkRelNode transform(
RelTraitSet providedTraitSet) {
Tuple2<Option<IntervalJoinSpec.WindowBounds>, Option<RexNode>> tuple2 =
extractWindowBounds(join);
boolean isEventTime = tuple2.f0.get().isEventTime();
EarlyFire earlyFire = extractEarlyFire(join.getHints(), isEventTime);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

anchor-A

return new StreamPhysicalIntervalJoin(
join.getCluster(),
providedTraitSet,
Expand All @@ -141,7 +152,53 @@ public FlinkRelNode transform(
join.getJoinType(),
join.getCondition(),
tuple2.f1.getOrElse(() -> join.getCluster().getRexBuilder().makeLiteral(true)),
tuple2.f0.get());
tuple2.f0.get(),
earlyFire.delay,
earlyFire.timeMode);
Comment on lines +156 to +157

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At Anchor-A, the early fire parameters are now encapsulated within a dedicated class. That said, the call sites currently split them apart for invocation.

Given the semantics of this link and its subsequent stages, are there any specific downsides to propagating these parameters strictly via the EarlyFire(or EarlyFireConfig) wrapper?

Since these two parameters are almost always co-located, I’m merely questioning the trade-off. If using EarlyFire(or EarlyFireConfig) proves cumbersome, decoupling them is an acceptable compromise.

And I'd like to hear more ideas about it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @RocMarshal for the review and the +1.

Good observation. EarlyFire is a small planner-internal holder for the result of extractEarlyFire(), effectively Java's stand-in for a tuple, because that method resolves the effective time mode and performs the domain validation together. It is unpacked once at its single call site into the two values propagated downstream.

My reason for continuing to propagate them separately is the eventual ExecNode boundary, which is also the compiled-plan serde boundary. earlyFireDelay and earlyFireTimeMode are persisted as two optional scalar properties on StreamExecIntervalJoin. Keeping them flat follows existing ExecNode patterns for small optional properties and avoids adding a nullable nested spec to the plan format for two values. IntervalJoinSpec is nested, but it represents an always-present group of interval-join properties, whereas early fire itself is optional.

Regarding whether more configuration will join these values: the next PR adds a target option, but it remains a rule-level applicability gate. It determines whether the hint applies to this operator kind and is discarded after extraction, so the propagated state remains exactly delay plus timeMode.

My preference is therefore to keep the two flat fields for now. If another parameter eventually needs to reach the operator, promoting them to a small shared spec would make more sense. If you prefer establishing the wrapper now, though, I'm happy to change it.

}

private static EarlyFire extractEarlyFire(List<RelHint> hints, boolean isEventTime) {
RelHint earlyFireHint = null;
for (RelHint hint : hints) {
if (JoinStrategy.isEarlyFireHint(hint.hintName)) {
earlyFireHint = hint;
break;
}
}
if (earlyFireHint == null) {
return new EarlyFire(null, null);
}

Configuration conf = Configuration.fromMap(earlyFireHint.kvOptions);
Duration delay = conf.get(EarlyFireJoinHintOptions.DELAY);
TimeMode timeMode = conf.get(EarlyFireJoinHintOptions.TIME_MODE);
if (timeMode == null) {
timeMode = isEventTime ? TimeMode.ROWTIME : TimeMode.PROCTIME;
}

if (!isEventTime && timeMode == TimeMode.ROWTIME) {
throw new ValidationException(
"EARLY_FIRE hint requested row-time triggering on a processing-time interval"
+ " join. Row-time triggering requires a row-time interval join.");
}
if (isEventTime && timeMode == TimeMode.PROCTIME) {
// Processing-time triggering on an event-time interval join is not supported.
throw new TableException(
"EARLY_FIRE hint requested processing-time triggering on a row-time interval"
+ " join, which is not yet supported.");
}

return new EarlyFire(delay == null ? null : delay.toMillis(), timeMode);
}

private static final class EarlyFire {
@Nullable private final Long delay;
@Nullable private final TimeMode timeMode;

EarlyFire(@Nullable Long delay, @Nullable TimeMode timeMode) {
this.delay = delay;
this.timeMode = timeMode;
}
}

/** Configuration for {@link StreamPhysicalIntervalJoinRule}. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package org.apache.flink.table.planner.plan.nodes.physical.stream

import org.apache.flink.table.api.TableException
import org.apache.flink.table.api.config.EarlyFireJoinHintOptions.TimeMode
import org.apache.flink.table.planner.calcite.FlinkTypeFactory
import org.apache.flink.table.planner.plan.nodes.exec.{ExecNode, InputProperty}
import org.apache.flink.table.planner.plan.nodes.exec.spec.IntervalJoinSpec
Expand Down Expand Up @@ -45,7 +46,9 @@ class StreamPhysicalIntervalJoin(
val originalCondition: RexNode,
// remaining join condition contains all of join condition except window bounds
remainingCondition: RexNode,
windowBounds: WindowBounds)
windowBounds: WindowBounds,
earlyFireDelay: java.lang.Long,
earlyFireTimeMode: TimeMode)
extends CommonPhysicalJoin(cluster, traitSet, leftRel, rightRel, remainingCondition, joinType)
with StreamPhysicalRel {

Expand Down Expand Up @@ -76,7 +79,9 @@ class StreamPhysicalIntervalJoin(
joinType,
originalCondition,
conditionExpr,
windowBounds)
windowBounds,
earlyFireDelay,
earlyFireTimeMode)
}

override def explainTerms(pw: RelWriter): RelWriter = {
Expand All @@ -98,12 +103,16 @@ class StreamPhysicalIntervalJoin(
preferExpressionFormat(pw),
pw.getDetailLevel))
.item("select", getRowType.getFieldNames.mkString(", "))
.itemIf("earlyFireDelay", earlyFireDelay, earlyFireDelay != null)
.itemIf("earlyFireTimeMode", earlyFireTimeMode, earlyFireTimeMode != null)
}

override def translateToExecNode(): ExecNode[_] = {
new StreamExecIntervalJoin(
unwrapTableConfig(this),
new IntervalJoinSpec(joinSpec, windowBounds),
earlyFireDelay,
earlyFireTimeMode,
InputProperty.DEFAULT,
InputProperty.DEFAULT,
FlinkTypeFactory.toLogicalRowType(getRowType),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ void before() {
+ " 'connector' = 'values',\n"
+ " 'bounded' = 'false'\n"
+ ")");
util.tableEnv()
.executeSql(
"CREATE TABLE MySink (\n"
+ " a INT,\n"
+ " b VARCHAR\n"
+ ") WITH (\n"
+ " 'connector' = 'values'\n"
+ ")");
}

@Test
Expand Down Expand Up @@ -142,6 +150,58 @@ void testEarlyFireLowerCaseHintNamePreservesOptions() {
verify(sql);
}

@Test
void testEarlyFireOnRowTimeLeftOuterJoin() {
String sql =
"SELECT /*+ EARLY_FIRE('delay'='5s') */ t1.a, t2.b\n"
+ "FROM MyTable t1 LEFT OUTER JOIN MyTable2 t2 ON\n"
+ " t1.a = t2.a AND\n"
+ " t1.rowtime BETWEEN t2.rowtime - INTERVAL '10' SECOND AND t2.rowtime + INTERVAL '1' HOUR";
verify(sql);
}

@Test
void testEarlyFireRowTimeOnProcTimeJoin() {
String sql =
"SELECT /*+ EARLY_FIRE('delay'='5s', 'time-mode'='rowtime') */ t1.a, t2.b\n"
+ "FROM MyTable t1 LEFT OUTER JOIN MyTable2 t2 ON\n"
+ " t1.a = t2.a AND\n"
+ " t1.proctime BETWEEN t2.proctime - INTERVAL '1' HOUR AND t2.proctime + INTERVAL '1' HOUR";
assertThatThrownBy(() -> verify(sql))
.hasStackTraceContaining("requires a row-time interval join");
}

@Test
void testEarlyFireProcTimeOnRowTimeJoin() {
String sql =
"SELECT /*+ EARLY_FIRE('delay'='5s', 'time-mode'='proctime') */ t1.a, t2.b\n"
+ "FROM MyTable t1 LEFT OUTER JOIN MyTable2 t2 ON\n"
+ " t1.a = t2.a AND\n"
+ " t1.rowtime BETWEEN t2.rowtime - INTERVAL '10' SECOND AND t2.rowtime + INTERVAL '1' HOUR";
assertThatThrownBy(() -> verify(sql)).hasStackTraceContaining("not yet supported");
}

@Test
void testEarlyFireOnProcTimeLeftOuterJoin() {
String sql =
"SELECT /*+ EARLY_FIRE('delay'='5s') */ t1.a, t2.b\n"
+ "FROM MyTable t1 LEFT OUTER JOIN MyTable2 t2 ON\n"
+ " t1.a = t2.a AND\n"
+ " t1.proctime BETWEEN t2.proctime - INTERVAL '1' HOUR AND t2.proctime + INTERVAL '1' HOUR";
verify(sql);
}

@Test
void testEarlyFireJsonPlanRoundTrip() {
String insert =
"INSERT INTO MySink\n"
+ "SELECT /*+ EARLY_FIRE('delay'='5s') */ t1.a, t2.b\n"
+ "FROM MyTable t1 LEFT OUTER JOIN MyTable2 t2 ON\n"
+ " t1.a = t2.a AND\n"
+ " t1.rowtime BETWEEN t2.rowtime - INTERVAL '10' SECOND AND t2.rowtime + INTERVAL '1' HOUR";
util.verifyJsonPlan(insert);
}

private void verify(String sql) {
util.doVerifyPlan(
sql,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,75 @@ LogicalProject(a=[$0], b=[$6])
<Resource name="optimized exec plan">
<![CDATA[
Calc(select=[a, b])
+- IntervalJoin(joinType=[LeftOuterJoin], windowBounds=[isRowTime=true, leftLowerBound=-10000, leftUpperBound=3600000, leftTimeIndex=1, rightTimeIndex=2], where=[((a = a0) AND (rowtime >= (rowtime0 - 10000:INTERVAL SECOND)) AND (rowtime <= (rowtime0 + 3600000:INTERVAL HOUR)))], select=[a, rowtime, a0, b, rowtime0])
+- IntervalJoin(joinType=[LeftOuterJoin], windowBounds=[isRowTime=true, leftLowerBound=-10000, leftUpperBound=3600000, leftTimeIndex=1, rightTimeIndex=2], where=[((a = a0) AND (rowtime >= (rowtime0 - 10000:INTERVAL SECOND)) AND (rowtime <= (rowtime0 + 3600000:INTERVAL HOUR)))], select=[a, rowtime, a0, b, rowtime0], earlyFireDelay=[5000], earlyFireTimeMode=[ROWTIME])
:- Exchange(distribution=[hash[a]])
: +- WatermarkAssigner(rowtime=[rowtime], watermark=[rowtime])
: +- TableSourceScan(table=[[default_catalog, default_database, MyTable, project=[a, rowtime], metadata=[]]], fields=[a, rowtime])
+- Exchange(distribution=[hash[a]])
+- WatermarkAssigner(rowtime=[rowtime], watermark=[rowtime])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable2, project=[a, b, rowtime], metadata=[]]], fields=[a, b, rowtime])
]]>
</Resource>
</TestCase>
<TestCase name="testEarlyFireOnProcTimeLeftOuterJoin">
<Resource name="sql">
<![CDATA[SELECT /*+ EARLY_FIRE('delay'='5s') */ t1.a, t2.b
FROM MyTable t1 LEFT OUTER JOIN MyTable2 t2 ON
t1.a = t2.a AND
t1.proctime BETWEEN t2.proctime - INTERVAL '1' HOUR AND t2.proctime + INTERVAL '1' HOUR]]>
</Resource>
<Resource name="ast">
<![CDATA[
LogicalProject(a=[$0], b=[$6])
+- LogicalJoin(condition=[AND(=($0, $5), >=($3, -($8, 3600000:INTERVAL HOUR)), <=($3, +($8, 3600000:INTERVAL HOUR)))], joinType=[left], joinHints=[[[EARLY_FIRE inheritPath:[0] options:{delay=5s}]]])
:- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[$4])
: +- LogicalProject(a=[$0], b=[$1], c=[$2], proctime=[PROCTIME()], rowtime=[$3])
: +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
+- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[$4])
+- LogicalProject(a=[$0], b=[$1], c=[$2], proctime=[PROCTIME()], rowtime=[$3])
+- LogicalTableScan(table=[[default_catalog, default_database, MyTable2]])
]]>
</Resource>
<Resource name="optimized exec plan">
<![CDATA[
Calc(select=[a, b])
+- IntervalJoin(joinType=[LeftOuterJoin], windowBounds=[isRowTime=false, leftLowerBound=-3600000, leftUpperBound=3600000, leftTimeIndex=1, rightTimeIndex=2], where=[((a = a0) AND (proctime >= (proctime0 - 3600000:INTERVAL HOUR)) AND (proctime <= (proctime0 + 3600000:INTERVAL HOUR)))], select=[a, proctime, a0, b, proctime0], earlyFireDelay=[5000], earlyFireTimeMode=[PROCTIME])
:- Exchange(distribution=[hash[a]])
: +- Calc(select=[a, proctime])
: +- WatermarkAssigner(rowtime=[rowtime], watermark=[rowtime])
: +- Calc(select=[a, PROCTIME() AS proctime, rowtime])
: +- TableSourceScan(table=[[default_catalog, default_database, MyTable, project=[a, rowtime], metadata=[]]], fields=[a, rowtime])
+- Exchange(distribution=[hash[a]])
+- Calc(select=[a, b, proctime])
+- WatermarkAssigner(rowtime=[rowtime], watermark=[rowtime])
+- Calc(select=[a, b, PROCTIME() AS proctime, rowtime])
+- TableSourceScan(table=[[default_catalog, default_database, MyTable2, project=[a, b, rowtime], metadata=[]]], fields=[a, b, rowtime])
]]>
</Resource>
</TestCase>
<TestCase name="testEarlyFireOnRowTimeLeftOuterJoin">
<Resource name="sql">
<![CDATA[SELECT /*+ EARLY_FIRE('delay'='5s') */ t1.a, t2.b
FROM MyTable t1 LEFT OUTER JOIN MyTable2 t2 ON
t1.a = t2.a AND
t1.rowtime BETWEEN t2.rowtime - INTERVAL '10' SECOND AND t2.rowtime + INTERVAL '1' HOUR]]>
</Resource>
<Resource name="ast">
<![CDATA[
LogicalProject(a=[$0], b=[$6])
+- LogicalJoin(condition=[AND(=($0, $5), >=($4, -($9, 10000:INTERVAL SECOND)), <=($4, +($9, 3600000:INTERVAL HOUR)))], joinType=[left], joinHints=[[[EARLY_FIRE inheritPath:[0] options:{delay=5s}]]])
:- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[$4])
: +- LogicalProject(a=[$0], b=[$1], c=[$2], proctime=[PROCTIME()], rowtime=[$3])
: +- LogicalTableScan(table=[[default_catalog, default_database, MyTable]])
+- LogicalWatermarkAssigner(rowtime=[rowtime], watermark=[$4])
+- LogicalProject(a=[$0], b=[$1], c=[$2], proctime=[PROCTIME()], rowtime=[$3])
+- LogicalTableScan(table=[[default_catalog, default_database, MyTable2]])
]]>
</Resource>
<Resource name="optimized exec plan">
<![CDATA[
Calc(select=[a, b])
+- IntervalJoin(joinType=[LeftOuterJoin], windowBounds=[isRowTime=true, leftLowerBound=-10000, leftUpperBound=3600000, leftTimeIndex=1, rightTimeIndex=2], where=[((a = a0) AND (rowtime >= (rowtime0 - 10000:INTERVAL SECOND)) AND (rowtime <= (rowtime0 + 3600000:INTERVAL HOUR)))], select=[a, rowtime, a0, b, rowtime0], earlyFireDelay=[5000], earlyFireTimeMode=[ROWTIME])
:- Exchange(distribution=[hash[a]])
: +- WatermarkAssigner(rowtime=[rowtime], watermark=[rowtime])
: +- TableSourceScan(table=[[default_catalog, default_database, MyTable, project=[a, rowtime], metadata=[]]], fields=[a, rowtime])
Expand Down
Loading