This is an automated email from the ASF dual-hosted git repository.

pnowojski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git

commit d93e92c665af50f92ff3f25b68ca3943ad2699fa
Author: Piotr Nowojski <[email protected]>
AuthorDate: Fri Jun 14 15:12:26 2024 +0200

    fixup! [FLINK-34252][table] Optimize WatermarkAssignerOperator to avoid 
calling System.currentTimeMillis per every record
---
 .../wmassigners/WatermarkAssignerOperator.java     | 97 +++++++++++++++++-----
 .../wmassigners/WatermarkAssignerOperatorTest.java | 69 ++++++++++++---
 2 files changed, 132 insertions(+), 34 deletions(-)

diff --git 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperator.java
 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperator.java
index 9a7a5067973..5fd7c8a22e1 100644
--- 
a/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperator.java
+++ 
b/flink-table/flink-table-runtime/src/main/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperator.java
@@ -18,6 +18,7 @@
 
 package org.apache.flink.table.runtime.operators.wmassigners;
 
+import org.apache.flink.annotation.VisibleForTesting;
 import org.apache.flink.api.common.functions.DefaultOpenContext;
 import org.apache.flink.api.common.functions.util.FunctionUtils;
 import org.apache.flink.streaming.api.operators.AbstractStreamOperator;
@@ -30,6 +31,7 @@ import 
org.apache.flink.streaming.runtime.watermarkstatus.WatermarkStatus;
 import org.apache.flink.table.data.RowData;
 import org.apache.flink.table.runtime.generated.WatermarkGenerator;
 
