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

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


The following commit(s) were added to refs/heads/master by this push:
     new 8d8b54954e3 Flink dropping watermarks hold fix (#40149)
8d8b54954e3 is described below

commit 8d8b54954e3b58b6ef360de1faa348dc052bb97b
Author: CarloMariaProietti <[email protected]>
AuthorDate: Mon Oct 12 20:13:32 2026 +0200

    Flink dropping watermarks hold fix (#40149)
    
    * Fix BufferingDoFnRunner.checkpointCompleted() leveraging polymorphism in 
BufferedElement
    
    * Add a test for the fix
    
    * refine the logic introducing minTimestampPerBuffer and improve the test
---
 .../streaming/stableinput/BufferingDoFnRunner.java | 24 ++++--
 .../wrappers/streaming/DoFnOperatorTest.java       | 91 ++++++++++++++++++++++
 2 files changed, 109 insertions(+), 6 deletions(-)

diff --git 
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java
 
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java
index 4d44d4633a2..428ab09f598 100644
--- 
a/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java
+++ 
b/runners/flink/src/main/java/org/apache/beam/runners/flink/translation/wrappers/streaming/stableinput/BufferingDoFnRunner.java
@@ -19,6 +19,7 @@ package 
org.apache.beam.runners.flink.translation.wrappers.streaming.stableinput
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
@@ -129,6 +130,9 @@ public class BufferingDoFnRunner<InputT, OutputT> 
implements DoFnRunner<InputT,
   /** The maximum number of buffers for data of not yet acknowledged 
checkpoints. */
   final int numCheckpointBuffers;
 
+  /** Minimum timestamp for each checkpoint buffer. */
+  private final long[] minTimestampPerBuffer;
+
   /** The current active state id which, on checkpoint, is linked to a 
checkpoint id. */
   int currentStateIndex;
 
@@ -193,6 +197,8 @@ public class BufferingDoFnRunner<InputT, OutputT> 
implements DoFnRunner<InputT,
           }
         };
     this.numCheckpointBuffers = initializeState(maxConcurrentCheckpoints);
+    this.minTimestampPerBuffer = new long[numCheckpointBuffers];
+    Arrays.fill(this.minTimestampPerBuffer, Long.MAX_VALUE);
     this.currentBufferingElementsHandler =
         bufferingElementsHandlerFactory.get(rotateAndGetStateIndex());
     this.keyedStateBackend = keyedStateBackend;
