This is an automated email from the ASF dual-hosted git repository. daim pushed a commit to branch OAK-12074 in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git
commit fe5c6d1d68a08c32fb3d2bde5e4ca20a7f5befa0 Author: rishabhdaim <[email protected]> AuthorDate: Tue Jan 27 11:34:45 2026 +0530 OAK-12074 : added awaitUninterruptibly() in oak-commons --- .../internal/concurrent/UninterruptibleUtils.java | 118 +++++++++++++++++ .../concurrent/UninterruptibleUtilsTest.java | 146 +++++++++++++++++++++ 2 files changed, 264 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 new file mode 100644 index 0000000000..fa58291620 --- /dev/null +++ b/oak-commons/src/main/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtils.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.commons.internal.concurrent; + +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Utility methods for waiting on synchronization primitives without + * propagating {@link InterruptedException} to callers. + */ +public class UninterruptibleUtils { + + private UninterruptibleUtils() { + // no instance for you + } + + /** + * Waits uninterruptibly until the given {@link CountDownLatch} reaches zero. + * <p> + * This method repeatedly invokes {@link CountDownLatch#await()} and + * ignores any {@link InterruptedException} that occurs while waiting, + * but remembers that an interruption happened. After the latch has + * reached zero (or the method otherwise returns), this method restores + * the thread's interrupted status if any interruptions were detected + * during the wait. + * + * @param latch the latch to wait on; must not be {@code null} + * @throws NullPointerException if {@code latch} is {@code null} + */ + public static void awaitUninterruptibly(CountDownLatch latch) { + + Objects.requireNonNull(latch, "latch is null"); + + boolean interrupted = false; + try { + for (;;) { + try { + latch.await(); + return; // completed normally + } catch (InterruptedException e) { + interrupted = true; // remember and retry + } + } + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); // restore flag + } + } + } + + /** + * Waits uninterruptibly until either the given {@link CountDownLatch} reaches + * zero or the specified waiting time elapses. + * <p> + * This method behaves like {@link CountDownLatch#await(long, TimeUnit)}, + * except that it does not throw {@link InterruptedException}. Instead, it + * continues waiting when interruptions occur, tracking the remaining time + * based on a fixed deadline, and restores the thread's interrupted status + * before returning if any interruptions were detected. + * + * @param latch the latch to wait on; 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} + * @return {@code true} if the latch reached zero before the timeout expired; + * {@code false} if the waiting time elapsed before the latch reached zero + * @throws NullPointerException if {@code latch} or {@code unit} is {@code null} + * @throws IllegalArgumentException if {@code timeout} is negative + */ + public static boolean awaitUninterruptibly(final CountDownLatch latch, final long timeout, final TimeUnit unit) { + + Objects.requireNonNull(latch, "latch is null"); + Objects.requireNonNull(unit, "unit 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 { + return latch.await(remainingNanos, TimeUnit.NANOSECONDS); + } catch (InterruptedException e) { + interrupted = true; + remainingNanos = end - System.nanoTime(); + if (remainingNanos <= 0L) { + // Time is up: whether we return true or false depends on latch state. + return latch.getCount() == 0L; + } + } + } + } 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 new file mode 100644 index 0000000000..cc478fb251 --- /dev/null +++ b/oak-commons/src/test/java/org/apache/jackrabbit/oak/commons/internal/concurrent/UninterruptibleUtilsTest.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.jackrabbit.oak.commons.internal.concurrent; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Unit cases for {@link UninterruptibleUtils} + */ +public class UninterruptibleUtilsTest { + + @Test + public void testNullLatch() { + Assert.assertThrows(NullPointerException.class, + () -> UninterruptibleUtils.awaitUninterruptibly(null)); + } + + @Test + public void testWaitsUntilLatchReachesZero() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + Thread t = new Thread(() -> UninterruptibleUtils.awaitUninterruptibly(latch)); + t.start(); + + // Ensure the thread is actually waiting + Thread.sleep(5); + Assert.assertTrue(t.isAlive()); + + latch.countDown(); + t.join(10); + + Assert.assertFalse(t.isAlive()); + } + + @Test + public void testSwallowInterruptsButRestoreFlag() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + Thread t = new Thread(() -> { + UninterruptibleUtils.awaitUninterruptibly(latch); + // After returning, interrupted flag should be set if we interrupted during wait + Assert.assertTrue(Thread.currentThread().isInterrupted()); + }); + + t.start(); + Thread.sleep(5); + + // Interrupt while it's waiting + t.interrupt(); + + Thread.sleep(5); + latch.countDown(); + t.join(10); + + Assert.assertFalse(t.isAlive()); + } + + @Test + public void testWithNullTimeoutAndLatch() { + CountDownLatch latch = new CountDownLatch(1); + + Assert.assertThrows(NullPointerException.class, + () -> UninterruptibleUtils.awaitUninterruptibly(null, 1, TimeUnit.MILLISECONDS)); + + Assert.assertThrows(NullPointerException.class, + () -> UninterruptibleUtils.awaitUninterruptibly(latch, 1, null)); + } + + @Test + public void testWithNegativeTimeout() { + CountDownLatch latch = new CountDownLatch(1); + + Assert.assertThrows(IllegalArgumentException.class, + () -> UninterruptibleUtils.awaitUninterruptibly(latch, -1, TimeUnit.MILLISECONDS)); + } + + @Test + public void testWithTimeoutReturnsTrueWhenLatchCountsDownInTime() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + Thread t = new Thread(() -> { + boolean result = UninterruptibleUtils.awaitUninterruptibly(latch, 1, TimeUnit.MILLISECONDS); + Assert.assertTrue(result); + }); + + t.start(); + Thread.sleep(10); + latch.countDown(); + t.join(20); + + Assert.assertFalse(t.isAlive()); + } + + @Test + public void testWithTimeoutReturnsFalseWhenTimeoutExpires() { + CountDownLatch latch = new CountDownLatch(1); + + long start = System.nanoTime(); + boolean result = UninterruptibleUtils.awaitUninterruptibly(latch, 100, TimeUnit.MILLISECONDS); + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + Assert.assertFalse(result); + Assert.assertTrue("Elapsed time should be close to timeout", elapsedMillis >= 90L); + } + + @Test + public void testWithTimeoutSwallowInterruptsButRestoreFlag() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + + Thread t = new Thread(() -> { + boolean result = UninterruptibleUtils.awaitUninterruptibly(latch, 500, TimeUnit.MILLISECONDS); + // Might be true or false depending on when we count down, but interrupt flag must be set. + Assert.assertTrue(Thread.currentThread().isInterrupted()); + }); + + t.start(); + Thread.sleep(5); + t.interrupt(); + Thread.sleep(5); + latch.countDown(); + t.join(20); + + Assert.assertFalse(t.isAlive()); + } + +} \ No newline at end of file