+import static org.apache.flink.util.Preconditions.checkArgument;
 import static org.apache.flink.util.Preconditions.checkNotNull;
 
 /**
@@ -52,12 +54,22 @@ public class WatermarkAssignerOperator extends 
AbstractStreamOperator<RowData>
 
     private transient long watermarkInterval;
 
+    private transient long timerInterval;
+
     private transient long currentWatermark;
 
-    private transient long lastRecordTime;
+    // Last time watermark have been (periodically) emitted
+    private transient long lastWatermarkPeriodicEmitTime;
+
+    // Last time idleness status has been checked
+    private transient long timeSinceLastIdleCheck;
 
     private transient WatermarkStatus currentStatus = WatermarkStatus.ACTIVE;
 
+    private transient long processedElements;
+
+    private transient long lastIdleCheckProcessedElements = -1;
+
     /**
      * Create a watermark assigner operator.
      *
@@ -87,11 +99,14 @@ public class WatermarkAssignerOperator extends 
AbstractStreamOperator<RowData>
         // watermark and timestamp should start from 0
         this.currentWatermark = 0;
         this.watermarkInterval = 
getExecutionConfig().getAutoWatermarkInterval();
-        this.lastRecordTime = 
getProcessingTimeService().getCurrentProcessingTime();
+        long now = getProcessingTimeService().getCurrentProcessingTime();
+        this.lastWatermarkPeriodicEmitTime = now;
+        this.timeSinceLastIdleCheck = now;
 
-        if (watermarkInterval > 0) {
-            long now = getProcessingTimeService().getCurrentProcessingTime();
-            getProcessingTimeService().registerTimer(now + watermarkInterval, 
this);
+        if (watermarkInterval > 0 || idleTimeout > 0) {
+            this.timerInterval =
+                    calculateProcessingTimeTimerInterval(watermarkInterval, 
idleTimeout);
+            getProcessingTimeService().registerTimer(now + timerInterval, 
this);
         }
 
         FunctionUtils.setFunctionRuntimeContext(watermarkGenerator, 
getRuntimeContext());
@@ -100,12 +115,11 @@ public class WatermarkAssignerOperator extends 
AbstractStreamOperator<RowData>
 
     @Override
     public void processElement(StreamRecord<RowData> element) throws Exception 
{
-        if (idleTimeout > 0) {
-            if (currentStatus.equals(WatermarkStatus.IDLE)) {
-                // mark the channel active
-                emitWatermarkStatus(WatermarkStatus.ACTIVE);
-            }
-            lastRecordTime = 
getProcessingTimeService().getCurrentProcessingTime();
+        processedElements++;
+
+        if (isIdlenessEnabled() && currentStatus.equals(WatermarkStatus.IDLE)) 
{
+            // mark the channel active
+            emitWatermarkStatus(WatermarkStatus.ACTIVE);
         }
 
         RowData row = element.getValue();
@@ -139,19 +153,28 @@ public class WatermarkAssignerOperator extends 
AbstractStreamOperator<RowData>
 
     @Override
     public void onProcessingTime(long timestamp) throws Exception {
-        advanceWatermark();
+        // timestamp and now can be off in case TM is heavily overloaded.
+        long now = getProcessingTimeService().getCurrentProcessingTime();
 
-        if (idleTimeout > 0 && currentStatus.equals(WatermarkStatus.ACTIVE)) {
-            final long currentTime = 
getProcessingTimeService().getCurrentProcessingTime();
-            if (currentTime - lastRecordTime > idleTimeout) {
-                // mark the channel as idle to ignore watermarks from this 
channel
-                emitWatermarkStatus(WatermarkStatus.IDLE);
-            }
+        if (watermarkInterval > 0 && lastWatermarkPeriodicEmitTime + 
watermarkInterval <= now) {
+            lastWatermarkPeriodicEmitTime = now;
+            advanceWatermark();
+        }
+
+        if (processedElements != lastIdleCheckProcessedElements) {
+            timeSinceLastIdleCheck = now;
+            lastIdleCheckProcessedElements = processedElements;
+        }
+
+        if (isIdlenessEnabled()
+                && currentStatus.equals(WatermarkStatus.ACTIVE)
+                && timeSinceLastIdleCheck + idleTimeout <= now) {
+            // mark the channel as idle to ignore watermarks from this channel
+            emitWatermarkStatus(WatermarkStatus.IDLE);
         }
 
         // register next timer
-        long now = getProcessingTimeService().getCurrentProcessingTime();
-        getProcessingTimeService().registerTimer(now + watermarkInterval, 
this);
+        getProcessingTimeService().registerTimer(now + timerInterval, this);
     }
 
     /**
@@ -163,7 +186,7 @@ public class WatermarkAssignerOperator extends 
AbstractStreamOperator<RowData>
         // if we receive a Long.MAX_VALUE watermark we forward it since it is 
used
         // to signal the end of input and to not block watermark progress 
downstream
         if (mark.getTimestamp() == Long.MAX_VALUE && currentWatermark != 
Long.MAX_VALUE) {
-            if (idleTimeout > 0 && currentStatus.equals(WatermarkStatus.IDLE)) 
{
+            if (isIdlenessEnabled() && 
currentStatus.equals(WatermarkStatus.IDLE)) {
                 // mark the channel active
                 emitWatermarkStatus(WatermarkStatus.ACTIVE);
             }
@@ -193,4 +216,36 @@ public class WatermarkAssignerOperator extends 
AbstractStreamOperator<RowData>
         FunctionUtils.closeFunction(watermarkGenerator);
         super.close();
     }
+
+    private boolean isIdlenessEnabled() {
+        return idleTimeout > 0;
+    }
+
+    @VisibleForTesting
+    static long calculateProcessingTimeTimerInterval(long watermarkInterval, 
long idleTimeout) {
+        checkArgument(watermarkInterval > 0 || idleTimeout > 0);
+        if (watermarkInterval <= 0) {
+            return idleTimeout;
+        }
+        if (idleTimeout <= 0) {
+            return watermarkInterval;
+        }
+
+        long smallerInterval = Math.min(watermarkInterval, idleTimeout);
+        long largerInterval = Math.max(watermarkInterval, idleTimeout);
+
+        // If one of the intervals is 5x smaller, just pick the smaller one. 
The firing interval
+        // for the smaller one this way will be perfectly accurate, while for 
the larger one it will
+        // be good enoughâ„¢. For example one timer is every 2s the other every 
11s, the 2nd timer
+        // will be effectively checked every 12s, which is an acceptable 
accuracy.
+        long timerInterval;
+        if (smallerInterval * 5 < largerInterval) {
+            timerInterval = smallerInterval;
+        } else {
+            // Otherwise, just pick an interval 5x smaller than the smaller 
interval. Again accuracy
+            // will be good enoughâ„¢.
+            timerInterval = smallerInterval / 5;
+        }
+        return Math.max(timerInterval, 1);
+    }
 }
diff --git 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperatorTest.java
 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperatorTest.java
index e5deacb3999..9cd558629bc 100644
--- 
a/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperatorTest.java
+++ 
b/flink-table/flink-table-runtime/src/test/java/org/apache/flink/table/runtime/operators/wmassigners/WatermarkAssignerOperatorTest.java
@@ -37,6 +37,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
+import static 
org.apache.flink.table.runtime.operators.wmassigners.WatermarkAssignerOperator.calculateProcessingTimeTimerInterval;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.fail;
 
@@ -46,6 +47,24 @@ public class WatermarkAssignerOperatorTest extends 
WatermarkAssignerOperatorTest
     private static final WatermarkGenerator WATERMARK_GENERATOR =
             new BoundedOutOfOrderWatermarkGenerator(0, 1);
 
+    @Test
+    public void testCalculateProcessingTimeTimerInterval() {
+        assertThat(calculateProcessingTimeTimerInterval(5, 0)).isEqualTo(5);
+        assertThat(calculateProcessingTimeTimerInterval(5, -1)).isEqualTo(5);
+
+        assertThat(calculateProcessingTimeTimerInterval(0, 5)).isEqualTo(5);
+        assertThat(calculateProcessingTimeTimerInterval(-1, 5)).isEqualTo(5);
+
+        assertThat(calculateProcessingTimeTimerInterval(5, 42)).isEqualTo(5);
+        assertThat(calculateProcessingTimeTimerInterval(42, 5)).isEqualTo(5);
+
+        assertThat(calculateProcessingTimeTimerInterval(2, 4)).isEqualTo(1);
+        assertThat(calculateProcessingTimeTimerInterval(4, 2)).isEqualTo(1);
+
+        assertThat(calculateProcessingTimeTimerInterval(100, 
110)).isEqualTo(20);
+        assertThat(calculateProcessingTimeTimerInterval(110, 
100)).isEqualTo(20);
+    }
+
     @Test
     public void testWatermarkAssignerWithIdleSource() throws Exception {
         // with timeout 1000 ms
@@ -68,7 +87,10 @@ public class WatermarkAssignerOperatorTest extends 
WatermarkAssignerOperatorTest
         expectedOutput.add(new Watermark(3));
         assertThat(filterOutRecords(output)).isEqualTo(expectedOutput);
 
-        testHarness.setProcessingTime(1001);
+        stepProcessingTime(testHarness, 52, 1050, 50);
+        assertThat(filterOutRecords(output)).isEqualTo(expectedOutput);
+
+        stepProcessingTime(testHarness, 1051, 1100, 50);
         expectedOutput.add(WatermarkStatus.IDLE);
         assertThat(filterOutRecords(output)).isEqualTo(expectedOutput);
 
@@ -79,34 +101,55 @@ public class WatermarkAssignerOperatorTest extends 
WatermarkAssignerOperatorTest
         testHarness.processElement(new StreamRecord<>(GenericRowData.of(7L)));
         testHarness.processElement(new StreamRecord<>(GenericRowData.of(8L)));
 
-        testHarness.setProcessingTime(1060);
+        assertThat(filterOutRecords(output)).isEqualTo(expectedOutput);
+
+        stepProcessingTime(testHarness, 1101, 1200, 50);
         expectedOutput.add(new Watermark(7));
         assertThat(filterOutRecords(output)).isEqualTo(expectedOutput);
     }
 
     @Test
-    public void testIdleStateAvoidanceWithConsistentDataFlow() throws 
Exception {
+    public void testWatermarkIntervalSmallerThanIdleTimeout() throws Exception 
{
+        testIdleTimeout(1000, 50);
+    }
+
+    @Test
+    public void testIdleTimeoutSmallerThanWatermarkInterval() throws Exception 
{
+        testIdleTimeout(50, 1000);
+    }
+
+    private void testIdleTimeout(long idleTimeout, long watermarkInterval) 
throws Exception {
+        long step = Math.min(idleTimeout, watermarkInterval);
         OneInputStreamOperatorTestHarness<RowData, RowData> testHarness =
-                createTestHarness(0, WATERMARK_GENERATOR, 1000);
-        testHarness.getExecutionConfig().setAutoWatermarkInterval(50);
+                createTestHarness(0, WATERMARK_GENERATOR, idleTimeout);
+        
testHarness.getExecutionConfig().setAutoWatermarkInterval(watermarkInterval);
         testHarness.open();
 
         ConcurrentLinkedQueue<Object> output = testHarness.getOutput();
-        List<Object> expectedOutput = new ArrayList<>();
 
+        long timeBetweenRecords = (long) (idleTimeout * 0.9);
         // Process elements at intervals less than idleTimeout (1000ms)
         for (long i = 1; i <= 10; i++) {
-            long timestamp = i * 900;
+            long timestamp = i * timeBetweenRecords;
             testHarness.processElement(new 
StreamRecord<>(GenericRowData.of(timestamp), timestamp));
-            testHarness.setProcessingTime(timestamp);
-
-            // Expect a watermark for each element, lagging by 1
-            expectedOutput.add(new Watermark(timestamp - 1));
+            stepProcessingTime(testHarness, timestamp, timestamp + 
timeBetweenRecords - 1, step);
         }
 
         // Check if the status ever becomes IDLE (it shouldn't)
         
assertThat(extractWatermarkStatuses(output)).doesNotContain(WatermarkStatus.IDLE);
-        assertThat(filterOutRecords(output)).isEqualTo(expectedOutput);
+    }
+
+    private void stepProcessingTime(
+            OneInputStreamOperatorTestHarness<?, ?> testHarness,
+            long fromInclusive,
+            long toInclusive,
+            long step)
+            throws Exception {
+        for (long time = fromInclusive; time < toInclusive; time += step) {
+            // incrementally fire processing time timers
+            testHarness.setProcessingTime(time);
+        }
+        testHarness.setProcessingTime(toInclusive);
     }
 
     @Test
@@ -238,11 +281,11 @@ public class WatermarkAssignerOperatorTest extends 
WatermarkAssignerOperatorTest
         expected.add(Watermark.MAX_WATERMARK);
 
         // num_watermark + num_records
-        assertThat(testHarness.getOutput()).hasSize(expected.size() + 11);
         List<Watermark> results = extractWatermarks(testHarness.getOutput());
         assertThat(results).isEqualTo(expected);
         assertThat(MyWatermarkGenerator.openCalled).isTrue();
         assertThat(MyWatermarkGenerator.closeCalled).isTrue();
+        assertThat(testHarness.getOutput()).hasSize(expected.size() + 11);
     }
 
     private static OneInputStreamOperatorTestHarness<RowData, RowData> 
createTestHarness(

Reply via email to