From bb94ececc049230be1866f9f720b9006615bbac6 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 27 Jan 2026 14:11:24 +0000 Subject: [PATCH 1/4] fix lang-1817 restore interruption status of the thread --- .../lang3/concurrent/UncheckedFutureImpl.java | 2 + .../lang3/concurrent/UncheckedFutureTest.java | 52 ++++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/concurrent/UncheckedFutureImpl.java b/src/main/java/org/apache/commons/lang3/concurrent/UncheckedFutureImpl.java index 26c42fc34bf..6b92e8e7123 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/UncheckedFutureImpl.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/UncheckedFutureImpl.java @@ -42,6 +42,7 @@ public V get() { try { return super.get(); } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); throw new UncheckedInterruptedException(e); } catch (final ExecutionException e) { throw new UncheckedExecutionException(e); @@ -53,6 +54,7 @@ public V get(final long timeout, final TimeUnit unit) { try { return super.get(timeout, unit); } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); throw new UncheckedInterruptedException(e); } catch (final ExecutionException e) { throw new UncheckedExecutionException(e); diff --git a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java index ab670480468..a8f41f4cd97 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java @@ -17,21 +17,19 @@ package org.apache.commons.lang3.concurrent; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - import java.util.Arrays; import java.util.List; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; +import java.util.concurrent.*; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.commons.lang3.AbstractLangTest; import org.apache.commons.lang3.exception.UncheckedInterruptedException; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + /** * Tests {@link UncheckedFuture}. */ @@ -121,4 +119,44 @@ void testOnFuture() { assertEquals("Z", UncheckedFuture.on(new TestFuture<>("Z")).get()); } + @RepeatedTest(10) + void interruptFlagIsPreservedOnGet() throws Exception { + assertInterruptPreserved(future -> future.get()); + } + + @RepeatedTest(10) + void interruptFlagIsPreservedOnGetWithTimeout() throws Exception { + assertInterruptPreserved(future -> future.get(2, TimeUnit.SECONDS)); + } + + private static void assertInterruptPreserved( + Consumer> futureCall) throws Exception { + + ExecutorService executor = Executors.newFixedThreadPool(2); + try { + CountDownLatch future2IsAboutToWait = new CountDownLatch(1); + Future future1 = executor.submit(() -> { + Thread.sleep(10_000); + return 42; + }); + Future future2 = executor.submit(() -> { + future2IsAboutToWait.countDown(); + try { + futureCall.accept(UncheckedFuture.on(future1)); + return 1; + } catch (RuntimeException e) { + assertTrue(Thread.currentThread().isInterrupted()); + return 2; + } + }); + + assertTrue(future2IsAboutToWait.await(2, TimeUnit.SECONDS)); + executor.shutdownNow(); + assertEquals(2, future2.get(2, TimeUnit.SECONDS)); + } finally { + executor.shutdownNow(); + executor.awaitTermination(10, TimeUnit.SECONDS); + } + } + } From 77f0a010d08b15592314950d636a0c94fda39d6c Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 6 Feb 2026 15:13:18 +0000 Subject: [PATCH 2/4] deterministic test for thread interruption status --- .../lang3/concurrent/UncheckedFutureTest.java | 91 ++++++++++++------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java index a8f41f4cd97..504cae15b82 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java @@ -19,13 +19,18 @@ import java.util.Arrays; import java.util.List; -import java.util.concurrent.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.commons.lang3.AbstractLangTest; import org.apache.commons.lang3.exception.UncheckedInterruptedException; -import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -119,44 +124,62 @@ void testOnFuture() { assertEquals("Z", UncheckedFuture.on(new TestFuture<>("Z")).get()); } - @RepeatedTest(10) + + @Test void interruptFlagIsPreservedOnGet() throws Exception { - assertInterruptPreserved(future -> future.get()); + assertInterruptPreserved(UncheckedFuture::get); } - @RepeatedTest(10) + @Test void interruptFlagIsPreservedOnGetWithTimeout() throws Exception { - assertInterruptPreserved(future -> future.get(2, TimeUnit.SECONDS)); + assertInterruptPreserved(uf -> uf.get(1, TimeUnit.DAYS)); } - private static void assertInterruptPreserved( - Consumer> futureCall) throws Exception { - - ExecutorService executor = Executors.newFixedThreadPool(2); - try { - CountDownLatch future2IsAboutToWait = new CountDownLatch(1); - Future future1 = executor.submit(() -> { - Thread.sleep(10_000); - return 42; - }); - Future future2 = executor.submit(() -> { - future2IsAboutToWait.countDown(); - try { - futureCall.accept(UncheckedFuture.on(future1)); - return 1; - } catch (RuntimeException e) { - assertTrue(Thread.currentThread().isInterrupted()); - return 2; - } - }); - - assertTrue(future2IsAboutToWait.await(2, TimeUnit.SECONDS)); - executor.shutdownNow(); - assertEquals(2, future2.get(2, TimeUnit.SECONDS)); - } finally { - executor.shutdownNow(); - executor.awaitTermination(10, TimeUnit.SECONDS); - } + private static void assertInterruptPreserved(Consumer> call) throws Exception { + final CountDownLatch enteredGet = new CountDownLatch(1); + Future blockingFuture = new AbstractFutureProxy(ConcurrentUtils.constantFuture(42)) { + private final CountDownLatch neverRelease = new CountDownLatch(1); + + @Override + public Integer get() throws InterruptedException { + enteredGet.countDown(); + neverRelease.await(); + throw new AssertionError("We should not get here"); + } + + @Override + public Integer get(long timeout, TimeUnit unit) throws InterruptedException { + enteredGet.countDown(); + neverRelease.await(); + throw new AssertionError("We should not get here"); + } + + @Override + public boolean isDone() { + return false; + } + + }; + final UncheckedFuture uf = UncheckedFuture.on(blockingFuture); + final AtomicReference thrown = new AtomicReference<>(); + final AtomicBoolean interruptObserved = new AtomicBoolean(false); + Thread worker = new Thread(() -> { + try { + call.accept(uf); + thrown.set(new AssertionError("We should not get here")); + } catch (Throwable e) { + interruptObserved.set(Thread.currentThread().isInterrupted()); + thrown.set(e); + } + }, "unchecked-future-test-worker"); + worker.start(); + assertTrue(enteredGet.await(2, TimeUnit.SECONDS), "Worker did not enter Future.get() in time"); + worker.interrupt(); + worker.join(); + Throwable t = thrown.get(); + assertInstanceOf(UncheckedInterruptedException.class, t, "Unexpected exception: " + t); + assertInstanceOf(InterruptedException.class, t.getCause(), "Cause should be InterruptedException"); + assertTrue(interruptObserved.get(), "Interrupt flag was not restored by the wrapper"); } } From ca1fbe5356041d4abc2b7b3a5f5de043addc21e8 Mon Sep 17 00:00:00 2001 From: Ivan Date: Fri, 6 Feb 2026 17:35:29 +0000 Subject: [PATCH 3/4] more consistent interrupt state restoration --- .../commons/lang3/concurrent/BackgroundInitializer.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java index 2be935ad014..4d46f7ad4eb 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java @@ -345,7 +345,11 @@ public synchronized boolean isInitialized() { try { future.get(); return true; - } catch (CancellationException | ExecutionException | InterruptedException e) { + } catch (CancellationException | ExecutionException e) { + return false; + } catch (InterruptedException e) { + // reset interrupted state + Thread.currentThread().interrupt(); return false; } } From f584cf7f183fd9dcdd89366bd6ebfdc364c92dcd Mon Sep 17 00:00:00 2001 From: Ivan Date: Sat, 7 Feb 2026 12:06:10 +0000 Subject: [PATCH 4/4] fix Checkstyle --- .../lang3/concurrent/UncheckedFutureTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java index 504cae15b82..db8f59fda2c 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/UncheckedFutureTest.java @@ -17,6 +17,11 @@ package org.apache.commons.lang3.concurrent; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.util.Arrays; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -33,8 +38,6 @@ import org.apache.commons.lang3.exception.UncheckedInterruptedException; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - /** * Tests {@link UncheckedFuture}. */ @@ -137,7 +140,7 @@ void interruptFlagIsPreservedOnGetWithTimeout() throws Exception { private static void assertInterruptPreserved(Consumer> call) throws Exception { final CountDownLatch enteredGet = new CountDownLatch(1); - Future blockingFuture = new AbstractFutureProxy(ConcurrentUtils.constantFuture(42)) { + final Future blockingFuture = new AbstractFutureProxy(ConcurrentUtils.constantFuture(42)) { private final CountDownLatch neverRelease = new CountDownLatch(1); @Override @@ -163,7 +166,7 @@ public boolean isDone() { final UncheckedFuture uf = UncheckedFuture.on(blockingFuture); final AtomicReference thrown = new AtomicReference<>(); final AtomicBoolean interruptObserved = new AtomicBoolean(false); - Thread worker = new Thread(() -> { + final Thread worker = new Thread(() -> { try { call.accept(uf); thrown.set(new AssertionError("We should not get here")); @@ -176,7 +179,7 @@ public boolean isDone() { assertTrue(enteredGet.await(2, TimeUnit.SECONDS), "Worker did not enter Future.get() in time"); worker.interrupt(); worker.join(); - Throwable t = thrown.get(); + final Throwable t = thrown.get(); assertInstanceOf(UncheckedInterruptedException.class, t, "Unexpected exception: " + t); assertInstanceOf(InterruptedException.class, t.getCause(), "Cause should be InterruptedException"); assertTrue(interruptObserved.get(), "Interrupt flag was not restored by the wrapper");