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 <garydgregory@users.noreply.github.com>
This commit is contained in:
Benjamin Confino 2023-10-14 13:00:56 +01:00 committed by GitHub
parent 77973e55a1
commit 02132ee97b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 249 additions and 5 deletions

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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());
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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()");
}
}

View File

@ -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);
}
}