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

wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git


The following commit(s) were added to refs/heads/main by this push:
     new 7d7519f9c [runtime][java] Keep worker lock ownership when an async 
call leaves the interrupt flag set (#1298)
7d7519f9c is described below

commit 7d7519f9cdae66ad7794e9c62b15c2ea393a3a0f
Author: Sangkyoon Nam <[email protected]>
AuthorDate: Wed Oct 21 11:48:15 2026 +0900

    [runtime][java] Keep worker lock ownership when an async call leaves the 
interrupt flag set (#1298)
    
    Generated-by: Claude Code 2.1.303 (Claude Opus 5.5)
---
 .../runtime/async/ContinuationActionExecutor.java  | 23 +++++-
 .../parallel/ParallelExecutionCoordinator.java     | 14 +++-
 .../operator/parallel/ParallelExecutionLock.java   |  5 ++
 .../async/ContinuationActionExecutorTest.java      | 71 +++++++++++++++++
 .../parallel/ParallelExecutionCoordinatorTest.java | 89 ++++++++++++++++++++--
 5 files changed, 192 insertions(+), 10 deletions(-)

diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutor.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutor.java
index 6132ae039..d61fe390d 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutor.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutor.java
@@ -101,8 +101,7 @@ public class ContinuationActionExecutor {
         try {
             return supplier.get();
         } finally {
-            parallelExecutionLock.acquireByWorker(context.getRecordIndex(), 
context.getTaskIndex());
-            contextRestorer.restore(context.getKey(), context.getActionTask());
+            reacquireAndRestore(context);
         }
     }
 
@@ -132,9 +131,27 @@ public class ContinuationActionExecutor {
         try {
             return batchExecutor.execute(suppliers, timeout, maxParallelism);
         } finally {
+            reacquireAndRestore(context);
+        }
+    }
+
+    /**
+     * Re-acquires worker ownership and restores the runner context. An 
interrupt flag left set by
+     * the call (for example by code that re-interrupts after catching {@link 
InterruptedException})
+     * must not discard its outcome, so the flag is cleared for the wait and 
restored afterward. A
+     * new interrupt during the wait, such as the coordinator's shutdown 
interrupts, still aborts
+     * it.
+     */
+    private void reacquireAndRestore(ContinuationContext context) throws 
InterruptedException {
+        boolean interrupted = Thread.interrupted();
+        try {
             parallelExecutionLock.acquireByWorker(context.getRecordIndex(), 
context.getTaskIndex());
-            contextRestorer.restore(context.getKey(), context.getActionTask());
+        } finally {
+            if (interrupted) {
+                Thread.currentThread().interrupt();
+            }
         }
+        contextRestorer.restore(context.getKey(), context.getActionTask());
     }
 
     /**
diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinator.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinator.java
index 993188223..faef8f850 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinator.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinator.java
@@ -191,7 +191,9 @@ public final class ParallelExecutionCoordinator implements 
AutoCloseable {
             }
 
             // Holding the lock from here on: every failure is funneled to the 
mailbox (an uncaught
-            // throwable would only kill this pool thread silently) and the 
lock is always returned.
+            // throwable would only kill this pool thread silently), and the 
lock is returned
+            // unless a shutdown interrupt aborted the work's async re-acquire.
+            boolean ownsLock = true;
             try {
                 ParallelExecutionTaskQueue.Node node = 
schedule.pollPendingNode();
                 ParallelExecutionTask work = workFactory.get();
@@ -201,6 +203,12 @@ public final class ParallelExecutionCoordinator implements 
AutoCloseable {
                 // execute() never throws: it captures pull/prepare/run 
failures and rethrows at
                 // commit.
                 work.execute();
+                if (workerExecutor.isShutdown() && 
!parallelExecutionLock.isHeldByCurrentThread()) {
+                    // A shutdown interrupt aborted the work's async 
re-acquire, so this worker no
+                    // longer owns the lock: abandon the task without 
completing or releasing it.
+                    ownsLock = false;
+                    return;
+                }
                 schedule.markDone(node);
                 mailboxDispatcher.execute(() -> completeOnMailbox(node), 
"complete action task");
             } catch (Throwable t) {
@@ -210,7 +218,9 @@ public final class ParallelExecutionCoordinator implements 
AutoCloseable {
                         },
                         "throw action task scheduling failure");
             } finally {
-                parallelExecutionLock.releaseToMain();
+                if (ownsLock) {
+                    parallelExecutionLock.releaseToMain();
+                }
             }
         } finally {
             submittedPermits.decrementAndGet();
diff --git 
a/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionLock.java
 
b/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionLock.java
index f5009586c..b82e8d5ee 100644
--- 
a/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionLock.java
+++ 
b/runtime/src/main/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionLock.java
@@ -176,6 +176,11 @@ public final class ParallelExecutionLock {
         }
     }
 
+    /** Whether the calling thread currently owns the lock. */
+    boolean isHeldByCurrentThread() {
+        return ownerThread == Thread.currentThread();
+    }
+
     /** Asserts that the calling thread currently owns the lock. */
     public void checkReentrant() {
         if (ownerThread != Thread.currentThread()) {
diff --git 
a/runtime/src/test/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutorTest.java
 
b/runtime/src/test/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutorTest.java
index 0872d6ba8..4a196d708 100644
--- 
a/runtime/src/test/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutorTest.java
+++ 
b/runtime/src/test/java/org/apache/flink/agents/runtime/async/ContinuationActionExecutorTest.java
@@ -25,11 +25,13 @@ import org.junit.jupiter.api.Test;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -102,6 +104,75 @@ class ContinuationActionExecutorTest {
         }
     }
 
+    @Test
+    void executeAsyncKeepsResultWhenCallLeavesInterruptFlagSet() throws 
Throwable {
+        assertThat(
+                        runInterruptingCall(
+                                () -> {
+                                    // Code that catches an 
InterruptedException and re-interrupts.
+                                    Thread.currentThread().interrupt();
+                                    return "result";
+                                }))
+                .isEqualTo("result");
+    }
+
+    @Test
+    void executeAsyncKeepsFailureWhenCallLeavesInterruptFlagSet() {
+        IllegalStateException failure = new IllegalStateException("call 
failed");
+        assertThatThrownBy(
+                        () ->
+                                runInterruptingCall(
+                                        () -> {
+                                            Thread.currentThread().interrupt();
+                                            throw failure;
+                                        }))
+                .isSameAs(failure);
+    }
+
+    /**
+     * Runs {@code call} through {@code executeAsync} on a worker that owns 
the lock, and checks
+     * that the worker gets the lock and its context back with the interrupt 
flag still set.
+     */
+    private static String runInterruptingCall(Supplier<String> call) throws 
Throwable {
+        ParallelExecutionLock lock = new ParallelExecutionLock();
+        AtomicBoolean restored = new AtomicBoolean();
+        AtomicBoolean interruptedAfterCall = new AtomicBoolean();
+        ContinuationActionExecutor executor =
+                new ContinuationActionExecutor(
+                        1,
+                        () -> {},
+                        lock,
+                        (key, action) -> {
+                            lock.checkReentrant();
+                            restored.set(true);
+                        });
+        ExecutorService workerExecutor = Executors.newSingleThreadExecutor();
+        try {
+            Future<String> workerResult =
+                    workerExecutor.submit(
+                            () -> {
+                                lock.acquireByWorker(0, 0);
+                                try {
+                                    return executor.executeAsync(new 
ContinuationContext(), call);
+                                } finally {
+                                    
interruptedAfterCall.set(Thread.interrupted());
+                                    lock.release();
+                                }
+                            });
+            try {
+                return workerResult.get(5, TimeUnit.SECONDS);
+            } catch (ExecutionException e) {
+                throw e.getCause();
+            } finally {
+                assertThat(restored).isTrue();
+                assertThat(interruptedAfterCall).isTrue();
+            }
+        } finally {
+            workerExecutor.shutdownNow();
+            executor.close();
+        }
+    }
+
     @Test
     void executeAllAsyncReleasesAndReacquiresLockAndRestoresContext() throws 
Exception {
         ParallelExecutionLock lock = new ParallelExecutionLock();
diff --git 
a/runtime/src/test/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinatorTest.java
 
b/runtime/src/test/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinatorTest.java
index 271981df0..76bab05e1 100644
--- 
a/runtime/src/test/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinatorTest.java
+++ 
b/runtime/src/test/java/org/apache/flink/agents/runtime/operator/parallel/ParallelExecutionCoordinatorTest.java
@@ -34,6 +34,7 @@ import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Supplier;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -375,6 +376,57 @@ class ParallelExecutionCoordinatorTest {
         }
     }
 
+    @Test
+    @Timeout(15)
+    void shutdownDuringAsyncReacquireAbandonsTaskWithoutLockErrors() throws 
Exception {
+        ParallelExecutionLock lock = new ParallelExecutionLock();
+        BlockingQueue<ThrowingRunnable<? extends Exception>> mails = new 
LinkedBlockingQueue<>();
+        List<String> events = Collections.synchronizedList(new ArrayList<>());
+        AtomicReference<Throwable> uncaught = new AtomicReference<>();
+        CountDownLatch released = new CountDownLatch(1);
+        CountDownLatch mainHoldsLock = new CountDownLatch(1);
+        Supplier<ParallelExecutionTask> works =
+                () ->
+                        FakeWork.asyncAbandonedOnInterrupt(
+                                "A",
+                                events,
+                                lock,
+                                () -> {
+                                    Thread.currentThread()
+                                            .setUncaughtExceptionHandler(
+                                                    (thread, error) ->
+                                                            
uncaught.compareAndSet(null, error));
+                                    released.countDown();
+                                    await(mainHoldsLock);
+                                });
+        ParallelExecutionCoordinator coordinator = newCoordinator(lock, mails, 
works);
+        try {
+            lock.acquireByMain();
+            coordinator.addTask(KEY);
+            lock.release();
+            assertThat(released.await(5, TimeUnit.SECONDS)).isTrue();
+
+            // Hold the lock on the main thread so the worker's re-acquire has 
to wait, then shut
+            // down, whose interrupt aborts that wait.
+            lock.acquireByMain();
+            try {
+                mainHoldsLock.countDown();
+                waitUntilQueuedWorkers(lock, 1);
+                coordinator.shutdown();
+                coordinator.close();
+            } finally {
+                lock.release();
+            }
+
+            
assertThat(events).contains("abandon-A").doesNotContain("commit-A");
+            assertThat(mails).isEmpty();
+            assertThat(uncaught.get()).isNull();
+        } finally {
+            mainHoldsLock.countDown();
+            coordinator.close();
+        }
+    }
+
     @Test
     @Timeout(15)
     void closeWaitsForInFlightWorkerBeforeReturning() throws Exception {
@@ -734,6 +786,17 @@ class ParallelExecutionCoordinatorTest {
         }
     }
 
+    private static void waitUntilQueuedWorkers(ParallelExecutionLock lock, int 
expected)
+            throws InterruptedException {
+        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
+        while (lock.getQueuedWorkerCountForTesting() != expected) {
+            assertThat(System.nanoTime())
+                    .as("timed out waiting for %d queued workers", expected)
+                    .isLessThan(deadline);
+            Thread.sleep(10L);
+        }
+    }
+
     private static void runOneMail(BlockingQueue<ThrowingRunnable<? extends 
Exception>> mails)
             throws Exception {
         ThrowingRunnable<? extends Exception> mail = mails.poll(5, 
TimeUnit.SECONDS);
@@ -773,6 +836,7 @@ class ParallelExecutionCoordinatorTest {
         @Nullable private final Runnable onExecute;
         @Nullable private final ParallelExecutionLock asyncLock;
         @Nullable private final RuntimeException commitFailure;
+        private final boolean abandonOnInterrupt;
         private volatile boolean done;
         private volatile Object assignedKey;
         private volatile long assignedRecordIndex = -1L;
@@ -783,16 +847,18 @@ class ParallelExecutionCoordinatorTest {
                 List<String> events,
                 @Nullable Runnable onExecute,
                 @Nullable ParallelExecutionLock asyncLock,
-                @Nullable RuntimeException commitFailure) {
+                @Nullable RuntimeException commitFailure,
+                boolean abandonOnInterrupt) {
             this.id = id;
             this.events = events;
             this.onExecute = onExecute;
             this.asyncLock = asyncLock;
             this.commitFailure = commitFailure;
+            this.abandonOnInterrupt = abandonOnInterrupt;
         }
 
         static FakeWork blocking(String id, List<String> events, Runnable 
onExecute) {
-            return new FakeWork(id, events, onExecute, null, null);
+            return new FakeWork(id, events, onExecute, null, null, false);
         }
 
         /**
@@ -802,7 +868,7 @@ class ParallelExecutionCoordinatorTest {
          */
         static FakeWork blockingAsync(
                 String id, List<String> events, ParallelExecutionLock lock, 
Runnable onExecute) {
-            return new FakeWork(id, events, onExecute, lock, null);
+            return new FakeWork(id, events, onExecute, lock, null, false);
         }
 
         static FakeWork blockingAsyncFailingCommit(
@@ -811,12 +877,21 @@ class ParallelExecutionCoordinatorTest {
                 ParallelExecutionLock lock,
                 Runnable onExecute,
                 RuntimeException commitFailure) {
-            return new FakeWork(id, events, onExecute, lock, commitFailure);
+            return new FakeWork(id, events, onExecute, lock, commitFailure, 
false);
+        }
+
+        /**
+         * Like {@link #blockingAsync}, but an interrupted re-acquire returns 
without the lock, as
+         * the operator's work does when it records the failure for commit.
+         */
+        static FakeWork asyncAbandonedOnInterrupt(
+                String id, List<String> events, ParallelExecutionLock lock, 
Runnable onExecute) {
+            return new FakeWork(id, events, onExecute, lock, null, true);
         }
 
         static FakeWork failingCommit(
                 String id, List<String> events, RuntimeException 
commitFailure) {
-            return new FakeWork(id, events, null, null, commitFailure);
+            return new FakeWork(id, events, null, null, commitFailure, false);
         }
 
         @Override
@@ -847,6 +922,10 @@ class ParallelExecutionCoordinatorTest {
                         asyncLock.acquireByWorker(assignedRecordIndex, 
assignedTaskIndex);
                     } catch (InterruptedException e) {
                         Thread.currentThread().interrupt();
+                        if (abandonOnInterrupt) {
+                            events.add("abandon-" + id);
+                            return;
+                        }
                         throw new AssertionError("Interrupted while 
re-acquiring the lock", e);
                     }
                 }

Reply via email to