@@ -234,8 +240,10 @@ public class BufferingDoFnRunner<InputT, OutputT> 
implements DoFnRunner<InputT,
 
   @Override
   public void processElement(WindowedValue<InputT> elem) {
-    minBufferedElementTimestamp =
-        Math.min(elem.getTimestamp().getMillis(), minBufferedElementTimestamp);
+    final long elemTimestamp = elem.getTimestamp().getMillis();
+    minTimestampPerBuffer[currentStateIndex] =
+        Math.min(minTimestampPerBuffer[currentStateIndex], elemTimestamp);
+    minBufferedElementTimestamp = Math.min(elemTimestamp, 
minBufferedElementTimestamp);
     try (Locker lock = locker != null ? locker.get() : null) {
       if (keySelector != null) {
         keyedStateBackend.setCurrentKey(keySelector.apply(elem.getValue()));
@@ -255,8 +263,10 @@ public class BufferingDoFnRunner<InputT, OutputT> 
implements DoFnRunner<InputT,
       TimeDomain timeDomain,
       CausedByDrain causedByDrain) {
 
-    minBufferedElementTimestamp =
-        Math.min(outputTimestamp.getMillis(), minBufferedElementTimestamp);
+    final long outputTimestampInMillis = outputTimestamp.getMillis();
+    minTimestampPerBuffer[currentStateIndex] =
+        Math.min(minTimestampPerBuffer[currentStateIndex], 
outputTimestampInMillis);
+    minBufferedElementTimestamp = Math.min(outputTimestampInMillis, 
minBufferedElementTimestamp);
     try (Locker lock = locker != null ? locker.get() : null) {
       if (keySelector != null) {
         keyedStateBackend.setCurrentKey(key);
@@ -297,8 +307,9 @@ public class BufferingDoFnRunner<InputT, OutputT> 
implements DoFnRunner<InputT,
   public void checkpointCompleted(long checkpointId) throws Exception {
     List<CheckpointIdentifier> allToAck = 
gatherToBeAcknowledgedCheckpoints(checkpointId);
     for (CheckpointIdentifier toBeAcked : allToAck) {
+      final int toBeAckedId = toBeAcked.internalId;
       BufferingElementsHandler bufferingElementsHandler =
-          bufferingElementsHandlerFactory.get(toBeAcked.internalId);
+          bufferingElementsHandlerFactory.get(toBeAckedId);
       try (Locker lock = locker != null ? locker.get() : null) {
         final Iterator<BufferedElement> iterator =
             bufferingElementsHandler.getElements().iterator();
@@ -314,9 +325,10 @@ public class BufferingDoFnRunner<InputT, OutputT> 
implements DoFnRunner<InputT,
           underlying.finishBundle();
         }
         bufferingElementsHandler.clear();
+        minTimestampPerBuffer[toBeAckedId] = Long.MAX_VALUE;
       }
     }
-    minBufferedElementTimestamp = Long.MAX_VALUE;
+    minBufferedElementTimestamp = 
Arrays.stream(minTimestampPerBuffer).min().orElse(Long.MAX_VALUE);
   }
 
   public long getOutputWatermarkHold() {
diff --git 
a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java
 
b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java
index cce8f808c24..6a000f30890 100644
--- 
a/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java
+++ 
b/runners/flink/src/test/java/org/apache/beam/runners/flink/translation/wrappers/streaming/DoFnOperatorTest.java
@@ -99,6 +99,7 @@ import org.apache.flink.api.common.typeinfo.TypeInformation;
 import org.apache.flink.api.java.functions.KeySelector;
 import org.apache.flink.api.java.typeutils.ValueTypeInfo;
 import org.apache.flink.runtime.checkpoint.OperatorSubtaskState;
+import org.apache.flink.streaming.api.watermark.Watermark;
 import org.apache.flink.streaming.runtime.streamrecord.StreamRecord;
 import org.apache.flink.streaming.util.KeyedOneInputStreamOperatorTestHarness;
 import org.apache.flink.streaming.util.KeyedTwoInputStreamOperatorTestHarness;
@@ -2233,6 +2234,96 @@ public class DoFnOperatorTest {
             WindowedValues.valueInGlobalWindow(KV.of("key3", 
"finishBundle"))));
   }
 
+  /** Ensures that output watermark does not pass buffered elements */
+  @Test
+  public void testExactlyOnceBufferingOutputWatermarkCorrectness() throws 
Exception {
+    FlinkPipelineOptions options = FlinkPipelineOptions.defaults();
+    options.setStreaming(true);
+    options.setMaxBundleSize(1L);
+    options.setCheckpointingInterval(1L);
+    options.setCheckpointingMode("EXACTLY_ONCE");
+    options.setNumConcurrentCheckpoints(1);
+
+    TupleTag<String> outputTag = new TupleTag<>("main-output");
+
+    WindowedValues.FullWindowedValueCoder<String> windowedValueCoder =
+        WindowedValues.getFullCoder(StringUtf8Coder.of(), 
GlobalWindow.Coder.INSTANCE);
+
+    DoFn<String, String> doFn =
+        new DoFn<String, String>() {
+          @ProcessElement
+          // Use RequiresStableInput to force buffering elements
+          @RequiresStableInput
+          public void processElement(ProcessContext context) {
+            context.output(context.element());
+          }
+        };
+
+    DoFnOperator.MultiOutputOutputManagerFactory<String> outputManagerFactory =
+        new DoFnOperator.MultiOutputOutputManagerFactory<>(
+            outputTag,
+            WindowedValues.getFullCoder(StringUtf8Coder.of(), 
GlobalWindow.Coder.INSTANCE),
+            new SerializablePipelineOptions(options));
+
+    DoFnOperator<String, String, String> doFnOperator =
+        new DoFnOperator<>(
+            doFn,
+            "stepName",
+            windowedValueCoder,
+            Collections.emptyMap(),
+            outputTag,
+            Collections.emptyList(),
+            outputManagerFactory,
+            WindowingStrategy.globalDefault(),
+            new HashMap<>(), /* side-input mapping */
+            Collections.emptyList(), /* side inputs */
+            options,
+            null,
+            null,
+            DoFnSchemaInformation.create(),
+            Collections.emptyMap());
+
+    OneInputStreamOperatorTestHarness<WindowedValue<String>, 
WindowedValue<String>> testHarness =
+        new OneInputStreamOperatorTestHarness<>(doFnOperator);
+
+    testHarness.open();
+
+    testHarness.processElement(
+        new StreamRecord<>(WindowedValues.timestampedValueInGlobalWindow("A", 
new Instant(10))));
+    testHarness.snapshot(1L, 0L);
+    testHarness.processElement(
+        new StreamRecord<>(WindowedValues.timestampedValueInGlobalWindow("B", 
new Instant(20))));
+    testHarness.processWatermark(new Watermark(100));
+    assertThat(
+        "A@10 has not yet completed the checkpoint; output watermark must be 
held at 10 until checkpoint completion",
+        doFnOperator.getCurrentOutputWatermark(),
+        is(10L));
+
+    doFnOperator.notifyCheckpointComplete(1L);
+    assertThat(
+        "A@10 has been checked but B@20 is still buffered; output watermark 
must not pass it",
+        doFnOperator.getCurrentOutputWatermark(),
+        is(20L));
+
+    testHarness.snapshot(2L, 0L);
+    doFnOperator.notifyCheckpointComplete(2L);
+    assertThat(
+        "B@20 has been checked and the buffer is empty; output watermark must 
advance to input watermark",
+        doFnOperator.getCurrentOutputWatermark(),
+        is(100L));
+
+    assertThat(
+        testHarness.getOutput(),
+        contains(
+            new Watermark(10),
+            new 
StreamRecord<>(WindowedValues.timestampedValueInGlobalWindow("A", new 
Instant(10))),
+            new Watermark(20),
+            new 
StreamRecord<>(WindowedValues.timestampedValueInGlobalWindow("B", new 
Instant(20))),
+            new Watermark(100)));
+
+    testHarness.close();
+  }
+
   @Test(expected = IllegalStateException.class)
   public void testFailOnRequiresStableInputAndDisabledCheckpointing() {
     TupleTag<KV<String, String>> outputTag = new TupleTag<>("main-output");

Reply via email to