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 0b41916e5d9a6a1a9445e4e1e683bac2c7b3a097
Author: rishabhdaim <[email protected]>
AuthorDate: Thu Jan 29 11:20:00 2026 +0530

    OAK-12077 : added joinUninterruptibly() in oak-commons
---
 .../internal/concurrent/UninterruptibleUtils.java  |  46 ++++++++
 .../concurrent/UninterruptibleUtilsTest.java       | 123 +++++++++++++++++++++
 2 files changed, 169 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..784480db5d 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,50 @@ 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}
+     * @param timeout the maximum time to wait; must be non-negative
+     * @param unit the time unit of the {@code timeout} argument; 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, final long 
timeout, final TimeUnit unit) {
+
+        Objects.requireNonNull(toJoin, "thread to join is null");
+        Objects.requireNonNull(unit, "timeunit is null");
+
+        if (timeout < 0L) {
+            throw new IllegalArgumentException("timeout must be >= 0");
+        }
+
+        boolean interrupted = false;
+
+        try {
+            long remainingNanos = unit.toNanos(timeout);
+            long end = System.nanoTime() + remainingNanos;
+
+            for (;;) {
+                try {
+                    TimeUnit.NANOSECONDS.timedJoin(toJoin, remainingNanos);
+                    return; // either timeout elapsed or thread finished
+                } catch (InterruptedException e) {
+                    interrupted = true;
+                    remainingNanos = end - System.nanoTime();
+                }
+            }
+        } 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..1a127cffa0 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,127 @@ public class UninterruptibleUtilsTest {
         Assert.assertTrue("Zero sleep should return quickly", elapsedMillis < 
50L);
     }
 
+    @Test
+    public void testNullThread() {
+        Assert.assertThrows(NullPointerException.class,
+                () -> UninterruptibleUtils.joinUninterruptibly(null, 1L, 
TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void testNullUnit() {
+        Assert.assertThrows(NullPointerException.class,
+                () -> 
UninterruptibleUtils.joinUninterruptibly(Thread.currentThread(), 1L, null));
+    }
+
+    @Test
+    public void testNegativeTimeout() {
+        Assert.assertThrows(IllegalArgumentException.class,
+                () -> 
UninterruptibleUtils.joinUninterruptibly(Thread.currentThread(), -1L, 
TimeUnit.SECONDS));
+    }
+
+    @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, 100L, 
TimeUnit.MILLISECONDS);
+        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 testTimesOutIfThreadDoesNotFinish() {
+        final Thread worker = new Thread(() -> {
+            // Run longer than the join timeout
+            try {
+                Thread.sleep(20L);
+            } catch (InterruptedException ignored) {
+            }
+        });
+
+        worker.start();
+
+        long timeoutMillis = 10L;
+        long start = System.nanoTime();
+        UninterruptibleUtils.joinUninterruptibly(worker, timeoutMillis, 
TimeUnit.MILLISECONDS);
+        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 >= timeoutMillis && 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, 50L, 
TimeUnit.MILLISECONDS);
+            // 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, 1L, 
TimeUnit.SECONDS);
+            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

Reply via email to