MultiBackgroundInitializerTest.java
/*
* 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
*
* https://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.commons.lang3.concurrent;
import static org.apache.commons.lang3.LangAssertions.assertNullPointerException;
import static org.junit.jupiter.api.Assertions.assertEquals;
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;
import org.apache.commons.lang3.AbstractLangTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test class for {@link MultiBackgroundInitializer}.
*/
class MultiBackgroundInitializerTest extends AbstractLangTest {
/**
* A mostly complete implementation of {@code BackgroundInitializer} used for
* defining background tasks for {@code MultiBackgroundInitializer}.
*
* Subclasses will contain the initializer, either as an method implementation
* or by using a supplier.
*/
protected static class AbstractChildBackgroundInitializer extends BackgroundInitializer<CloseableCounter> {
/** Stores the current executor service. */
volatile ExecutorService currentExecutor;
/** An object containing the state we are testing */
CloseableCounter counter = new CloseableCounter();
/** A counter for the invocations of initialize(). */
volatile int initializeCalls;
/** 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;
public void enableLatch() {
waitForLatch = true;
}
public CloseableCounter getCloseableCounter() {
return counter;
}
/**
* Records this invocation. Optionally throws an exception.
*/
protected CloseableCounter initializeInternal() throws Exception {
initializeCalls++;
currentExecutor = getActiveExecutor();
if (waitForLatch) {
latch.await();
}
if (ex != null) {
throw ex;
}
return counter.increment();
}
public void releaseLatch() {
latch.countDown();
}
}
protected static class CloseableCounter {
// A convenience for testing that a CloseableCounter typed as Object has a specific initializeCalls value
public static CloseableCounter wrapInteger(final int i) {
return new CloseableCounter().setInitializeCalls(i);
}
/** The number of invocations of initialize(). */
volatile int initializeCalls;
/** Has the close consumer successfully reached this object. */
volatile boolean closed;
public void close() {
closed = true;
}
@Override
public boolean equals(final Object other) {
if (other instanceof CloseableCounter) {
return initializeCalls == ((CloseableCounter) other).getInitializeCalls();
}
return false;
}
public int getInitializeCalls() {
return initializeCalls;
}
@Override
public int hashCode() {
return initializeCalls;
}
public CloseableCounter increment() {
initializeCalls++;
return this;
}
public boolean isClosed() {
return closed;
}
public CloseableCounter setInitializeCalls(final int i) {
initializeCalls = i;
return this;
}
}
protected static class MethodChildBackgroundInitializer extends AbstractChildBackgroundInitializer {
@Override
protected CloseableCounter initialize() throws Exception {
return initializeInternal();
}
}
/** Constant for the names of the child initializers. */
private static final String CHILD_INIT = "childInitializer";
/** A short time to wait for background threads to run. */
protected static final long PERIOD_MILLIS = 50;
/** The initializer to be tested. */
protected MultiBackgroundInitializer initializer;
/**
* Tests whether a child initializer has been executed. Optionally the
* expected executor service can be checked, too.
*
* @param child the child initializer
* @param expExec the expected executor service (null if the executor should
* not be checked)
* @throws ConcurrentException if an error occurs
*/
private void checkChild(final BackgroundInitializer<?> child,
final ExecutorService expExec) throws ConcurrentException {
final AbstractChildBackgroundInitializer cinit = (AbstractChildBackgroundInitializer) child;
final Integer result = cinit.get().getInitializeCalls();
assertEquals(1, result.intValue(), "Wrong result");
assertEquals(1, cinit.initializeCalls, "Wrong number of executions");
if (expExec != null) {
assertEquals(expExec, cinit.currentExecutor, "Wrong executor service");
}
}
/**
* Helper method for testing the initialize() method. This method can
* operate with both an external and a temporary executor service.
*
* @return the result object produced by the initializer
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
private MultiBackgroundInitializer.MultiBackgroundInitializerResults checkInitialize()
throws ConcurrentException {
final int count = 5;
for (int i = 0; i < count; i++) {
initializer.addInitializer(CHILD_INIT + i,
createChildBackgroundInitializer());
}
initializer.start();
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
.get();
assertEquals(count, res.initializerNames().size(), "Wrong number of child initializers");
for (int i = 0; i < count; i++) {
final String key = CHILD_INIT + i;
assertTrue(res.initializerNames().contains(key), "Name not found: " + key);
assertEquals(CloseableCounter.wrapInteger(1), res.getResultObject(key), "Wrong result object");
assertFalse(res.isException(key), "Exception flag");
assertNull(res.getException(key), "Got an exception");
checkChild(res.getInitializer(key), initializer.getActiveExecutor());
}
return res;
}
/**
* An overrideable method to create concrete implementations of
* {@code BackgroundInitializer} used for defining background tasks
* for {@code MultiBackgroundInitializer}.
*/
protected AbstractChildBackgroundInitializer createChildBackgroundInitializer() {
return new MethodChildBackgroundInitializer();
}
@BeforeEach
public void setUp() {
initializer = new MultiBackgroundInitializer();
}
/**
* Tries to add another child initializer after the start() method has been
* called. This should not be allowed.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testAddInitializerAfterStart() throws ConcurrentException {
initializer.start();
assertThrows(IllegalStateException.class, () -> initializer.addInitializer(CHILD_INIT, createChildBackgroundInitializer()),
"Could add initializer after start()!");
initializer.get();
}
/**
* Tests addInitializer() if a null initializer is passed in. This should
* cause an exception.
*/
@Test
void testAddInitializerNullInit() {
assertNullPointerException(() -> initializer.addInitializer(CHILD_INIT, null));
}
/**
* Tests addInitializer() if a null name is passed in. This should cause an
* exception.
*/
@Test
void testAddInitializerNullName() {
assertNullPointerException(() -> initializer.addInitializer(null, createChildBackgroundInitializer()));
}
/**
* Tests the behavior of initialize() if a child initializer has a specific
* executor service. Then this service should not be overridden.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeChildWithExecutor() throws ConcurrentException, InterruptedException {
final String initExec = "childInitializerWithExecutor";
final ExecutorService exec = Executors.newSingleThreadExecutor();
try {
final AbstractChildBackgroundInitializer c1 = createChildBackgroundInitializer();
final AbstractChildBackgroundInitializer c2 = createChildBackgroundInitializer();
c2.setExternalExecutor(exec);
initializer.addInitializer(CHILD_INIT, c1);
initializer.addInitializer(initExec, c2);
initializer.start();
initializer.get();
checkChild(c1, initializer.getActiveExecutor());
checkChild(c2, exec);
} finally {
exec.shutdown();
exec.awaitTermination(1, TimeUnit.SECONDS);
}
}
/**
* Tests the behavior of the initializer if one of the child initializers
* throws a checked exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeEx() throws ConcurrentException {
final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
child.ex = new Exception();
initializer.addInitializer(CHILD_INIT, child);
initializer.start();
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
.get();
assertTrue(res.isException(CHILD_INIT), "No exception flag");
assertNull(res.getResultObject(CHILD_INIT), "Got a results object");
final ConcurrentException cex = res.getException(CHILD_INIT);
assertEquals(child.ex, cex.getCause(), "Wrong cause");
}
/**
* Tests background processing if an external executor service is provided.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeExternalExec() throws ConcurrentException, InterruptedException {
final ExecutorService exec = Executors.newCachedThreadPool();
try {
initializer = new MultiBackgroundInitializer(exec);
checkInitialize();
assertEquals(exec, initializer.getActiveExecutor(), "Wrong executor");
assertFalse(exec.isShutdown(), "Executor was shutdown");
} finally {
exec.shutdown();
exec.awaitTermination(1, TimeUnit.SECONDS);
}
}
/**
* Tests whether MultiBackgroundInitializers can be combined in a nested
* way.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeNested() throws ConcurrentException {
final String nameMulti = "multiChildInitializer";
initializer
.addInitializer(CHILD_INIT, createChildBackgroundInitializer());
final MultiBackgroundInitializer mi2 = new MultiBackgroundInitializer();
final int count = 3;
for (int i = 0; i < count; i++) {
mi2
.addInitializer(CHILD_INIT + i,
createChildBackgroundInitializer());
}
initializer.addInitializer(nameMulti, mi2);
initializer.start();
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
.get();
final ExecutorService exec = initializer.getActiveExecutor();
checkChild(res.getInitializer(CHILD_INIT), exec);
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res2 = (MultiBackgroundInitializer.MultiBackgroundInitializerResults) res
.getResultObject(nameMulti);
assertEquals(count, res2.initializerNames().size(), "Wrong number of initializers");
for (int i = 0; i < count; i++) {
checkChild(res2.getInitializer(CHILD_INIT + i), exec);
}
assertTrue(exec.isShutdown(), "Executor not shutdown");
}
/**
* Tests the background processing if there are no child initializers.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeNoChildren() throws ConcurrentException {
assertTrue(initializer.start(), "Wrong result of start()");
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
.get();
assertTrue(res.initializerNames().isEmpty(), "Got child initializers");
assertTrue(initializer.getActiveExecutor().isShutdown(), "Executor not shutdown");
}
/**
* Tests the isSuccessful() method of the result object if at least one
* child initializer has thrown an exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeResultsIsSuccessfulFalse()
throws ConcurrentException {
final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
child.ex = new Exception();
initializer.addInitializer(CHILD_INIT, child);
initializer.start();
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
.get();
assertFalse(res.isSuccessful(), "Wrong success flag");
}
/**
* Tests the isSuccessful() method of the result object if no child
* initializer has thrown an exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeResultsIsSuccessfulTrue()
throws ConcurrentException {
final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
initializer.addInitializer(CHILD_INIT, child);
initializer.start();
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
.get();
assertTrue(res.isSuccessful(), "Wrong success flag");
}
/**
* Tests the behavior of the initializer if one of the child initializers
* throws a runtime exception.
*/
@Test
void testInitializeRuntimeEx() {
final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
child.ex = new RuntimeException();
initializer.addInitializer(CHILD_INIT, child);
initializer.start();
final Exception ex = assertThrows(Exception.class, initializer::get);
assertEquals(child.ex, ex, "Wrong exception");
}
/**
* Tests background processing if a temporary executor is used.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testInitializeTempExec() throws ConcurrentException {
checkInitialize();
assertTrue(initializer.getActiveExecutor().isShutdown(), "Executor not shutdown");
}
@Test
void testIsInitialized()
throws ConcurrentException, InterruptedException {
final AbstractChildBackgroundInitializer childOne = createChildBackgroundInitializer();
final AbstractChildBackgroundInitializer childTwo = createChildBackgroundInitializer();
childOne.enableLatch();
childTwo.enableLatch();
assertFalse(initializer.isInitialized(), "Initialized without having anything to initialize");
initializer.addInitializer("child one", childOne);
initializer.addInitializer("child two", childTwo);
initializer.start();
final long startTime = System.currentTimeMillis();
final long waitTime = 3000;
final 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(), "Initialized with two children running");
childOne.releaseLatch();
childOne.get(); //ensure this child finishes initializing
assertFalse(initializer.isInitialized(), "Initialized with one child running");
childTwo.releaseLatch();
childTwo.get(); //ensure this child finishes initializing
assertTrue(initializer.isInitialized(), "Not initialized with no children running");
}
/**
* Tries to query the exception of an unknown child initializer from the
* results object. This should cause an exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testResultGetExceptionUnknown() throws ConcurrentException {
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
assertThrows(NoSuchElementException.class, () -> res.getException("unknown"));
}
/**
* Tries to query an unknown child initializer from the results object. This
* should cause an exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testResultGetInitializerUnknown() throws ConcurrentException {
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
assertThrows(NoSuchElementException.class, () -> res.getInitializer("unknown"));
}
/**
* Tries to query the results of an unknown child initializer from the
* results object. This should cause an exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testResultGetResultObjectUnknown() throws ConcurrentException {
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
assertThrows(NoSuchElementException.class, () -> res.getResultObject("unknown"));
}
/**
* Tests that the set with the names of the initializers cannot be modified.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testResultInitializerNamesModify() throws ConcurrentException {
checkInitialize();
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
.get();
final Iterator<String> it = res.initializerNames().iterator();
it.next();
assertThrows(UnsupportedOperationException.class, it::remove);
}
/**
* Tries to query the exception flag of an unknown child initializer from
* the results object. This should cause an exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
*/
@Test
void testResultIsExceptionUnknown() throws ConcurrentException {
final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
assertThrows(NoSuchElementException.class, () -> res.isException("unknown"));
}
}