This is an automated email from the ASF dual-hosted git repository. MartijnVisser pushed a commit to branch release-2.2 in repository https://gitbox.apache.org/repos/asf/flink.git
commit 9747a17c235b527d2ff2e5a779ed448290f07a94 Author: Martijn Visser <[email protected]> AuthorDate: Thu Jul 2 11:29:31 2026 +0200 [FLINK-39481][task] Execute deferrable mails when finishing an operator Since FLINK-35528 the continuation mail of an interrupted timer-firing chain is deferrable and hidden from tryYield(). The finish drain in StreamOperatorWrapper therefore skipped it, so a task reaching end of input mid-firing forwarded EndOfData before the pending watermark, and downstream operators discarded the state of windows that only fire on that watermark. Restore the pre-FLINK-35528 behaviour by draining with a new yield variant that matches deferrable mails by their original priority, used only in that place. Generated-by: Claude Code (Fable 5) (cherry picked from commit 83055aead2ead294715a59b97fa5b0dc1dd2453c) --- .../runtime/tasks/StreamOperatorWrapper.java | 14 ++- .../streaming/runtime/tasks/mailbox/Mail.java | 5 + .../runtime/tasks/mailbox/MailboxExecutorImpl.java | 13 ++- .../runtime/tasks/mailbox/TaskMailbox.java | 11 +++ .../runtime/tasks/mailbox/TaskMailboxImpl.java | 23 +++-- .../runtime/tasks/StreamOperatorWrapperTest.java | 18 ++++ .../tasks/mailbox/MailboxExecutorImplTest.java | 26 ++++++ ...nalignedCheckpointsInterruptibleTimersTest.java | 101 +++++++++++++++++++++ .../tasks/StreamTaskMailboxTestHarness.java | 10 ++ 9 files changed, 211 insertions(+), 10 deletions(-) diff --git a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapper.java b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapper.java index 6be1e5cf0c1..4f300ac8974 100755 --- a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapper.java +++ b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapper.java @@ -27,6 +27,7 @@ import org.apache.flink.streaming.api.operators.AbstractStreamOperatorV2; import org.apache.flink.streaming.api.operators.BoundedMultiInput; import org.apache.flink.streaming.api.operators.BoundedOneInput; import org.apache.flink.streaming.api.operators.StreamOperator; +import org.apache.flink.streaming.runtime.tasks.mailbox.MailboxExecutorImpl; import javax.annotation.Nonnull; @@ -39,6 +40,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static org.apache.flink.util.Preconditions.checkNotNull; +import static org.apache.flink.util.Preconditions.checkState; /** * This class handles the finish, endInput and other related logic of a {@link StreamOperator}. It @@ -55,7 +57,7 @@ public class StreamOperatorWrapper<OUT, OP extends StreamOperator<OUT>> { @SuppressWarnings("OptionalUsedAsFieldOrParameterType") private final Optional<ProcessingTimeService> processingTimeService; - private final MailboxExecutor mailboxExecutor; + private final MailboxExecutorImpl mailboxExecutor; private final boolean isHead; @@ -73,7 +75,11 @@ public class StreamOperatorWrapper<OUT, OP extends StreamOperator<OUT>> { this.wrapped = checkNotNull(wrapped); this.processingTimeService = checkNotNull(processingTimeService); - this.mailboxExecutor = checkNotNull(mailboxExecutor); + checkState( + mailboxExecutor instanceof MailboxExecutorImpl, + "The mailbox executor must be a MailboxExecutorImpl, so that deferrable mails" + + " can be executed when finishing the operator (FLINK-39481)."); + this.mailboxExecutor = (MailboxExecutorImpl) mailboxExecutor; this.isHead = isHead; } @@ -183,7 +189,9 @@ public class StreamOperatorWrapper<OUT, OP extends StreamOperator<OUT>> { // run the mailbox processing loop until all operations are finished while (!finishedFuture.isDone()) { - while (mailboxExecutor.tryYield()) {} + // FLINK-39481: also execute deferrable mails, so that a deferred watermark + // continuation completes and forwards the watermark before EndOfData is sent + while (mailboxExecutor.tryYieldIgnoringDeferrable()) {} // we wait a little bit to avoid unnecessary CPU occupation due to empty loops, // such as when all mails of the operator have been processed but the closed future diff --git a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/Mail.java b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/Mail.java index 016ae6be2cf..b68c8483eff 100644 --- a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/Mail.java +++ b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/Mail.java @@ -103,6 +103,11 @@ public class Mail { return mailOptions.isDeferrable() ? TaskMailbox.MIN_PRIORITY : priority; } + /** The priority the mail was created with, ignoring the deferrable demotion. */ + public int getPriorityIgnoringDeferrable() { + return priority; + } + public void tryCancel(boolean mayInterruptIfRunning) { if (runnable instanceof Future) { ((Future<?>) runnable).cancel(mayInterruptIfRunning); diff --git a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImpl.java b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImpl.java index 4fef813b109..2ad3d7b7e86 100644 --- a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImpl.java +++ b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImpl.java @@ -98,7 +98,18 @@ public final class MailboxExecutorImpl implements MailboxExecutor { @Override public boolean tryYield() { - Optional<Mail> optionalMail = mailbox.tryTake(priority); + return runIfPresent(mailbox.tryTake(priority)); + } + + /** + * Same as {@link #tryYield()}, but also executes deferrable mails (see {@link + * MailboxExecutor.MailOptions#deferrable()}). + */ + public boolean tryYieldIgnoringDeferrable() { + return runIfPresent(mailbox.tryTakeIgnoringDeferrable(priority)); + } + + private static boolean runIfPresent(Optional<Mail> optionalMail) { if (optionalMail.isPresent()) { try { optionalMail.get().run(); diff --git a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailbox.java b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailbox.java index df3a6f065de..62745e8eaa7 100644 --- a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailbox.java +++ b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailbox.java @@ -108,6 +108,17 @@ public interface TaskMailbox { */ Optional<Mail> tryTake(int priority); + /** + * Same as {@link #tryTake(int)}, but deferrable mails are matched against the priority they + * were created with instead of {@link #MIN_PRIORITY} (see {@link + * MailboxExecutor.MailOptions#deferrable()}). + * + * <p>Must be called from the mailbox thread ({@link #isMailboxThread()}. + * + * @throws IllegalStateException if mailbox is already closed. + */ + Optional<Mail> tryTakeIgnoringDeferrable(int priority); + /** * This method returns the oldest mail from the mailbox (head of queue) or blocks until a mail * is available. diff --git a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailboxImpl.java b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailboxImpl.java index 56188c3167f..18bff55c588 100644 --- a/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailboxImpl.java +++ b/flink-runtime/src/main/java/org/apache/flink/streaming/runtime/tasks/mailbox/TaskMailboxImpl.java @@ -115,12 +115,21 @@ public class TaskMailboxImpl implements TaskMailbox { @Override public Optional<Mail> tryTake(int priority) { + return tryTake(priority, false); + } + + @Override + public Optional<Mail> tryTakeIgnoringDeferrable(int priority) { + return tryTake(priority, true); + } + + private Optional<Mail> tryTake(int priority, boolean ignoreDeferrable) { checkIsMailboxThread(); checkTakeStateConditions(); moveUrgentMailsToBatchIfNeeded(true); - Mail head = takeOrNull(batch, priority); + Mail head = takeOrNull(batch, priority, ignoreDeferrable); if (head != null) { return Optional.of(head); } @@ -130,7 +139,7 @@ public class TaskMailboxImpl implements TaskMailbox { final ReentrantLock lock = this.lock; lock.lock(); try { - final Mail value = takeOrNull(queue, priority); + final Mail value = takeOrNull(queue, priority, ignoreDeferrable); if (value == null) { return Optional.empty(); } @@ -148,7 +157,7 @@ public class TaskMailboxImpl implements TaskMailbox { moveUrgentMailsToBatchIfNeeded(true); - Mail head = takeOrNull(batch, priority); + Mail head = takeOrNull(batch, priority, false); if (head != null) { return head; } @@ -156,7 +165,7 @@ public class TaskMailboxImpl implements TaskMailbox { lock.lockInterruptibly(); try { Mail headMail; - while ((headMail = takeOrNull(queue, priority)) == null) { + while ((headMail = takeOrNull(queue, priority, false)) == null) { // to ease debugging notEmpty.await(1, TimeUnit.SECONDS); } @@ -333,7 +342,7 @@ public class TaskMailboxImpl implements TaskMailbox { // ------------------------------------------------------------------------------------------------------------------ @Nullable - private Mail takeOrNull(Deque<Mail> queue, int priority) { + private Mail takeOrNull(Deque<Mail> queue, int priority, boolean ignoreDeferrable) { if (queue.isEmpty()) { return null; } @@ -341,7 +350,9 @@ public class TaskMailboxImpl implements TaskMailbox { Iterator<Mail> iterator = queue.iterator(); while (iterator.hasNext()) { Mail mail = iterator.next(); - if (mail.getPriority() >= priority) { + int mailPriority = + ignoreDeferrable ? mail.getPriorityIgnoringDeferrable() : mail.getPriority(); + if (mailPriority >= priority) { iterator.remove(); return mail; } diff --git a/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapperTest.java b/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapperTest.java index 09c5c3100c7..99a37642102 100644 --- a/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapperTest.java +++ b/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamOperatorWrapperTest.java @@ -45,6 +45,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -146,6 +147,23 @@ class StreamOperatorWrapperTest { .containsExactlyElementsOf(expected.subList(2, expected.size())); } + @Test + void testFinishExecutesDeferrableMails() throws Exception { + // FLINK-39481: a deferrable mail (e.g. a deferred watermark continuation) must be + // executed while finishing the operator, before EndOfData is sent downstream + MailboxExecutor localExecutor = + containingTask.getMailboxExecutorFactory().createExecutor(0); + AtomicBoolean deferrableMailExecuted = new AtomicBoolean(); + localExecutor.execute( + MailboxExecutor.MailOptions.deferrable(), + () -> deferrableMailExecuted.set(true), + "deferrable mail"); + + operatorWrappers.get(0).finish(containingTask.getActionExecutor(), StopMode.DRAIN); + + assertThat(deferrableMailExecuted.get()).isTrue(); + } + @Test void testFinishingOperatorWithException() { AbstractStreamOperator<Void> streamOperator = diff --git a/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImplTest.java b/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImplTest.java index 1b6fddc7d91..63a614c1269 100644 --- a/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImplTest.java +++ b/flink-runtime/src/test/java/org/apache/flink/streaming/runtime/tasks/mailbox/MailboxExecutorImplTest.java @@ -130,6 +130,32 @@ class MailboxExecutorImplTest { assertThat(deferrableMailExecuted.get()).isTrue(); } + @Test + void testTryYieldIgnoringDeferrable() throws Exception { + int priority = 42; + MailboxExecutorImpl localExecutor = + (MailboxExecutorImpl) mailboxProcessor.getMailboxExecutor(priority); + + AtomicBoolean deferrableMailExecuted = new AtomicBoolean(); + + localExecutor.execute( + MailboxExecutor.MailOptions.deferrable(), + () -> deferrableMailExecuted.set(true), + "deferrable mail"); + // the deferrable mail keeps its original priority, so it stays hidden from executors + // with a higher priority + assertThat( + ((MailboxExecutorImpl) mailboxProcessor.getMailboxExecutor(priority + 1)) + .tryYieldIgnoringDeferrable()) + .isFalse(); + assertThat(deferrableMailExecuted.get()).isFalse(); + assertThat(localExecutor.tryYield()).isFalse(); + assertThat(deferrableMailExecuted.get()).isFalse(); + + assertThat(localExecutor.tryYieldIgnoringDeferrable()).isTrue(); + assertThat(deferrableMailExecuted.get()).isTrue(); + } + @Test void testClose() throws Exception { final TestRunnable yieldRun = new TestRunnable(); diff --git a/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/io/checkpointing/UnalignedCheckpointsInterruptibleTimersTest.java b/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/io/checkpointing/UnalignedCheckpointsInterruptibleTimersTest.java index b73021125ff..105d55f9a1e 100644 --- a/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/io/checkpointing/UnalignedCheckpointsInterruptibleTimersTest.java +++ b/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/io/checkpointing/UnalignedCheckpointsInterruptibleTimersTest.java @@ -21,7 +21,11 @@ package org.apache.flink.streaming.runtime.io.checkpointing; import org.apache.flink.api.common.operators.MailboxExecutor; import org.apache.flink.api.common.typeinfo.Types; import org.apache.flink.api.common.typeutils.base.StringSerializer; +import org.apache.flink.api.java.functions.KeySelector; import org.apache.flink.configuration.CheckpointingOptions; +import org.apache.flink.runtime.io.network.api.EndOfData; +import org.apache.flink.runtime.io.network.api.EndOfPartitionEvent; +import org.apache.flink.runtime.io.network.api.StopMode; import org.apache.flink.streaming.api.graph.StreamConfig; import org.apache.flink.streaming.api.operators.AbstractStreamOperator; import org.apache.flink.streaming.api.operators.InternalTimer; @@ -147,6 +151,58 @@ class UnalignedCheckpointsInterruptibleTimersTest { } } + /** + * FLINK-39481: an interrupted timer-firing chain defers its continuation (and the downstream + * watermark emission) to a deferrable mail. When EndOfData arrives at that point, finishing the + * operator must still complete the deferred work, otherwise downstream operators never receive + * the watermark and lose the state of windows that only fire on it. + */ + @Test + void testDeferredWatermarkIsEmittedBeforeEndOfData() throws Exception { + final Instant windowEnd = Instant.ofEpochMilli(1000L); + final int numKeys = 2; + + try (final StreamTaskMailboxTestHarness<String> harness = + new StreamTaskMailboxTestHarnessBuilder<>(OneInputStreamTask::new, Types.STRING) + .addJobConfig( + CheckpointingOptions.CHECKPOINTING_INTERVAL, Duration.ofSeconds(1)) + .addJobConfig(CheckpointingOptions.ENABLE_UNALIGNED, true) + .addJobConfig( + CheckpointingOptions.ENABLE_UNALIGNED_INTERRUPTIBLE_TIMERS, true) + .setCollectNetworkEvents() + .setKeyType(Types.STRING) + .addInput(Types.STRING, 1, (KeySelector<String, String>) value -> value) + .setupOperatorChain( + SimpleOperatorFactory.of(new TimerPerElement(windowEnd))) + .name("first") + .finishForSingletonOperatorChain(StringSerializer.INSTANCE) + .build()) { + harness.setAutoProcess(false); + for (int keyIdx = 0; keyIdx < numKeys; keyIdx++) { + harness.processElement(new StreamRecord<>(String.format("key-%d", keyIdx))); + } + harness.processAll(); + + harness.processElement(asWatermark(windowEnd)); + harness.processEvent(new EndOfData(StopMode.DRAIN), 0, 0); + harness.processEvent(EndOfPartitionEvent.INSTANCE, 0, 0); + + // the mailbox loop lets the default action process EndOfData at a batch boundary, + // while the re-deferred watermark continuation is still pending; the single-step + // processing of processAll() cannot reach that interleaving + harness.runMailboxLoop(); + + assertThat(harness.getOutput()) + .containsExactly( + asFiredRecord("key-0"), + asMailRecord("key-0"), + asFiredRecord("key-1"), + asMailRecord("key-1"), + asWatermark(windowEnd), + new EndOfData(StopMode.DRAIN)); + } + } + private static Watermark asWatermark(Instant timestamp) { return new Watermark(timestamp.toEpochMilli()); } @@ -237,4 +293,49 @@ class UnalignedCheckpointsInterruptibleTimersTest { return new MultipleTimersAtTheSameTimestamp(copy); } } + + /** + * Registers a timer for the current key on every element; every fired timer enqueues a mail, so + * that firing is interrupted after each timer. + */ + private static class TimerPerElement extends AbstractStreamOperator<String> + implements OneInputStreamOperator<String, String>, + Triggerable<String, String>, + YieldingOperator<String> { + + private final Instant timerTimestamp; + private transient @Nullable MailboxExecutor mailboxExecutor; + + TimerPerElement(Instant timerTimestamp) { + this.timerTimestamp = timerTimestamp; + } + + @Override + public boolean useInterruptibleTimers() { + return true; + } + + @Override + public void setMailboxExecutor(MailboxExecutor mailboxExecutor) { + super.setMailboxExecutor(mailboxExecutor); + this.mailboxExecutor = mailboxExecutor; + } + + @Override + public void processElement(StreamRecord<String> element) { + final InternalTimerService<String> timers = + getInternalTimerService("timers", StringSerializer.INSTANCE, this); + timers.registerEventTimeTimer("window", timerTimestamp.toEpochMilli()); + } + + @Override + public void onEventTime(InternalTimer<String, String> timer) throws Exception { + mailboxExecutor.execute( + () -> output.collect(asMailRecord(timer.getKey())), "mail-" + timer.getKey()); + output.collect(asFiredRecord(timer.getKey())); + } + + @Override + public void onProcessingTime(InternalTimer<String, String> timer) throws Exception {} + } } diff --git a/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamTaskMailboxTestHarness.java b/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamTaskMailboxTestHarness.java index ddd06e313e3..a3320717cfe 100644 --- a/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamTaskMailboxTestHarness.java +++ b/flink-streaming-java/src/test/java/org/apache/flink/streaming/runtime/tasks/StreamTaskMailboxTestHarness.java @@ -136,6 +136,16 @@ public class StreamTaskMailboxTestHarness<OUT> implements AutoCloseable { return false; } + /** + * Runs the production mailbox loop until it is suspended or all actions are completed. Unlike + * {@link #processAll()}, this preserves the production interleaving of mails and the default + * action: the default action can process input at a mail-batch boundary while mails enqueued + * during the batch are still pending. + */ + public void runMailboxLoop() throws Exception { + streamTask.mailboxProcessor.runMailboxLoop(); + } + public MailboxExecutor getExecutor(int priority) { return streamTask.getMailboxExecutorFactory().createExecutor(priority); }
