This is an automated email from the ASF dual-hosted git repository. daim pushed a commit to branch OAK-12077 in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
commit 1ef74a11a892d7bebd6f3ba18a25f0d7bd34462f Author: rishabhdaim <[email protected]> AuthorDate: Thu Jan 29 11:20:00 2026 +0530 OAK-12077 : added joinUninterruptibly() in oak-commons --- .../internal/concurrent/UninterruptibleUtils.java | 35 +++++++ .../concurrent/UninterruptibleUtilsTest.java | 108 +++++++++++++++++++++ 2 files changed, 143 insertions(+) diff --git a/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtils.java b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtils.java index 6dcb24125b..5093ac0de3 100644 --- a/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtils.java +++ b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtils.java @@ -110,4 +110,39 @@ public class UninterruptibleUtils { } } } + + /** + * Invokes {@link TimeUnit#timedJoin(Thread, long)} uninterruptibly. + * <p> + * This method repeatedly calls {@link TimeUnit#timedJoin(Thread, long)} until the + * specified timeout has elapsed or the target thread terminates, ignoring + * {@link InterruptedException} but remembering that an interruption + * occurred. When the method finally returns, it restores the current + * thread's interrupted status if any interruptions were detected. + * + * @param toJoin the thread to wait for; must not be {@code null} + * @throws NullPointerException if {@code toJoin} or {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code timeout} is negative + */ + public static void joinUninterruptibly(final Thread toJoin) { + + Objects.requireNonNull(toJoin, "thread to join is null"); + + boolean interrupted = false; + + try { + for(;;) { + try { + toJoin.join(); + return; + } catch (InterruptedException var6) { + interrupted = true; + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + } } diff --git a/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtilsTest.java b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtilsTest.java index 1029a34a96..0e99f52084 100644 --- a/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtilsTest.java +++ b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtilsTest.java @@ -162,4 +162,112 @@ public class UninterruptibleUtilsTest { Assert.assertTrue("Zero sleep should return quickly", elapsedMillis < 50L); } + @Test + public void testNullThread() { + Assert.assertThrows(NullPointerException.class, + () -> UninterruptibleUtils.joinUninterruptibly(null)); + } + + @Test + public void testReturnsWhenThreadFinishesBeforeTimeout() throws Exception { + final long workMillis = 10L; + final Thread worker = new Thread(() -> { + try { + Thread.sleep(workMillis); + } catch (InterruptedException ignored) {} + }); + + worker.start(); + + long start = System.nanoTime(); + UninterruptibleUtils.joinUninterruptibly(worker); + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + Assert.assertFalse("Worker should be finished", worker.isAlive()); + Assert.assertTrue("Join should not take excessively long", + elapsedMillis >= workMillis && elapsedMillis < 100L); + } + + @Test + public void testJoinShouldWaitUntilThreadFinishes() { + final Thread worker = new Thread(() -> { + // Run longer than the join timeout + try { + Thread.sleep(20L); + } catch (InterruptedException ignored) { + } + }); + + worker.start(); + + long start = System.nanoTime(); + UninterruptibleUtils.joinUninterruptibly(worker); + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + // Worker may still be alive (likely), but we at least checked the timeout behavior + Assert.assertTrue("Join should respect timeout", elapsedMillis >= 20L ); + } + + @Test + public void testJoinUninterruptiblyIgnoresInterruptsButRestoresFlag() throws Exception { + final Thread worker = new Thread(() -> { + try { + Thread.sleep(20L); + } catch (InterruptedException ignored) { + } + }); + + worker.start(); + + Thread joiningThread = new Thread(() -> { + UninterruptibleUtils.joinUninterruptibly(worker); + // After returning, interrupted flag should be set if we interrupted during join + Assert.assertTrue("Interrupt flag should be restored", Thread.currentThread().isInterrupted()); + }); + + joiningThread.start(); + + // Let the joining thread enter join + Thread.sleep(50L); + + // Interrupt while it is joining + joiningThread.interrupt(); + + joiningThread.join(200L); + + Assert.assertFalse("Joining thread should have completed", joiningThread.isAlive()); + Assert.assertFalse("Worker should have completed", worker.isAlive()); + } + + @Test + public void testJoinUninterruptiblyMultipleInterruptsStillCompleteAndRestoreFlag() throws Exception { + final Thread worker = new Thread(() -> { + try { + Thread.sleep(30L); + } catch (InterruptedException ignored) { + } + }); + + worker.start(); + + Thread joiningThread = new Thread(() -> { + UninterruptibleUtils.joinUninterruptibly(worker); + Assert.assertTrue("Interrupt flag should be restored after multiple interrupts", + Thread.currentThread().isInterrupted()); + }); + + joiningThread.start(); + + // Interrupt the joining thread multiple times while it is waiting + for (int i = 0; i < 3; i++) { + Thread.sleep(50L); + joiningThread.interrupt(); + } + + joiningThread.join(2000L); + + Assert.assertFalse("Joining thread should have completed", joiningThread.isAlive()); + Assert.assertFalse("Worker should have completed", worker.isAlive()); + } + } \ No newline at end of file
