This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push: new 02132ee97 Add method ConcurrentInitializer#isInitialized() (#1120) 02132ee97 is described below commit 02132ee97bf0d57c85aa45bb64d6eb8dfbe0cf5e Author: Benjamin Confino <benja...@uk.ibm.com> AuthorDate: Sat Oct 14 13:00:56 2023 +0100 Add method ConcurrentInitializer#isInitialized() (#1120) * use NO_INIT to better handle initialize returning null * add isInitialized method to all ConcurrentInitializer classes and test * Update AtomicInitializer.java * Update AtomicSafeInitializer.java --------- Co-authored-by: Gary Gregory <garydgreg...@users.noreply.github.com> --- .../concurrent/AbstractConcurrentInitializer.java | 9 ++++ .../lang3/concurrent/AtomicInitializer.java | 19 +++++-- .../lang3/concurrent/AtomicSafeInitializer.java | 16 +++++- .../lang3/concurrent/BackgroundInitializer.java | 23 +++++++++ .../lang3/concurrent/ConstantInitializer.java | 11 ++++ .../concurrent/MultiBackgroundInitializer.java | 16 ++++++ .../AbstractConcurrentInitializerTest.java | 16 ++++++ .../lang3/concurrent/AtomicInitializerTest.java | 25 ++++++++++ .../concurrent/AtomicSafeInitializerTest.java | 21 ++++++++ .../concurrent/BackgroundInitializerTest.java | 30 +++++++++++ .../lang3/concurrent/ConstantInitializerTest.java | 10 ++++ .../concurrent/MultiBackgroundInitializerTest.java | 58 ++++++++++++++++++++++ 12 files changed, 249 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java index 8e064a73f..7786c8057 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java @@ -36,4 +36,13 @@ public abstract class AbstractConcurrentInitializer<T, E extends Exception> impl */ protected abstract T initialize() throws E; + /** + * Returns true if initialization has been completed. If initialization threw an exception this will return false, but it will return true if a subsequent + * call to initialize completes successfully. If the implementation of ConcurrentInitializer can initialize multiple objects, this will only return true if + * all objects have been initialized. + * + * @return true if all initialization is complete, otherwise false + */ + protected abstract boolean isInitialized(); + } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java index 4560fd424..9c20bd5ac 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java @@ -63,8 +63,10 @@ import java.util.concurrent.atomic.AtomicReference; * @param <T> the type of the object managed by this initializer class */ public abstract class AtomicInitializer<T> extends AbstractConcurrentInitializer<T, RuntimeException> { + + private static final Object NO_INIT = new Object(); /** Holds the reference to the managed object. */ - private final AtomicReference<T> reference = new AtomicReference<>(); + private final AtomicReference<T> reference = new AtomicReference<>((T) NO_INIT); /** * Returns the object managed by this initializer. The object is created if @@ -79,9 +81,9 @@ public abstract class AtomicInitializer<T> extends AbstractConcurrentInitializer public T get() throws ConcurrentException { T result = reference.get(); - if (result == null) { + if (result == (T) NO_INIT) { result = initialize(); - if (!reference.compareAndSet(null, result)) { + if (!reference.compareAndSet((T) NO_INIT, result)) { // another thread has initialized the reference result = reference.get(); } @@ -89,4 +91,15 @@ public abstract class AtomicInitializer<T> extends AbstractConcurrentInitializer return result; } + + /** + * Tests whether this instance is initialized. Once initialized, always returns true. + * + * @return whether this instance is initialized. Once initialized, always returns true. + * @since 3.14.0 + */ + @Override + public boolean isInitialized() { + return reference.get() != NO_INIT; + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java index 87a09ae44..e6093339d 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java @@ -53,12 +53,13 @@ import java.util.concurrent.atomic.AtomicReference; */ public abstract class AtomicSafeInitializer<T> extends AbstractConcurrentInitializer<T, RuntimeException> { + private static final Object NO_INIT = new Object(); /** A guard which ensures that initialize() is called only once. */ private final AtomicReference<AtomicSafeInitializer<T>> factory = new AtomicReference<>(); /** Holds the reference to the managed object. */ - private final AtomicReference<T> reference = new AtomicReference<>(); + private final AtomicReference<T> reference = new AtomicReference<>((T) NO_INIT); /** * Gets (and initialize, if not initialized yet) the required object @@ -71,7 +72,7 @@ public abstract class AtomicSafeInitializer<T> extends AbstractConcurrentInitial public final T get() throws ConcurrentException { T result; - while ((result = reference.get()) == null) { + while ((result = reference.get()) == (T) NO_INIT) { if (factory.compareAndSet(null, this)) { reference.set(initialize()); } @@ -79,4 +80,15 @@ public abstract class AtomicSafeInitializer<T> extends AbstractConcurrentInitial return result; } + + /** + * Tests whether this instance is initialized. Once initialized, always returns true. + * + * @return whether this instance is initialized. Once initialized, always returns true. + * @since 3.14.0 + */ + @Override + public boolean isInitialized() { + return reference.get() != NO_INIT; + } } 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 dba81d5a8..91ee1c015 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java @@ -17,6 +17,7 @@ package org.apache.commons.lang3.concurrent; import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -123,6 +124,28 @@ public abstract class BackgroundInitializer<T> extends AbstractConcurrentInitial return externalExecutor; } + /** + * Tests whether this instance is initialized. Once initialized, always returns true. + * If initialization failed then the failure will be cached and this will never return + * true. + * + * @return true if initialization completed successfully, otherwise false + * @since 3.14.0 + */ + @Override + public synchronized boolean isInitialized() { + if (future == null || ! future.isDone() ) { + return false; + } + + try { + future.get(); + return true; + } catch (CancellationException | ExecutionException | InterruptedException e) { + return false; + } + } + /** * Returns a flag whether this {@link BackgroundInitializer} has already * been started. diff --git a/src/main/java/org/apache/commons/lang3/concurrent/ConstantInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/ConstantInitializer.java index ea6c6a502..9f52aa009 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/ConstantInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/ConstantInitializer.java @@ -80,6 +80,17 @@ public class ConstantInitializer<T> implements ConcurrentInitializer<T> { return getObject(); } + /** + * As a {@link ConstantInitializer} is initialized on construction this will + * always return true. + * + * @return true. + * @since 3.14.0 + */ + public boolean isInitialized() { + return true; + } + /** * Returns a hash code for this object. This implementation returns the hash * code of the managed object. diff --git a/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java index 4f5abaad1..f294ef407 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java @@ -199,6 +199,22 @@ public class MultiBackgroundInitializer return new MultiBackgroundInitializerResults(inits, results, excepts); } + /** + * Tests whether this all child {@code BackgroundInitializer} objects are initialized. + * Once initialized, always returns true. + * + * @return whether all child {@code BackgroundInitializer} objects instance are initialized. Once initialized, always returns true. If there are no child {@code BackgroundInitializer} objects return false. + * @since 3.14.0 + */ + @Override + public boolean isInitialized() { + if (childInitializers.isEmpty()) { + return false; + } + + return childInitializers.values().stream().allMatch(BackgroundInitializer::isInitialized); + } + /** * A data class for storing the results of the background initialization * performed by {@link MultiBackgroundInitializer}. Objects of this inner diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java index d0b1db398..6f575ff54 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerTest.java @@ -17,7 +17,9 @@ package org.apache.commons.lang3.concurrent; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.concurrent.CountDownLatch; @@ -46,6 +48,20 @@ public abstract class AbstractConcurrentInitializerTest extends AbstractLangTest assertNotNull(createInitializer().get(), "No managed object"); } + /** + * Tests a simple invocation of the isInitialized() method. + */ + @Test + public void testisInitialized() throws Throwable { + final ConcurrentInitializer<Object> initializer = createInitializer(); + if (initializer instanceof AbstractConcurrentInitializer) { + AbstractConcurrentInitializer castedInitializer = (AbstractConcurrentInitializer) initializer; + assertFalse(castedInitializer.isInitialized(), "was initialized before get()"); + assertNotNull(castedInitializer.get(), "No managed object"); + assertTrue(castedInitializer.isInitialized(), "was not initialized after get()"); + } + } + /** * Tests whether sequential get() invocations always return the same * instance. diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java index 9b05a1ae3..cdf42bd1a 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerTest.java @@ -16,6 +16,12 @@ */ package org.apache.commons.lang3.concurrent; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + /** * Test class for {@code AtomicInitializer}. */ @@ -34,4 +40,23 @@ public class AtomicInitializerTest extends AbstractConcurrentInitializerTest { } }; } + + @Test + public void testGetThatReturnsNullFirstTime() throws ConcurrentException { + final AtomicInitializer<Object> initializer = new AtomicInitializer<Object>() { + final AtomicBoolean firstRun = new AtomicBoolean(true); + + @Override + protected Object initialize() { + if (firstRun.getAndSet(false)) { + return null; + } else { + return new Object(); + } + } + }; + + assertNull(initializer.get()); + assertNull(initializer.get()); + } } diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java index 6739ef758..8b0393472 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerTest.java @@ -17,7 +17,9 @@ package org.apache.commons.lang3.concurrent; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; @@ -58,6 +60,25 @@ public class AtomicSafeInitializerTest extends AbstractConcurrentInitializerTest assertEquals(1, initializer.initCounter.get(), "Wrong number of invocations"); } + @Test + public void testGetThatReturnsNullFirstTime() throws ConcurrentException { + final AtomicSafeInitializer<Object> initializer = new AtomicSafeInitializer<Object>() { + final AtomicBoolean firstRun = new AtomicBoolean(true); + + @Override + protected Object initialize() { + if (firstRun.getAndSet(false)) { + return null; + } else { + return new Object(); + } + } + }; + + assertNull(initializer.get()); + assertNull(initializer.get()); + } + /** * A concrete test implementation of {@code AtomicSafeInitializer} which also serves as a simple example. * <p> diff --git a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java index 895a2247c..90193f2af 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java @@ -267,6 +267,21 @@ public class BackgroundInitializerTest extends AbstractLangTest { assertTrue(init.isStarted(), "Not started"); } + /** + * Tests isInitialized() before and after the background task has finished. + */ + @Test + public void testIsInitialized() throws ConcurrentException { + final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + init.enableLatch(); + init.start(); + assertTrue(init.isStarted(), "Not started"); //Started and Initialized should return opposite values + assertFalse(init.isInitialized(), "Initalized before releasing latch"); + init.releaseLatch(); + init.get(); //to ensure the initialize thread has completed. + assertTrue(init.isInitialized(), "Not initalized after releasing latch"); + } + /** * A concrete implementation of BackgroundInitializer. It also overloads * some methods that simplify testing. @@ -282,6 +297,10 @@ public class BackgroundInitializerTest extends AbstractLangTest { /** The number of invocations of initialize(). */ volatile int initializeCalls; + /** A latch tests can use to control when initialize completes. */ + final CountDownLatch latch = new CountDownLatch(1); + boolean waitForLatch = false; + BackgroundInitializerTestImpl() { } @@ -289,6 +308,14 @@ public class BackgroundInitializerTest extends AbstractLangTest { super(exec); } + public void enableLatch() { + waitForLatch = true; + } + + public void releaseLatch() { + latch.countDown(); + } + /** * Records this invocation. Optionally throws an exception or sleeps a * while. @@ -303,6 +330,9 @@ public class BackgroundInitializerTest extends AbstractLangTest { if (shouldSleep) { ThreadUtils.sleep(Duration.ofMinutes(1)); } + if (waitForLatch) { + latch.await(); + } return Integer.valueOf(++initializeCalls); } } diff --git a/src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java index c296f056f..eddc2cbbf 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/ConstantInitializerTest.java @@ -130,4 +130,14 @@ public class ConstantInitializerTest extends AbstractLangTest { final String s = new ConstantInitializer<>(null).toString(); assertTrue(s.indexOf("object = null") > 0, "Object not found: " + s); } + + /** + * Tests a simple invocation of the isInitialized() method. + */ + @Test + public void testisInitialized() { + assertTrue(init.isInitialized(), "was not initialized before get()"); + assertEquals(VALUE, init.getObject(), "Wrong object"); + assertTrue(init.isInitialized(), "was not initialized after get()"); + } } diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java index c054c8396..f861e0302 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java @@ -21,9 +21,11 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -42,6 +44,9 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { /** The initializer to be tested. */ private MultiBackgroundInitializer initializer; + /** A short time to wait for background threads to run. */ + private static final long PERIOD_MILLIS = 50; + @BeforeEach public void setUp() { initializer = new MultiBackgroundInitializer(); @@ -366,6 +371,43 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { assertTrue(exec.isShutdown(), "Executor not shutdown"); } + @Test + public void testIsInitialized() + throws ConcurrentException, InterruptedException { + final ChildBackgroundInitializer childOne = new ChildBackgroundInitializer(); + final ChildBackgroundInitializer childTwo = new ChildBackgroundInitializer(); + + childOne.enableLatch(); + childTwo.enableLatch(); + + assertFalse(initializer.isInitialized(), "Initalized without having anything to initalize"); + + initializer.addInitializer("child one", childOne); + initializer.addInitializer("child two", childTwo); + initializer.start(); + + long startTime = System.currentTimeMillis(); + long waitTime = 3000; + long endTime = startTime + waitTime; + //wait for the children to start + while (! childOne.isStarted() || ! childTwo.isStarted()) { + if (System.currentTimeMillis() > endTime) { + fail("children never started"); + Thread.sleep(PERIOD_MILLIS); + } + } + + assertFalse(initializer.isInitialized(), "Initalized with two children running"); + + childOne.releaseLatch(); + childOne.get(); //ensure this child finishes initializing + assertFalse(initializer.isInitialized(), "Initalized with one child running"); + + childTwo.releaseLatch(); + childTwo.get(); //ensure this child finishes initializing + assertTrue(initializer.isInitialized(), "Not initalized with no children running"); + } + /** * A concrete implementation of {@code BackgroundInitializer} used for * defining background tasks for {@code MultiBackgroundInitializer}. @@ -381,6 +423,18 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { /** An exception to be thrown by initialize(). */ Exception ex; + /** A latch tests can use to control when initialize completes. */ + final CountDownLatch latch = new CountDownLatch(1); + boolean waitForLatch = false; + + public void enableLatch() { + waitForLatch = true; + } + + public void releaseLatch() { + latch.countDown(); + } + /** * Records this invocation. Optionally throws an exception. */ @@ -393,6 +447,10 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { throw ex; } + if (waitForLatch) { + latch.await(); + } + return Integer.valueOf(initializeCalls); } }