From 7fb3d6fa4dee76ddae285802d50fe5b3d710449c Mon Sep 17 00:00:00 2001 From: JaveysWuu Date: Mon, 10 Aug 2026 13:57:30 +0800 Subject: [PATCH] fix(task): clear runner interrupt status after execution --- src/main/java/io/threadforge/ThreadScope.java | 2 + .../threadforge/ChannelInterruptionTest.java | 118 ++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/main/java/io/threadforge/ThreadScope.java b/src/main/java/io/threadforge/ThreadScope.java index dafa082..07dcec3 100644 --- a/src/main/java/io/threadforge/ThreadScope.java +++ b/src/main/java/io/threadforge/ThreadScope.java @@ -1217,12 +1217,14 @@ public void run() { Thread runner = Thread.currentThread(); if (!task.beginExecution(runner)) { task.markExecutionFinished(runner); + Thread.interrupted(); return; } try { super.run(); } finally { task.markExecutionFinished(runner); + Thread.interrupted(); } } } diff --git a/src/test/java/io/threadforge/ChannelInterruptionTest.java b/src/test/java/io/threadforge/ChannelInterruptionTest.java index 5dc33c7..dc9808c 100644 --- a/src/test/java/io/threadforge/ChannelInterruptionTest.java +++ b/src/test/java/io/threadforge/ChannelInterruptionTest.java @@ -6,10 +6,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.AbstractExecutorService; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -86,6 +90,47 @@ public Integer call() { } } + @Test + void cancelledChannelWaitDoesNotLeakInterruptToNextTask() throws Exception { + InterruptRetainingExecutor executor = new InterruptRetainingExecutor(); + Channel cancelledChannel = Channel.bounded(1); + CountDownLatch cancelledStarted = new CountDownLatch(1); + Channel deadlineChannel = Channel.bounded(1); + CountDownLatch deadlineStarted = new CountDownLatch(1); + try { + Scheduler scheduler = Scheduler.from(executor); + try (ThreadScope first = ThreadScope.open().withScheduler(scheduler)) { + Task cancelled = first.submit(new Callable() { + @Override + public Integer call() { + cancelledStarted.countDown(); + return cancelledChannel.receive(); + } + }); + assertTrue(cancelledStarted.await(1L, TimeUnit.SECONDS)); + assertTrue(cancelled.cancel()); + } + cancelledChannel.close(); + + try (ThreadScope second = ThreadScope.open() + .withScheduler(scheduler) + .withDeadline(Duration.ofMillis(50))) { + Task deadlineTask = second.submit(new Callable() { + @Override + public Integer call() { + deadlineStarted.countDown(); + return deadlineChannel.receive(); + } + }); + assertTrue(deadlineStarted.await(1L, TimeUnit.SECONDS)); + assertThrows(ScopeTimeoutException.class, () -> second.await(deadlineTask)); + } + } finally { + deadlineChannel.close(); + executor.shutdownNow(); + } + } + @Test void closedChannelDrainsBufferBeforeIteratorEnds() { Channel channel = Channel.bounded(3); @@ -101,4 +146,77 @@ void closedChannelDrainsBufferBeforeIteratorEnds() { assertEquals(Arrays.asList(2), remaining); assertThrows(ChannelClosedException.class, channel::receive); } + + private static final class InterruptRetainingExecutor extends AbstractExecutorService { + private final LinkedBlockingQueue queue = new LinkedBlockingQueue(); + private final AtomicBoolean shutdown = new AtomicBoolean(); + private final AtomicBoolean terminated = new AtomicBoolean(); + private final AtomicReference worker = new AtomicReference(); + + private InterruptRetainingExecutor() { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + worker.set(Thread.currentThread()); + try { + while (!shutdown.get()) { + Runnable command = queue.poll(); + if (command != null) { + command.run(); + } else { + Thread.yield(); + } + } + } finally { + terminated.set(true); + } + } + }, "threadforge-test-interrupt-retaining"); + thread.setDaemon(true); + thread.start(); + } + + @Override + public void execute(Runnable command) { + if (shutdown.get()) { + throw new RejectedExecutionException("executor is shut down"); + } + queue.offer(command); + } + + @Override + public void shutdown() { + shutdown.set(true); + } + + @Override + public java.util.List shutdownNow() { + shutdown.set(true); + Thread thread = worker.get(); + if (thread != null) { + thread.interrupt(); + } + return new java.util.ArrayList(queue); + } + + @Override + public boolean isShutdown() { + return shutdown.get(); + } + + @Override + public boolean isTerminated() { + return terminated.get(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + long deadline = System.nanoTime() + unit.toNanos(timeout); + while (!terminated.get() && System.nanoTime() < deadline) { + Thread.yield(); + } + return terminated.get(); + } + + } }