This is an automated email from the ASF dual-hosted git repository.
zhanglistar pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new d8c868b6e3 [GLUTEN-12397][FLINK] Support WatermarkStatus propagation
(#12398)
d8c868b6e3 is described below
commit d8c868b6e362fd1fe465f79b448a7df99bda4092
Author: lgbo <[email protected]>
AuthorDate: Fri Jul 3 14:08:24 2026 +0800
[GLUTEN-12397][FLINK] Support WatermarkStatus propagation (#12398)
* Support watermark status propagation
* Update Flink CI velox4j reference
* Deduplicate watermark status conversion
* Document two-input watermark status handling
* Complete two-input operator checkpoint hooks
* Support Gluten two-input operator base
* Clarify Gluten operator base fork
* Add Flink watermark status tests
* Test records do not reactivate idle watermark status
* Document watermark status test scenarios
* Fix Gluten two-input offload factory
* Update Flink CI velox4j reference
* Fix Gluten offloaded operator factory setup
* Document Gluten one-input factory setup
* fix: Fix spotless import order in OffloadedJobGraphGenerator
* fix: Update velox4j CI reference to latest watermark status commit
* [CORE] Guard watermark handlers against null task and document dead
combined-watermark code
Add null-task guards to GlutenTwoInputOperator.processWatermark/1/2 and
processWatermarkStatus1/2, throwing NullPointerException with lifecycle
context, matching the defensive style already used in snapshotState and
notifyCheckpointComplete. Add @Override to processWatermark(Watermark)
for consistency with the other watermark overrides.
Document that GlutenAbstractStreamOperator.combinedWatermark and the
private indexed processWatermark/processWatermarkStatus overloads are
effectively dead code for current two-input subclasses (which bypass the
Java-side combined watermark), kept only to stay aligned with Flink
1.19.2 AbstractStreamOperator.
---
.github/workflows/flink.yml | 4 +-
.../plan/nodes/exec/stream/StreamExecJoin.java | 35 +-
.../nodes/exec/stream/StreamExecWindowJoin.java | 6 +-
.../gluten/client/OffloadedJobGraphGenerator.java | 11 +-
.../operators/GlutenAbstractStreamOperator.java | 717 +++++++++++++++++++++
.../operators/GlutenTwoInputOperatorFactory.java | 77 +++
.../runtime/operators/GlutenOneInputOperator.java | 10 +
.../runtime/operators/GlutenTwoInputOperator.java | 90 ++-
.../runtime/operators/GlutenWatermarkStatuses.java | 30 +
.../main/java/org/apache/gluten/util/Utils.java | 3 +
.../GlutenStreamTwoInputWatermarkStatusTest.java | 107 +++
.../operators/GlutenStreamWatermarkStatusTest.java | 120 ++++
12 files changed, 1189 insertions(+), 21 deletions(-)
diff --git a/.github/workflows/flink.yml b/.github/workflows/flink.yml
index a222cc7a25..b66bddf427 100644
--- a/.github/workflows/flink.yml
+++ b/.github/workflows/flink.yml
@@ -87,8 +87,8 @@ jobs:
export VELOX_DEPENDENCY_SOURCE=BUNDLED
export fmt_SOURCE=BUNDLED
export folly_SOURCE=BUNDLED
- git clone -b gluten-0530 https://github.com/bigo-sg/velox4j.git
- cd velox4j && git reset --hard
1b1ad0833220476be6a042b83a474cf58f8f076c
+ git clone -b feature/watermark-status-propagation
https://github.com/bigo-sg/velox4j.git
+ cd velox4j && git reset --hard 6e2046f
git apply $GITHUB_WORKSPACE/gluten-flink/patches/fix-velox4j.patch
$GITHUB_WORKSPACE/build/mvn clean install -DskipTests -Dgpg.skip
-Dspotless.skip=true
cd ..
diff --git
a/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecJoin.java
b/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecJoin.java
index cf66cbbee4..7a638e71fa 100644
---
a/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecJoin.java
+++
b/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecJoin.java
@@ -19,6 +19,7 @@ package org.apache.flink.table.planner.plan.nodes.exec.stream;
import org.apache.gluten.rexnode.RexConversionContext;
import org.apache.gluten.rexnode.RexNodeConverter;
import org.apache.gluten.rexnode.Utils;
+import org.apache.gluten.streaming.api.operators.GlutenTwoInputOperatorFactory;
import org.apache.gluten.table.runtime.operators.GlutenTwoInputOperator;
import org.apache.gluten.util.LogicalTypeConverter;
import org.apache.gluten.util.PlanNodeIdGenerator;
@@ -40,6 +41,7 @@ import io.github.zhztheplayer.velox4j.type.BooleanType;
import org.apache.flink.FlinkVersion;
import org.apache.flink.api.dag.Transformation;
import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.streaming.api.operators.StreamOperator;
import org.apache.flink.streaming.api.operators.TwoInputStreamOperator;
import org.apache.flink.streaming.api.transformations.TwoInputTransformation;
import org.apache.flink.table.data.RowData;
@@ -323,15 +325,30 @@ public class StreamExecJoin extends ExecNodeBase<RowData>
}
final RowType returnType = (RowType) getOutputType();
- final TwoInputTransformation<RowData, RowData, RowData> transform =
- ExecNodeUtil.createTwoInputTransformation(
- leftTransform,
- rightTransform,
- createTransformationMeta(JOIN_TRANSFORMATION, config),
- operator,
- InternalTypeInfo.of(returnType),
- leftTransform.getParallelism(),
- false);
+ final TwoInputTransformation<RowData, RowData, RowData> transform;
+ if (operator instanceof GlutenTwoInputOperator) {
+ transform =
+ ExecNodeUtil.createTwoInputTransformation(
+ leftTransform,
+ rightTransform,
+ createTransformationMeta(JOIN_TRANSFORMATION, config),
+ new GlutenTwoInputOperatorFactory<RowData, RowData, RowData>(
+ (StreamOperator<RowData>) operator),
+ InternalTypeInfo.of(returnType),
+ leftTransform.getParallelism(),
+ 0L,
+ false);
+ } else {
+ transform =
+ ExecNodeUtil.createTwoInputTransformation(
+ leftTransform,
+ rightTransform,
+ createTransformationMeta(JOIN_TRANSFORMATION, config),
+ operator,
+ InternalTypeInfo.of(returnType),
+ leftTransform.getParallelism(),
+ false);
+ }
// set KeyType and Selector for state
RowDataKeySelector leftSelect =
diff --git
a/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecWindowJoin.java
b/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecWindowJoin.java
index 400a86ccb0..5a947f1ef5 100644
---
a/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecWindowJoin.java
+++
b/gluten-flink/planner/src/main/java/org/apache/flink/table/planner/plan/nodes/exec/stream/StreamExecWindowJoin.java
@@ -17,6 +17,7 @@
package org.apache.flink.table.planner.plan.nodes.exec.stream;
import org.apache.gluten.rexnode.Utils;
+import org.apache.gluten.streaming.api.operators.GlutenTwoInputOperatorFactory;
import org.apache.gluten.table.runtime.operators.GlutenTwoInputOperator;
import org.apache.gluten.util.LogicalTypeConverter;
import org.apache.gluten.util.PlanNodeIdGenerator;
@@ -36,6 +37,7 @@ import io.github.zhztheplayer.velox4j.plan.TableScanNode;
import org.apache.flink.FlinkVersion;
import org.apache.flink.api.dag.Transformation;
import org.apache.flink.configuration.ReadableConfig;
+import org.apache.flink.streaming.api.operators.StreamOperator;
import org.apache.flink.streaming.api.operators.TwoInputStreamOperator;
import org.apache.flink.streaming.api.transformations.TwoInputTransformation;
import org.apache.flink.table.api.TableException;
@@ -245,9 +247,11 @@ public class StreamExecWindowJoin extends
ExecNodeBase<RowData>
leftTransform,
rightTransform,
createTransformationMeta(WINDOW_JOIN_TRANSFORMATION, config),
- operator,
+ new GlutenTwoInputOperatorFactory<RowData, RowData, RowData>(
+ (StreamOperator<RowData>) operator),
InternalTypeInfo.of(returnType),
leftTransform.getParallelism(),
+ 0L,
false);
// set KeyType and Selector for state
diff --git
a/gluten-flink/runtime/src/main/java/org/apache/gluten/client/OffloadedJobGraphGenerator.java
b/gluten-flink/runtime/src/main/java/org/apache/gluten/client/OffloadedJobGraphGenerator.java
index 27dc0b1933..a9fa29557d 100644
---
a/gluten-flink/runtime/src/main/java/org/apache/gluten/client/OffloadedJobGraphGenerator.java
+++
b/gluten-flink/runtime/src/main/java/org/apache/gluten/client/OffloadedJobGraphGenerator.java
@@ -16,8 +16,10 @@
*/
package org.apache.gluten.client;
+import org.apache.gluten.streaming.api.operators.GlutenOneInputOperatorFactory;
import org.apache.gluten.streaming.api.operators.GlutenOperator;
import org.apache.gluten.streaming.api.operators.GlutenStreamSource;
+import org.apache.gluten.streaming.api.operators.GlutenTwoInputOperatorFactory;
import org.apache.gluten.table.runtime.keyselector.GlutenKeySelector;
import org.apache.gluten.table.runtime.operators.GlutenOneInputOperator;
import org.apache.gluten.table.runtime.operators.GlutenSourceFunction;
@@ -241,7 +243,9 @@ public class OffloadedJobGraphGenerator {
Class<?> outClass = supportsVectorOutput ? StatefulRecord.class :
RowData.class;
GlutenOneInputOperator<?, ?> newOneInputOp =
sourceOperator.cloneWithInputOutputClasses(planNode, inClass,
outClass);
- offloadedOpConfig.setStreamOperator(newOneInputOp);
+ // setStreamOperator would wrap this in Flink's SimpleOperatorFactory and
skip Gluten-specific
+ // mailbox binding performed by GlutenOneInputOperatorFactory.
+ offloadedOpConfig.setStreamOperatorFactory(new
GlutenOneInputOperatorFactory<>(newOneInputOp));
if (supportsVectorOutput) {
setOffloadedOutputSerializer(offloadedOpConfig, sourceOperator);
}
@@ -279,7 +283,10 @@ public class OffloadedJobGraphGenerator {
sourceOperator.getOutputTypes(),
inClass,
outClass);
- offloadedOpConfig.setStreamOperator(newTwoInputOp);
+ // setStreamOperator would wrap this in Flink's SimpleOperatorFactory,
which only initializes
+ // ProcessingTimeService for Flink AbstractStreamOperator.
GlutenTwoInputOperator uses
+ // GlutenAbstractStreamOperator so it needs the Gluten-specific factory.
+ offloadedOpConfig.setStreamOperatorFactory(new
GlutenTwoInputOperatorFactory<>(newTwoInputOp));
offloadedOpConfig.setStatePartitioner(0, new GlutenKeySelector());
offloadedOpConfig.setStatePartitioner(1, new GlutenKeySelector());
if (supportsVectorOutput) {
diff --git
a/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenAbstractStreamOperator.java
b/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenAbstractStreamOperator.java
new file mode 100644
index 0000000000..7c8202d32a
--- /dev/null
+++
b/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenAbstractStreamOperator.java
@@ -0,0 +1,717 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.streaming.api.operators;
+
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.annotation.Internal;
+import org.apache.flink.annotation.PublicEvolving;
+import org.apache.flink.annotation.VisibleForTesting;
+import org.apache.flink.api.common.ExecutionConfig;
+import org.apache.flink.api.common.eventtime.IndexedCombinedWatermarkStatus;
+import org.apache.flink.api.common.state.KeyedStateStore;
+import org.apache.flink.api.common.state.State;
+import org.apache.flink.api.common.state.StateDescriptor;
+import org.apache.flink.api.common.typeutils.TypeSerializer;
+import org.apache.flink.api.java.functions.KeySelector;
+import org.apache.flink.configuration.Configuration;
+import org.apache.flink.configuration.MetricOptions;
+import org.apache.flink.core.fs.CloseableRegistry;
+import org.apache.flink.core.memory.ManagedMemoryUseCase;
+import org.apache.flink.metrics.MetricGroup;
+import org.apache.flink.metrics.groups.OperatorMetricGroup;
+import org.apache.flink.runtime.checkpoint.CheckpointOptions;
+import org.apache.flink.runtime.execution.Environment;
+import org.apache.flink.runtime.jobgraph.OperatorID;
+import org.apache.flink.runtime.metrics.groups.InternalOperatorMetricGroup;
+import org.apache.flink.runtime.metrics.groups.UnregisteredMetricGroups;
+import org.apache.flink.runtime.state.CheckpointStreamFactory;
+import org.apache.flink.runtime.state.KeyedStateBackend;
+import org.apache.flink.runtime.state.OperatorStateBackend;
+import org.apache.flink.runtime.state.StateInitializationContext;
+import org.apache.flink.runtime.state.StateSnapshotContext;
+import org.apache.flink.runtime.state.VoidNamespace;
+import org.apache.flink.runtime.state.VoidNamespaceSerializer;
+import org.apache.flink.streaming.api.graph.StreamConfig;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperatorV2;
+import org.apache.flink.streaming.api.operators.AbstractUdfStreamOperator;
+import org.apache.flink.streaming.api.operators.ChainingStrategy;
+import org.apache.flink.streaming.api.operators.InternalTimeServiceManager;
+import org.apache.flink.streaming.api.operators.InternalTimerService;
+import org.apache.flink.streaming.api.operators.KeyContextHandler;
+import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
+import org.apache.flink.streaming.api.operators.OperatorSnapshotFutures;
+import org.apache.flink.streaming.api.operators.Output;
+import org.apache.flink.streaming.api.operators.SetupableStreamOperator;
+import org.apache.flink.streaming.api.operators.StreamOperator;
+import org.apache.flink.streaming.api.operators.StreamOperatorStateContext;
+import org.apache.flink.streaming.api.operators.StreamOperatorStateHandler;
+import
org.apache.flink.streaming.api.operators.StreamOperatorStateHandler.CheckpointedStreamOperator;
+import org.apache.flink.streaming.api.operators.StreamTaskStateInitializer;
+import org.apache.flink.streaming.api.operators.StreamingRuntimeContext;
+import org.apache.flink.streaming.api.operators.Triggerable;
+import org.apache.flink.streaming.api.operators.TwoInputStreamOperator;
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.runtime.streamrecord.LatencyMarker;
+import org.apache.flink.streaming.runtime.streamrecord.RecordAttributes;
+import org.apache.flink.streaming.runtime.streamrecord.RecordAttributesBuilder;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.tasks.ProcessingTimeService;
+import org.apache.flink.streaming.runtime.tasks.StreamTask;
+import org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus;
+import org.apache.flink.streaming.util.LatencyStats;
+import org.apache.flink.util.Preconditions;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Optional;
+
+import static org.apache.flink.util.Preconditions.checkState;
+
+/**
+ * Base class for all stream operators. Operators that contain a user function
should extend the
+ * class {@link AbstractUdfStreamOperator} instead (which is a specialized
subclass of this class).
+ *
+ * <p>For concrete implementations, one of the following two interfaces must
also be implemented, to
+ * mark the operator as unary or binary: {@link OneInputStreamOperator} or
{@link
+ * TwoInputStreamOperator}.
+ *
+ * <p>Methods of {@code StreamOperator} are guaranteed not to be called
concurrently. Also, if using
+ * the timer service, timer callbacks are also guaranteed not to be called
concurrently with methods
+ * on {@code StreamOperator}.
+ *
+ * <p>Note, this class is going to be removed and replaced in the future by
{@link
+ * AbstractStreamOperatorV2}. However as {@link AbstractStreamOperatorV2} is
currently experimental,
+ * {@link AbstractStreamOperator} has not been deprecated just yet.
+ *
+ * <p>This class is a Gluten-local copy of Flink 1.19.2 {@link
AbstractStreamOperator}. Gluten needs
+ * this fork because Flink's watermark status entrypoints are final in both
operator base classes:
+ * {@code AbstractStreamOperator.processWatermarkStatus1/2(...)} and {@code
+ * AbstractStreamOperatorV2.processWatermarkStatus(...)}. Those methods route
two-input {@code
+ * WatermarkStatus} events through Flink's Java-side combined watermark
status. Gluten must instead
+ * forward each input's {@code IDLE}/{@code ACTIVE} status to native
execution, where the Gluten
+ * operator chain keeps the combined watermark/status as the source of truth.
Reusing either Flink
+ * base class would prevent {@code GlutenTwoInputOperator} from overriding the
status handling,
+ * while not using a base class at all would lose the runtime, state,
key-context, timer, latency,
+ * and checkpoint behavior implemented by {@code AbstractStreamOperator}. Keep
this file aligned
+ * with Flink 1.19.2 except for making {@code processWatermarkStatus1/2(...)}
overridable.
+ *
+ * @param <OUT> The output type of the operator.
+ */
+@PublicEvolving
+public abstract class GlutenAbstractStreamOperator<OUT>
+ implements StreamOperator<OUT>,
+ SetupableStreamOperator<OUT>,
+ CheckpointedStreamOperator,
+ KeyContextHandler,
+ Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /** The logger used by the operator class and its subclasses. */
+ protected static final Logger LOG =
LoggerFactory.getLogger(GlutenAbstractStreamOperator.class);
+
+ // ----------- configuration properties -------------
+
+ // A sane default for most operators
+ protected ChainingStrategy chainingStrategy = ChainingStrategy.HEAD;
+
+ // ---------------- runtime fields ------------------
+
+ /** The task that contains this operator (and other operators in the same
chain). */
+ private transient StreamTask<?, ?> container;
+
+ protected transient StreamConfig config;
+
+ protected transient Output<StreamRecord<OUT>> output;
+
+ // Used by the default processWatermark1/2 and processWatermarkStatus1/2
implementations below.
+ // Concrete two-input subclasses (e.g. GlutenTwoInputOperator) override
those entrypoints to
+ // forward each input's watermark/status directly to native, bypassing this
Java-side combined
+ // watermark. The field and the private indexed overloads are kept to stay
aligned with Flink
+ // 1.19.2's AbstractStreamOperator and to serve any future subclass that
wants the default
+ // behavior; they are effectively dead code for the current Gluten operators.
+ private transient IndexedCombinedWatermarkStatus combinedWatermark;
+
+ /** The runtime context for UDFs. */
+ private transient StreamingRuntimeContext runtimeContext;
+
+ // ---------------- key/value state ------------------
+
+ /**
+ * {@code KeySelector} for extracting a key from an element being processed.
This is used to scope
+ * keyed state to a key. This is null if the operator is not a keyed
operator.
+ *
+ * <p>This is for elements from the first input.
+ */
+ private transient KeySelector<?, ?> stateKeySelector1;
+
+ /**
+ * {@code KeySelector} for extracting a key from an element being processed.
This is used to scope
+ * keyed state to a key. This is null if the operator is not a keyed
operator.
+ *
+ * <p>This is for elements from the second input.
+ */
+ private transient KeySelector<?, ?> stateKeySelector2;
+
+ private transient StreamOperatorStateHandler stateHandler;
+
+ private transient InternalTimeServiceManager<?> timeServiceManager;
+
+ // --------------- Metrics ---------------------------
+
+ /** Metric group for the operator. */
+ protected transient InternalOperatorMetricGroup metrics;
+
+ protected transient LatencyStats latencyStats;
+
+ // ---------------- time handler ------------------
+
+ protected transient ProcessingTimeService processingTimeService;
+
+ protected transient RecordAttributes lastRecordAttributes1;
+ protected transient RecordAttributes lastRecordAttributes2;
+
+ // ------------------------------------------------------------------------
+ // Life Cycle
+ // ------------------------------------------------------------------------
+
+ @Override
+ public void setup(
+ StreamTask<?, ?> containingTask, StreamConfig config,
Output<StreamRecord<OUT>> output) {
+ final Environment environment = containingTask.getEnvironment();
+ this.container = containingTask;
+ this.config = config;
+ this.output = output;
+ this.metrics =
+ environment
+ .getMetricGroup()
+ .getOrAddOperator(config.getOperatorID(),
config.getOperatorName());
+ this.combinedWatermark = IndexedCombinedWatermarkStatus.forInputsCount(2);
+
+ try {
+ Configuration taskManagerConfig =
environment.getTaskManagerInfo().getConfiguration();
+ int historySize =
taskManagerConfig.get(MetricOptions.LATENCY_HISTORY_SIZE);
+ if (historySize <= 0) {
+ LOG.warn(
+ "{} has been set to a value equal or below 0: {}. Using default.",
+ MetricOptions.LATENCY_HISTORY_SIZE,
+ historySize);
+ historySize = MetricOptions.LATENCY_HISTORY_SIZE.defaultValue();
+ }
+
+ final String configuredGranularity =
+ taskManagerConfig.get(MetricOptions.LATENCY_SOURCE_GRANULARITY);
+ LatencyStats.Granularity granularity;
+ try {
+ granularity =
+
LatencyStats.Granularity.valueOf(configuredGranularity.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException iae) {
+ granularity = LatencyStats.Granularity.OPERATOR;
+ LOG.warn(
+ "Configured value {} option for {} is invalid. Defaulting to {}.",
+ configuredGranularity,
+ MetricOptions.LATENCY_SOURCE_GRANULARITY.key(),
+ granularity);
+ }
+ MetricGroup taskMetricGroup = this.metrics.getTaskMetricGroup();
+ this.latencyStats =
+ new LatencyStats(
+ taskMetricGroup.addGroup("latency"),
+ historySize,
+ container.getIndexInSubtaskGroup(),
+ getOperatorID(),
+ granularity);
+ } catch (Exception e) {
+ LOG.warn("An error occurred while instantiating latency metrics.", e);
+ this.latencyStats =
+ new LatencyStats(
+
UnregisteredMetricGroups.createUnregisteredTaskMetricGroup().addGroup("latency"),
+ 1,
+ 0,
+ new OperatorID(),
+ LatencyStats.Granularity.SINGLE);
+ }
+
+ this.runtimeContext =
+ new StreamingRuntimeContext(
+ environment,
+ environment.getAccumulatorRegistry().getUserMap(),
+ getMetricGroup(),
+ getOperatorID(),
+ getProcessingTimeService(),
+ null,
+ environment.getExternalResourceInfoProvider());
+
+ stateKeySelector1 = config.getStatePartitioner(0,
getUserCodeClassloader());
+ stateKeySelector2 = config.getStatePartitioner(1,
getUserCodeClassloader());
+
+ lastRecordAttributes1 = RecordAttributes.EMPTY_RECORD_ATTRIBUTES;
+ lastRecordAttributes2 = RecordAttributes.EMPTY_RECORD_ATTRIBUTES;
+ }
+
+ /**
+ * @deprecated The {@link ProcessingTimeService} instance should be passed
by the operator
+ * constructor and this method will be removed along with {@link
SetupableStreamOperator}.
+ */
+ @Deprecated
+ public void setProcessingTimeService(ProcessingTimeService
processingTimeService) {
+ this.processingTimeService =
Preconditions.checkNotNull(processingTimeService);
+ }
+
+ @Override
+ public OperatorMetricGroup getMetricGroup() {
+ return metrics;
+ }
+
+ @Override
+ public final void initializeState(StreamTaskStateInitializer
streamTaskStateManager)
+ throws Exception {
+
+ final TypeSerializer<?> keySerializer =
config.getStateKeySerializer(getUserCodeClassloader());
+
+ final StreamTask<?, ?> containingTask =
Preconditions.checkNotNull(getContainingTask());
+ final CloseableRegistry streamTaskCloseableRegistry =
+ Preconditions.checkNotNull(containingTask.getCancelables());
+
+ final StreamOperatorStateContext context =
+ streamTaskStateManager.streamOperatorStateContext(
+ getOperatorID(),
+ getClass().getSimpleName(),
+ getProcessingTimeService(),
+ this,
+ keySerializer,
+ streamTaskCloseableRegistry,
+ metrics,
+ config.getManagedMemoryFractionOperatorUseCaseOfSlot(
+ ManagedMemoryUseCase.STATE_BACKEND,
+ runtimeContext.getJobConfiguration(),
+ runtimeContext.getTaskManagerRuntimeInfo().getConfiguration(),
+ runtimeContext.getUserCodeClassLoader()),
+ isUsingCustomRawKeyedState());
+
+ stateHandler =
+ new StreamOperatorStateHandler(context, getExecutionConfig(),
streamTaskCloseableRegistry);
+ timeServiceManager = context.internalTimerServiceManager();
+ stateHandler.initializeOperatorState(this);
+
runtimeContext.setKeyedStateStore(stateHandler.getKeyedStateStore().orElse(null));
+ }
+
+ /**
+ * Indicates whether or not implementations of this class is writing to the
raw keyed state
+ * streams on snapshots, using {@link #snapshotState(StateSnapshotContext)}.
If yes, subclasses
+ * should override this method to return {@code true}.
+ *
+ * <p>Subclasses need to explicitly indicate the use of raw keyed state
because, internally, the
+ * {@link GlutenAbstractStreamOperator} may attempt to read from it as well
to restore heap-based
+ * timers and ultimately fail with read errors. By setting this flag to
{@code true}, this allows
+ * the {@link GlutenAbstractStreamOperator} to know that the data written in
the raw keyed states
+ * were not written by the timer services, and skips the timer restore
attempt.
+ *
+ * <p>Please refer to FLINK-19741 for further details.
+ *
+ * <p>TODO: this method can be removed once all timers are moved to be
managed by state backends.
+ *
+ * @return flag indicating whether or not this operator is writing to raw
keyed state via {@link
+ * #snapshotState(StateSnapshotContext)}.
+ */
+ @Internal
+ protected boolean isUsingCustomRawKeyedState() {
+ return false;
+ }
+
+ /**
+ * This method is called immediately before any elements are processed, it
should contain the
+ * operator's initialization logic, e.g. state initialization.
+ *
+ * <p>The default implementation does nothing.
+ *
+ * @throws Exception An exception in this method causes the operator to fail.
+ */
+ @Override
+ public void open() throws Exception {}
+
+ @Override
+ public void finish() throws Exception {}
+
+ @Override
+ public void close() throws Exception {
+ if (stateHandler != null) {
+ stateHandler.dispose();
+ }
+ }
+
+ @Override
+ public void prepareSnapshotPreBarrier(long checkpointId) throws Exception {
+ // the default implementation does nothing and accepts the checkpoint
+ // this is purely for subclasses to override
+ }
+
+ @Override
+ public final OperatorSnapshotFutures snapshotState(
+ long checkpointId,
+ long timestamp,
+ CheckpointOptions checkpointOptions,
+ CheckpointStreamFactory factory)
+ throws Exception {
+ return stateHandler.snapshotState(
+ this,
+ Optional.ofNullable(timeServiceManager),
+ getOperatorName(),
+ checkpointId,
+ timestamp,
+ checkpointOptions,
+ factory,
+ isUsingCustomRawKeyedState());
+ }
+
+ /**
+ * Stream operators with state, which want to participate in a snapshot need
to override this hook
+ * method.
+ *
+ * @param context context that provides information and means required for
taking a snapshot
+ */
+ @Override
+ public void snapshotState(StateSnapshotContext context) throws Exception {}
+
+ /**
+ * Stream operators with state which can be restored need to override this
hook method.
+ *
+ * @param context context that allows to register different states.
+ */
+ @Override
+ public void initializeState(StateInitializationContext context) throws
Exception {}
+
+ @Override
+ public void notifyCheckpointComplete(long checkpointId) throws Exception {
+ stateHandler.notifyCheckpointComplete(checkpointId);
+ }
+
+ @Override
+ public void notifyCheckpointAborted(long checkpointId) throws Exception {
+ stateHandler.notifyCheckpointAborted(checkpointId);
+ }
+
+ // ------------------------------------------------------------------------
+ // Properties and Services
+ // ------------------------------------------------------------------------
+
+ /**
+ * Gets the execution config defined on the execution environment of the job
to which this
+ * operator belongs.
+ *
+ * @return The job's execution config.
+ */
+ public ExecutionConfig getExecutionConfig() {
+ return container.getExecutionConfig();
+ }
+
+ public StreamConfig getOperatorConfig() {
+ return config;
+ }
+
+ public StreamTask<?, ?> getContainingTask() {
+ return container;
+ }
+
+ public ClassLoader getUserCodeClassloader() {
+ return container.getUserCodeClassLoader();
+ }
+
+ /**
+ * Return the operator name. If the runtime context has been set, then the
task name with subtask
+ * index is returned. Otherwise, the simple class name is returned.
+ *
+ * @return If runtime context is set, then return task name with subtask
index. Otherwise return
+ * simple class name.
+ */
+ protected String getOperatorName() {
+ if (runtimeContext != null) {
+ return runtimeContext.getTaskInfo().getTaskNameWithSubtasks();
+ } else {
+ return getClass().getSimpleName();
+ }
+ }
+
+ /**
+ * Returns a context that allows the operator to query information about the
execution and also to
+ * interact with systems such as broadcast variables and managed state. This
also allows to
+ * register timers.
+ */
+ @VisibleForTesting
+ public StreamingRuntimeContext getRuntimeContext() {
+ return runtimeContext;
+ }
+
+ public <K> KeyedStateBackend<K> getKeyedStateBackend() {
+ return stateHandler.getKeyedStateBackend();
+ }
+
+ @VisibleForTesting
+ public OperatorStateBackend getOperatorStateBackend() {
+ return stateHandler.getOperatorStateBackend();
+ }
+
+ /**
+ * Returns the {@link ProcessingTimeService} responsible for getting the
current processing time
+ * and registering timers.
+ */
+ @VisibleForTesting
+ public ProcessingTimeService getProcessingTimeService() {
+ return processingTimeService;
+ }
+
+ /**
+ * Creates a partitioned state handle, using the state backend configured
for this task.
+ *
+ * @throws IllegalStateException Thrown, if the key/value state was already
initialized.
+ * @throws Exception Thrown, if the state backend cannot create the
key/value state.
+ */
+ protected <S extends State> S getPartitionedState(StateDescriptor<S, ?>
stateDescriptor)
+ throws Exception {
+ return getPartitionedState(
+ VoidNamespace.INSTANCE, VoidNamespaceSerializer.INSTANCE,
stateDescriptor);
+ }
+
+ protected <N, S extends State, T> S getOrCreateKeyedState(
+ TypeSerializer<N> namespaceSerializer, StateDescriptor<S, T>
stateDescriptor)
+ throws Exception {
+ return stateHandler.getOrCreateKeyedState(namespaceSerializer,
stateDescriptor);
+ }
+
+ /**
+ * Creates a partitioned state handle, using the state backend configured
for this task.
+ *
+ * @throws IllegalStateException Thrown, if the key/value state was already
initialized.
+ * @throws Exception Thrown, if the state backend cannot create the
key/value state.
+ */
+ protected <S extends State, N> S getPartitionedState(
+ N namespace, TypeSerializer<N> namespaceSerializer, StateDescriptor<S,
?> stateDescriptor)
+ throws Exception {
+ KeyedStateBackend<?> keyedStateBackend =
stateHandler.getKeyedStateBackend();
+ if (keyedStateBackend != null) {
+ return keyedStateBackend.getPartitionedState(namespace,
namespaceSerializer, stateDescriptor);
+ }
+ throw new RuntimeException(
+ "Cannot create partitioned state. The keyed state "
+ + "backend has not been set. This indicates that the operator is
not "
+ + "partitioned/keyed.");
+ }
+
+ @Override
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void setKeyContextElement1(StreamRecord record) throws Exception {
+ setKeyContextElement(record, stateKeySelector1);
+ }
+
+ @Override
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void setKeyContextElement2(StreamRecord record) throws Exception {
+ setKeyContextElement(record, stateKeySelector2);
+ }
+
+ @Internal
+ @Override
+ public boolean hasKeyContext1() {
+ return stateKeySelector1 != null;
+ }
+
+ @Internal
+ @Override
+ public boolean hasKeyContext2() {
+ return stateKeySelector2 != null;
+ }
+
+ private <T> void setKeyContextElement(StreamRecord<T> record, KeySelector<T,
?> selector)
+ throws Exception {
+ if (selector != null) {
+ Object key = selector.getKey(record.getValue());
+ setCurrentKey(key);
+ }
+ }
+
+ public void setCurrentKey(Object key) {
+ stateHandler.setCurrentKey(key);
+ }
+
+ public Object getCurrentKey() {
+ return stateHandler.getCurrentKey();
+ }
+
+ public KeyedStateStore getKeyedStateStore() {
+ if (stateHandler == null) {
+ return null;
+ }
+ return stateHandler.getKeyedStateStore().orElse(null);
+ }
+
+ // ------------------------------------------------------------------------
+ // Context and chaining properties
+ // ------------------------------------------------------------------------
+
+ @Override
+ public final void setChainingStrategy(ChainingStrategy strategy) {
+ this.chainingStrategy = strategy;
+ }
+
+ @Override
+ public final ChainingStrategy getChainingStrategy() {
+ return chainingStrategy;
+ }
+
+ // ------------------------------------------------------------------------
+ // Metrics
+ // ------------------------------------------------------------------------
+
+ // ------- One input stream
+ public void processLatencyMarker(LatencyMarker latencyMarker) throws
Exception {
+ reportOrForwardLatencyMarker(latencyMarker);
+ }
+
+ // ------- Two input stream
+ public void processLatencyMarker1(LatencyMarker latencyMarker) throws
Exception {
+ reportOrForwardLatencyMarker(latencyMarker);
+ }
+
+ public void processLatencyMarker2(LatencyMarker latencyMarker) throws
Exception {
+ reportOrForwardLatencyMarker(latencyMarker);
+ }
+
+ protected void reportOrForwardLatencyMarker(LatencyMarker marker) {
+ // all operators are tracking latencies
+ this.latencyStats.reportLatency(marker);
+
+ // everything except sinks forwards latency markers
+ this.output.emitLatencyMarker(marker);
+ }
+
+ // ------------------------------------------------------------------------
+ // Watermark handling
+ // ------------------------------------------------------------------------
+
+ /**
+ * Returns a {@link InternalTimerService} that can be used to query current
processing time and
+ * event time and to set timers. An operator can have several timer
services, where each has its
+ * own namespace serializer. Timer services are differentiated by the string
key that is given
+ * when requesting them, if you call this method with the same key multiple
times you will get the
+ * same timer service instance in subsequent requests.
+ *
+ * <p>Timers are always scoped to a key, the currently active key of a keyed
stream operation.
+ * When a timer fires, this key will also be set as the currently active key.
+ *
+ * <p>Each timer has attached metadata, the namespace. Different timer
services can have a
+ * different namespace type. If you don't need namespace differentiation you
can use {@link
+ * VoidNamespaceSerializer} as the namespace serializer.
+ *
+ * @param name The name of the requested timer service. If no service exists
under the given name
+ * a new one will be created and returned.
+ * @param namespaceSerializer {@code TypeSerializer} for the timer namespace.
+ * @param triggerable The {@link Triggerable} that should be invoked when
timers fire
+ * @param <N> The type of the timer namespace.
+ */
+ public <K, N> InternalTimerService<N> getInternalTimerService(
+ String name, TypeSerializer<N> namespaceSerializer, Triggerable<K, N>
triggerable) {
+ if (timeServiceManager == null) {
+ throw new RuntimeException("The timer service has not been
initialized.");
+ }
+ @SuppressWarnings("unchecked")
+ InternalTimeServiceManager<K> keyedTimeServiceHandler =
+ (InternalTimeServiceManager<K>) timeServiceManager;
+ KeyedStateBackend<K> keyedStateBackend = getKeyedStateBackend();
+ checkState(keyedStateBackend != null, "Timers can only be used on keyed
operators.");
+ return keyedTimeServiceHandler.getInternalTimerService(
+ name, keyedStateBackend.getKeySerializer(), namespaceSerializer,
triggerable);
+ }
+
+ public void processWatermark(Watermark mark) throws Exception {
+ if (timeServiceManager != null) {
+ timeServiceManager.advanceWatermark(mark);
+ }
+ output.emitWatermark(mark);
+ }
+
+ private void processWatermark(Watermark mark, int index) throws Exception {
+ if (combinedWatermark.updateWatermark(index, mark.getTimestamp())) {
+ processWatermark(new
Watermark(combinedWatermark.getCombinedWatermark()));
+ }
+ }
+
+ public void processWatermark1(Watermark mark) throws Exception {
+ processWatermark(mark, 0);
+ }
+
+ public void processWatermark2(Watermark mark) throws Exception {
+ processWatermark(mark, 1);
+ }
+
+ public void processWatermarkStatus(WatermarkStatus watermarkStatus) throws
Exception {
+ output.emitWatermarkStatus(watermarkStatus);
+ }
+
+ private void processWatermarkStatus(WatermarkStatus watermarkStatus, int
index) throws Exception {
+ boolean wasIdle = combinedWatermark.isIdle();
+ if (combinedWatermark.updateStatus(index, watermarkStatus.isIdle())) {
+ processWatermark(new
Watermark(combinedWatermark.getCombinedWatermark()));
+ }
+ if (wasIdle != combinedWatermark.isIdle()) {
+ output.emitWatermarkStatus(watermarkStatus);
+ }
+ }
+
+ public void processWatermarkStatus1(WatermarkStatus watermarkStatus) throws
Exception {
+ processWatermarkStatus(watermarkStatus, 0);
+ }
+
+ public void processWatermarkStatus2(WatermarkStatus watermarkStatus) throws
Exception {
+ processWatermarkStatus(watermarkStatus, 1);
+ }
+
+ @Override
+ public OperatorID getOperatorID() {
+ return config.getOperatorID();
+ }
+
+ protected Optional<InternalTimeServiceManager<?>> getTimeServiceManager() {
+ return Optional.ofNullable(timeServiceManager);
+ }
+
+ @Experimental
+ public void processRecordAttributes(RecordAttributes recordAttributes)
throws Exception {
+ output.emitRecordAttributes(
+ new
RecordAttributesBuilder(Collections.singletonList(recordAttributes)).build());
+ }
+
+ @Experimental
+ public void processRecordAttributes1(RecordAttributes recordAttributes) {
+ lastRecordAttributes1 = recordAttributes;
+ output.emitRecordAttributes(
+ new RecordAttributesBuilder(Arrays.asList(lastRecordAttributes1,
lastRecordAttributes2))
+ .build());
+ }
+
+ @Experimental
+ public void processRecordAttributes2(RecordAttributes recordAttributes) {
+ lastRecordAttributes2 = recordAttributes;
+ output.emitRecordAttributes(
+ new RecordAttributesBuilder(Arrays.asList(lastRecordAttributes1,
lastRecordAttributes2))
+ .build());
+ }
+}
diff --git
a/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenTwoInputOperatorFactory.java
b/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenTwoInputOperatorFactory.java
new file mode 100644
index 0000000000..95aa7a1087
--- /dev/null
+++
b/gluten-flink/runtime/src/main/java/org/apache/gluten/streaming/api/operators/GlutenTwoInputOperatorFactory.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.streaming.api.operators;
+
+import org.apache.gluten.table.runtime.operators.GlutenMailboxOperatorHelper;
+
+import io.github.zhztheplayer.velox4j.serde.Serde;
+
+import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
+import org.apache.flink.streaming.api.operators.AbstractStreamOperatorFactory;
+import org.apache.flink.streaming.api.operators.SetupableStreamOperator;
+import org.apache.flink.streaming.api.operators.StreamOperator;
+import org.apache.flink.streaming.api.operators.StreamOperatorParameters;
+import org.apache.flink.streaming.api.operators.TwoInputStreamOperatorFactory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Two input operator factory for gluten. */
+public class GlutenTwoInputOperatorFactory<IN1, IN2, OUT> extends
AbstractStreamOperatorFactory<OUT>
+ implements TwoInputStreamOperatorFactory<IN1, IN2, OUT> {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(GlutenTwoInputOperatorFactory.class);
+
+ private final StreamOperator<OUT> operator;
+
+ public GlutenTwoInputOperatorFactory(StreamOperator<OUT> operator) {
+ this.operator = operator;
+ if (!(operator instanceof GlutenOperator)) {
+ throw new RuntimeException("Operator is not gluten operator");
+ }
+ }
+
+ public GlutenOperator getOperator() {
+ return (GlutenOperator) operator;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public <T extends StreamOperator<OUT>> T createStreamOperator(
+ StreamOperatorParameters<OUT> parameters) {
+ LOG.debug("Build gluten operator {}",
Serde.toJson(getOperator().getPlanNode()));
+ if (operator instanceof GlutenAbstractStreamOperator) {
+ ((GlutenAbstractStreamOperator<OUT>) operator)
+ .setProcessingTimeService(parameters.getProcessingTimeService());
+ }
+ if (operator instanceof AbstractStreamOperator) {
+ ((AbstractStreamOperator)
operator).setProcessingTimeService(processingTimeService);
+ }
+ if (operator instanceof SetupableStreamOperator) {
+ ((SetupableStreamOperator<OUT>) operator)
+ .setup(
+ parameters.getContainingTask(), parameters.getStreamConfig(),
parameters.getOutput());
+ }
+
GlutenMailboxOperatorHelper.bindAtTaskStartup(getOperator().mailboxHolder(),
parameters);
+ return (T) operator;
+ }
+
+ @Override
+ public Class<? extends StreamOperator> getStreamOperatorClass(ClassLoader
classLoader) {
+ return operator.getClass();
+ }
+}
diff --git
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
index ef9d0d845d..5729a60b41 100644
---
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
+++
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenOneInputOperator.java
@@ -43,6 +43,7 @@ import org.apache.flink.runtime.state.StateSnapshotContext;
import org.apache.flink.streaming.api.operators.OneInputStreamOperator;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus;
import org.apache.flink.table.runtime.operators.TableStreamOperator;
import java.util.List;
@@ -223,6 +224,9 @@ public class GlutenOneInputOperator<IN, OUT> extends
TableStreamOperator<OUT>
if (statefulElement.isWatermark()) {
StatefulWatermark watermark = statefulElement.asWatermark();
output.emitWatermark(new Watermark(watermark.getTimestamp()));
+ } else if (statefulElement.isWatermarkStatus()) {
+ output.emitWatermarkStatus(
+
GlutenWatermarkStatuses.toFlinkWatermarkStatus(statefulElement));
} else {
outputBridge.collect(
output, statefulElement.asRecord(),
sessionResource.getAllocator(), outputType);
@@ -249,6 +253,12 @@ public class GlutenOneInputOperator<IN, OUT> extends
TableStreamOperator<OUT>
processElementInternal();
}
+ @Override
+ public void processWatermarkStatus(WatermarkStatus status) throws Exception {
+ task.notifyWatermarkStatus(status.isIdle());
+ processElementInternal();
+ }
+
@Override
public void processWatermark1(Watermark mark) throws Exception {
throw new UnsupportedOperationException("Not implemented for
GlutenOneInputOperator");
diff --git
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenTwoInputOperator.java
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenTwoInputOperator.java
index 7845e22a5b..b51ececd9a 100644
---
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenTwoInputOperator.java
+++
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenTwoInputOperator.java
@@ -16,6 +16,7 @@
*/
package org.apache.gluten.table.runtime.operators;
+import org.apache.gluten.streaming.api.operators.GlutenAbstractStreamOperator;
import org.apache.gluten.streaming.api.operators.GlutenOperator;
import org.apache.gluten.table.runtime.config.VeloxConnectorConfig;
import org.apache.gluten.table.runtime.config.VeloxQueryConfig;
@@ -36,10 +37,10 @@ import io.github.zhztheplayer.velox4j.type.RowType;
import org.apache.flink.runtime.state.StateInitializationContext;
import org.apache.flink.runtime.state.StateSnapshotContext;
-import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
import org.apache.flink.streaming.api.operators.TwoInputStreamOperator;
import org.apache.flink.streaming.api.watermark.Watermark;
import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -49,8 +50,14 @@ import java.util.Map;
/**
* Two input operator in gluten, which will call Velox to run. It receives
RowVector from upstream
* instead of flink RowData.
+ *
+ * <p>This class intentionally does not extend {@code AbstractStreamOperator}:
in the Flink version
+ * used here, {@code AbstractStreamOperator.processWatermarkStatus1/2(...)}
are final and maintain
+ * Flink's Java-side combined watermark status. Gluten needs to forward each
input's status to
+ * native, which is the source of truth for combined watermark status inside
the Gluten operator
+ * chain.
*/
-public class GlutenTwoInputOperator<IN, OUT> extends
AbstractStreamOperator<OUT>
+public class GlutenTwoInputOperator<IN, OUT> extends
GlutenAbstractStreamOperator<OUT>
implements TwoInputStreamOperator<IN, IN, OUT>, GlutenOperator,
NativeCallbackTarget {
private static final Logger LOG =
LoggerFactory.getLogger(GlutenTwoInputOperator.class);
@@ -75,6 +82,7 @@ public class GlutenTwoInputOperator<IN, OUT> extends
AbstractStreamOperator<OUT>
private VectorOutputBridge<OUT> outputBridge;
private String description;
private final GlutenMailboxHolder mailboxHolder = new GlutenMailboxHolder();
+ private transient boolean stateInitialized;
public GlutenTwoInputOperator(
StatefulPlanNode plan,
@@ -203,6 +211,8 @@ public class GlutenTwoInputOperator<IN, OUT> extends
AbstractStreamOperator<OUT>
if (element.isWatermark()) {
StatefulWatermark watermark = element.asWatermark();
output.emitWatermark(new Watermark(watermark.getTimestamp()));
+ } else if (element.isWatermarkStatus()) {
+
output.emitWatermarkStatus(GlutenWatermarkStatuses.toFlinkWatermarkStatus(element));
} else {
outputBridge.collect(
output, element.asRecord(), sessionResource.getAllocator(),
outputType);
@@ -218,26 +228,73 @@ public class GlutenTwoInputOperator<IN, OUT> extends
AbstractStreamOperator<OUT>
@Override
public void processWatermark(Watermark mark) throws Exception {
+ if (task == null) {
+ throw new NullPointerException(
+ "task is null in processWatermark; expected to be initialized in
open() and "
+ + "cleared only after close()");
+ }
task.notifyWatermark(mark.getTimestamp());
processElementInternal();
}
@Override
public void processWatermark1(Watermark mark) throws Exception {
+ if (task == null) {
+ throw new NullPointerException(
+ "task is null in processWatermark1; expected to be initialized in
open() and "
+ + "cleared only after close()");
+ }
task.notifyWatermark(mark.getTimestamp(), 0);
processElementInternal();
}
@Override
public void processWatermark2(Watermark mark) throws Exception {
+ if (task == null) {
+ throw new NullPointerException(
+ "task is null in processWatermark2; expected to be initialized in
open() and "
+ + "cleared only after close()");
+ }
task.notifyWatermark(mark.getTimestamp(), 1);
processElementInternal();
}
+ @Override
+ public void processWatermarkStatus1(WatermarkStatus status) throws Exception
{
+ if (task == null) {
+ throw new NullPointerException(
+ "task is null in processWatermarkStatus1; expected to be initialized
in open() and "
+ + "cleared only after close()");
+ }
+ task.notifyWatermarkStatus(status.isIdle(), 0);
+ processElementInternal();
+ }
+
+ @Override
+ public void processWatermarkStatus2(WatermarkStatus status) throws Exception
{
+ if (task == null) {
+ throw new NullPointerException(
+ "task is null in processWatermarkStatus2; expected to be initialized
in open() and "
+ + "cleared only after close()");
+ }
+ task.notifyWatermarkStatus(status.isIdle(), 1);
+ processElementInternal();
+ }
+
@Override
public void close() throws Exception {
closing = true;
GlutenCloseables.runWithCleanup(
+ () -> {
+ if (leftInputQueue != null) {
+ leftInputQueue.noMoreInput();
+ }
+ },
+ () -> {
+ if (rightInputQueue != null) {
+ rightInputQueue.noMoreInput();
+ }
+ },
() -> {
if (leftInputQueue != null) {
leftInputQueue.close();
@@ -303,24 +360,39 @@ public class GlutenTwoInputOperator<IN, OUT> extends
AbstractStreamOperator<OUT>
@Override
public void prepareSnapshotPreBarrier(long checkpointId) throws Exception {
// TODO: notify velox
+ processElementInternal();
super.prepareSnapshotPreBarrier(checkpointId);
}
@Override
public void snapshotState(StateSnapshotContext context) throws Exception {
// TODO: implement it
- task.snapshotState(0);
+ snapshotNativeState(context.getCheckpointId());
super.snapshotState(context);
}
@Override
public void initializeState(StateInitializationContext context) throws
Exception {
- initSession();
// TODO: implement it
- task.initializeState(0, null);
+ initializeNativeState();
super.initializeState(context);
}
+ private void snapshotNativeState(long checkpointId) {
+ if (task != null) {
+ task.snapshotState(checkpointId);
+ }
+ }
+
+ private void initializeNativeState() {
+ if (stateInitialized) {
+ return;
+ }
+ initSession();
+ task.initializeState(0, null);
+ stateInitialized = true;
+ }
+
private void initSession() {
if (sessionResource != null) {
return;
@@ -352,14 +424,18 @@ public class GlutenTwoInputOperator<IN, OUT> extends
AbstractStreamOperator<OUT>
@Override
public void notifyCheckpointComplete(long checkpointId) throws Exception {
// TODO: notify velox
- task.notifyCheckpointComplete(checkpointId);
+ if (task != null) {
+ task.notifyCheckpointComplete(checkpointId);
+ }
super.notifyCheckpointComplete(checkpointId);
}
@Override
public void notifyCheckpointAborted(long checkpointId) throws Exception {
// TODO: notify velox
- task.notifyCheckpointAborted(checkpointId);
+ if (task != null) {
+ task.notifyCheckpointAborted(checkpointId);
+ }
super.notifyCheckpointAborted(checkpointId);
}
}
diff --git
a/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenWatermarkStatuses.java
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenWatermarkStatuses.java
new file mode 100644
index 0000000000..b45def322f
--- /dev/null
+++
b/gluten-flink/runtime/src/main/java/org/apache/gluten/table/runtime/operators/GlutenWatermarkStatuses.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.table.runtime.operators;
+
+import io.github.zhztheplayer.velox4j.stateful.StatefulElement;
+
+import org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus;
+
+final class GlutenWatermarkStatuses {
+
+ private GlutenWatermarkStatuses() {}
+
+ static WatermarkStatus toFlinkWatermarkStatus(StatefulElement element) {
+ return element.asWatermarkStatus().isIdle() ? WatermarkStatus.IDLE :
WatermarkStatus.ACTIVE;
+ }
+}
diff --git
a/gluten-flink/runtime/src/main/java/org/apache/gluten/util/Utils.java
b/gluten-flink/runtime/src/main/java/org/apache/gluten/util/Utils.java
index eaa6a7d20e..840abcbf3f 100644
--- a/gluten-flink/runtime/src/main/java/org/apache/gluten/util/Utils.java
+++ b/gluten-flink/runtime/src/main/java/org/apache/gluten/util/Utils.java
@@ -18,6 +18,7 @@ package org.apache.gluten.util;
import org.apache.gluten.streaming.api.operators.GlutenOneInputOperatorFactory;
import org.apache.gluten.streaming.api.operators.GlutenOperator;
+import org.apache.gluten.streaming.api.operators.GlutenTwoInputOperatorFactory;
import org.apache.flink.connector.file.table.stream.PartitionCommitInfo;
import org.apache.flink.streaming.api.graph.StreamConfig;
@@ -64,6 +65,8 @@ public class Utils {
}
} else if (operatorFactory instanceof GlutenOneInputOperatorFactory) {
return Optional.of(((GlutenOneInputOperatorFactory)
operatorFactory).getOperator());
+ } else if (operatorFactory instanceof GlutenTwoInputOperatorFactory) {
+ return Optional.of(((GlutenTwoInputOperatorFactory)
operatorFactory).getOperator());
}
return Optional.empty();
}
diff --git
a/gluten-flink/ut/src/test/java/org/apache/gluten/streaming/api/operators/GlutenStreamTwoInputWatermarkStatusTest.java
b/gluten-flink/ut/src/test/java/org/apache/gluten/streaming/api/operators/GlutenStreamTwoInputWatermarkStatusTest.java
new file mode 100644
index 0000000000..8306c9d20d
--- /dev/null
+++
b/gluten-flink/ut/src/test/java/org/apache/gluten/streaming/api/operators/GlutenStreamTwoInputWatermarkStatusTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.streaming.api.operators;
+
+import org.apache.gluten.table.runtime.operators.GlutenTwoInputOperator;
+
+import io.github.zhztheplayer.velox4j.stateful.StatefulRecord;
+
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus;
+import org.apache.flink.streaming.util.TwoInputStreamOperatorTestHarness;
+import org.apache.flink.table.runtime.operators.join.FlinkJoinType;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GlutenStreamTwoInputWatermarkStatusTest extends
GlutenStreamJoinOperatorTestBase {
+
+ @Test
+ public void testWatermarkStatusPropagationThroughNativeJoinOperator() throws
Exception {
+ // Two-input operators combine per-input watermark status in native
execution. The operator
+ // becomes idle only when all inputs are idle, and becomes active when any
input reactivates.
+ GlutenTwoInputOperator operator =
createGlutenJoinOperator(FlinkJoinType.INNER);
+
+ try (TwoInputStreamOperatorTestHarness<StatefulRecord, StatefulRecord,
StatefulRecord> harness =
+ new TwoInputStreamOperatorTestHarness<>(operator)) {
+ harness.setup();
+ harness.open();
+
+ // One idle input is excluded from combined watermark calculation, but
the operator is not
+ // idle yet because the other input is still active.
+ harness.processWatermarkStatus1(WatermarkStatus.IDLE);
+ assertThat(harness.getOutput()).isEmpty();
+
+ // Once both inputs are idle, the combined status changes to IDLE and is
emitted downstream.
+ harness.processWatermarkStatus2(WatermarkStatus.IDLE);
+ assertThat(harness.getOutput()).containsExactly(WatermarkStatus.IDLE);
+
+ // Reactivating either input changes the combined status back to ACTIVE.
+ harness.processWatermarkStatus1(WatermarkStatus.ACTIVE);
+ assertThat(harness.getOutput()).containsExactly(WatermarkStatus.IDLE,
WatermarkStatus.ACTIVE);
+ }
+ }
+
+ @Test
+ public void testWatermarkReactivatesIdleNativeJoinOperator() throws
Exception {
+ // A watermark received after all inputs became idle should implicitly
reactivate that input.
+ // Native execution must emit ACTIVE before the resulting combined
watermark.
+ GlutenTwoInputOperator operator =
createGlutenJoinOperator(FlinkJoinType.INNER);
+
+ try (TwoInputStreamOperatorTestHarness<StatefulRecord, StatefulRecord,
StatefulRecord> harness =
+ new TwoInputStreamOperatorTestHarness<>(operator)) {
+ harness.setup();
+ harness.open();
+
+ harness.processWatermarkStatus1(WatermarkStatus.IDLE);
+ harness.processWatermarkStatus2(WatermarkStatus.IDLE);
+ assertThat(harness.getOutput()).containsExactly(WatermarkStatus.IDLE);
+
+ // Input 1's watermark makes the combined status active again and
advances the watermark.
+ harness.processWatermark1(new Watermark(1L));
+ assertThat(harness.getOutput())
+ .containsExactly(WatermarkStatus.IDLE, WatermarkStatus.ACTIVE, new
Watermark(1L));
+ }
+ }
+
+ @Test
+ public void testWatermarksUseNativeTwoInputMinimum() throws Exception {
+ // While both inputs are active, native execution should combine indexed
input watermarks by
+ // forwarding only monotonically increasing minimum watermarks downstream.
+ GlutenTwoInputOperator operator =
createGlutenJoinOperator(FlinkJoinType.INNER);
+
+ try (TwoInputStreamOperatorTestHarness<StatefulRecord, StatefulRecord,
StatefulRecord> harness =
+ new TwoInputStreamOperatorTestHarness<>(operator)) {
+ harness.setup();
+ harness.open();
+
+ // The first input alone cannot advance the combined watermark because
the second input has no
+ // watermark yet.
+ harness.processWatermark1(new Watermark(100L));
+ assertThat(harness.getOutput()).isEmpty();
+
+ // Once both inputs have watermarks, the lower input watermark is
emitted.
+ harness.processWatermark2(new Watermark(90L));
+ assertThat(harness.getOutput()).containsExactly(new Watermark(90L));
+
+ // Advancing the lower input exposes input 1's watermark as the new
combined minimum.
+ harness.processWatermark2(new Watermark(110L));
+ assertThat(harness.getOutput()).containsExactly(new Watermark(90L), new
Watermark(100L));
+ }
+ }
+}
diff --git
a/gluten-flink/ut/src/test/java/org/apache/gluten/streaming/api/operators/GlutenStreamWatermarkStatusTest.java
b/gluten-flink/ut/src/test/java/org/apache/gluten/streaming/api/operators/GlutenStreamWatermarkStatusTest.java
new file mode 100644
index 0000000000..6370eda2b3
--- /dev/null
+++
b/gluten-flink/ut/src/test/java/org/apache/gluten/streaming/api/operators/GlutenStreamWatermarkStatusTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.streaming.api.operators;
+
+import org.apache.gluten.rexnode.Utils;
+import org.apache.gluten.table.runtime.operators.GlutenOneInputOperator;
+import org.apache.gluten.util.PlanNodeIdGenerator;
+
+import io.github.zhztheplayer.velox4j.expression.TypedExpr;
+import io.github.zhztheplayer.velox4j.plan.EmptyNode;
+import io.github.zhztheplayer.velox4j.plan.PlanNode;
+import io.github.zhztheplayer.velox4j.plan.ProjectNode;
+
+import org.apache.flink.streaming.api.watermark.Watermark;
+import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
+import org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus;
+import org.apache.flink.streaming.util.OneInputStreamOperatorTestHarness;
+import org.apache.flink.table.data.RowData;
+
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GlutenStreamWatermarkStatusTest extends
GlutenStreamOperatorTestBase {
+
+ @Test
+ public void testWatermarkStatusPropagationThroughNativeProjectOperator()
throws Exception {
+ // One-input operators should forward explicit upstream IDLE/ACTIVE status
changes through
+ // native execution and emit the corresponding Flink WatermarkStatus
downstream.
+ GlutenOneInputOperator operator = createTestOperator(createProjectPlan(),
typeInfo, typeInfo);
+
+ try (OneInputStreamOperatorTestHarness<RowData, RowData> harness =
+ createTestHarness(operator, typeInfo, typeInfo)) {
+ // IDLE marks the only input idle, so the operator immediately becomes
idle.
+ harness.processWatermarkStatus(WatermarkStatus.IDLE);
+ assertThat(harness.getOutput()).containsExactly(WatermarkStatus.IDLE);
+
+ // An explicit ACTIVE status from upstream should reactivate the
operator.
+ harness.processWatermarkStatus(WatermarkStatus.ACTIVE);
+ assertThat(harness.getOutput()).containsExactly(WatermarkStatus.IDLE,
WatermarkStatus.ACTIVE);
+ }
+ }
+
+ @Test
+ public void testWatermarkReactivatesIdleNativeProjectOperator() throws
Exception {
+ // Flink treats a watermark from an idle input as an implicit
reactivation. Native execution
+ // must emit ACTIVE before forwarding the watermark so downstream
operators see valid ordering.
+ GlutenOneInputOperator operator = createTestOperator(createProjectPlan(),
typeInfo, typeInfo);
+
+ try (OneInputStreamOperatorTestHarness<RowData, RowData> harness =
+ createTestHarness(operator, typeInfo, typeInfo)) {
+ harness.processWatermarkStatus(WatermarkStatus.IDLE);
+ assertThat(harness.getOutput()).containsExactly(WatermarkStatus.IDLE);
+
+ harness.processWatermark(new Watermark(1L));
+ assertThat(harness.getOutput())
+ .containsExactly(WatermarkStatus.IDLE, WatermarkStatus.ACTIVE, new
Watermark(1L));
+ }
+ }
+
+ @Test
+ public void testRecordDoesNotReactivateIdleNativeProjectOperator() throws
Exception {
+ // Records are not watermark/status events. They should still be processed
while the input is
+ // marked idle, but they must not implicitly emit ACTIVE; upstream is
responsible for that.
+ GlutenOneInputOperator operator = createTestOperator(createProjectPlan(),
typeInfo, typeInfo);
+
+ try (OneInputStreamOperatorTestHarness<RowData, RowData> harness =
+ createTestHarness(operator, typeInfo, typeInfo)) {
+ harness.processWatermarkStatus(WatermarkStatus.IDLE);
+ assertThat(harness.getOutput()).containsExactly(WatermarkStatus.IDLE);
+
+ // The projected record is produced, while the watermark status output
remains idle only.
+ harness.processElement(new StreamRecord<>(testData.get(0), 1L));
+ assertThat(harness.getOutput()).contains(WatermarkStatus.IDLE);
+ assertThat(harness.getOutput()).doesNotContain(WatermarkStatus.ACTIVE);
+
assertThat(extractOutputFromHarness(harness)).containsExactly(testData.get(0));
+ }
+ }
+
+ private PlanNode createProjectPlan() {
+ List<String> inputFieldNames = Utils.getNamesFromRowType(rowType);
+ List<TypedExpr> veloxProjections =
+ convertRexListToVelox(createProjectionExpressions(), inputFieldNames);
+
+ return new ProjectNode(
+ PlanNodeIdGenerator.newId(),
+ List.of(new EmptyNode(veloxType)),
+ inputFieldNames,
+ veloxProjections);
+ }
+
+ private List<RexNode> createProjectionExpressions() {
+ RexNode idFieldRef =
rexBuilder.makeInputRef(typeFactory.createSqlType(SqlTypeName.INTEGER), 0);
+ RexNode nameFieldRef =
+
rexBuilder.makeInputRef(typeFactory.createSqlType(SqlTypeName.VARCHAR), 1);
+ RexNode ageFieldRef =
+
rexBuilder.makeInputRef(typeFactory.createSqlType(SqlTypeName.INTEGER), 2);
+
+ return Arrays.asList(idFieldRef, nameFieldRef, ageFieldRef);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]