BackgroundInitializerTest.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.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.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.AbstractLangTest;
import org.apache.commons.lang3.ThreadUtils;
import org.junit.jupiter.api.Test;
class BackgroundInitializerTest extends AbstractLangTest {
/**
* A concrete implementation of BackgroundInitializer. It also overloads
* some methods that simplify testing.
*/
protected static class AbstractBackgroundInitializerTestImpl extends
BackgroundInitializer<CloseableCounter> {
/** An exception to be thrown by initialize(). */
Exception ex;
/** A flag whether the background task should sleep a while. */
boolean shouldSleep;
/** A latch tests can use to control when initialize completes. */
final CountDownLatch latch = new CountDownLatch(1);
boolean waitForLatch;
/** An object containing the state we are testing */
CloseableCounter counter = new CloseableCounter();
AbstractBackgroundInitializerTestImpl() {
}
AbstractBackgroundInitializerTestImpl(final ExecutorService exec) {
super(exec);
}
public void enableLatch() {
waitForLatch = true;
}
public CloseableCounter getCloseableCounter() {
return counter;
}
/**
* Records this invocation. Optionally throws an exception or sleeps a
* while.
*
* @throws Exception in case of an error.
*/
protected CloseableCounter initializeInternal() throws Exception {
if (ex != null) {
throw ex;
}
if (shouldSleep) {
ThreadUtils.sleep(Duration.ofMinutes(1));
}
if (waitForLatch) {
latch.await();
}
return counter.increment();
}
public void releaseLatch() {
latch.countDown();
}
}
protected static class CloseableCounter {
/** The number of invocations of initialize(). */
AtomicInteger initializeCalls = new AtomicInteger();
/** Has the close consumer successfully reached this object. */
AtomicBoolean closed = new AtomicBoolean();
public void close() {
closed.set(true);
}
public int getInitializeCalls() {
return initializeCalls.get();
}
public CloseableCounter increment() {
initializeCalls.incrementAndGet();
return this;
}
public boolean isClosed() {
return closed.get();
}
}
protected static class MethodBackgroundInitializerTestImpl extends AbstractBackgroundInitializerTestImpl {
MethodBackgroundInitializerTestImpl() {
}
MethodBackgroundInitializerTestImpl(final ExecutorService exec) {
super(exec);
}
@Override
protected CloseableCounter initialize() throws Exception {
return initializeInternal();
}
}
/**
* Helper method for checking whether the initialize() method was correctly
* called. start() must already have been invoked.
*
* @param init the initializer to test.
*/
private void checkInitialize(final AbstractBackgroundInitializerTestImpl init) throws ConcurrentException {
final Integer result = init.get().getInitializeCalls();
assertEquals(1, result.intValue(), "Wrong result");
assertEquals(1, init.getCloseableCounter().getInitializeCalls(), "Wrong number of invocations");
assertNotNull(init.getFuture(), "No future");
}
protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl() {
return new MethodBackgroundInitializerTestImpl();
}
protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl(final ExecutorService exec) {
return new MethodBackgroundInitializerTestImpl(exec);
}
@Test
void testBuilder() throws ConcurrentException {
// @formatter:off
final BackgroundInitializer<Object> backgroundInitializer = BackgroundInitializer.builder()
.setCloser(null)
.setExternalExecutor(null)
.setInitializer(null)
.get();
// @formatter:on
assertNull(backgroundInitializer.getExternalExecutor());
assertFalse(backgroundInitializer.isInitialized());
assertFalse(backgroundInitializer.isStarted());
assertThrows(IllegalStateException.class, backgroundInitializer::getFuture);
}
@Test
void testBuilderThenGetFailures() throws ConcurrentException {
// @formatter:off
final BackgroundInitializer<Object> backgroundInitializer = BackgroundInitializer.builder()
.setCloser(null)
.setExternalExecutor(null)
.setInitializer(() -> {
throw new IllegalStateException("test");
})
.get();
// @formatter:on
assertNull(backgroundInitializer.getExternalExecutor());
assertFalse(backgroundInitializer.isInitialized());
assertFalse(backgroundInitializer.isStarted());
assertThrows(IllegalStateException.class, backgroundInitializer::getFuture);
// start
backgroundInitializer.start();
assertEquals("test", assertThrows(IllegalStateException.class, backgroundInitializer::get).getMessage());
}
/**
* Tries to obtain the executor before start(). It should not have been
* initialized yet.
*/
@Test
void testGetActiveExecutorBeforeStart() {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
assertNull(init.getActiveExecutor(), "Got an executor");
}
/**
* Tests whether an external executor is correctly detected.
*/
@Test
void testGetActiveExecutorExternal() throws InterruptedException, ConcurrentException {
final ExecutorService exec = Executors.newSingleThreadExecutor();
try {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(exec);
init.start();
assertSame(exec, init.getActiveExecutor(), "Wrong executor");
checkInitialize(init);
} finally {
exec.shutdown();
exec.awaitTermination(1, TimeUnit.SECONDS);
}
}
/**
* Tests getActiveExecutor() for a temporary executor.
*/
@Test
void testGetActiveExecutorTemp() throws ConcurrentException {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
init.start();
assertNotNull(init.getActiveExecutor(), "No active executor");
checkInitialize(init);
}
/**
* Tests calling get() before start(). This should cause an exception.
*/
@Test
void testGetBeforeStart() {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
assertThrows(IllegalStateException.class, init::get);
}
/**
* Tests the get() method if background processing causes a checked
* exception.
*/
@Test
void testGetCheckedException() {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
final Exception ex = new Exception();
init.ex = ex;
init.start();
final ConcurrentException cex = assertThrows(ConcurrentException.class, init::get);
assertEquals(ex, cex.getCause(), "Exception not thrown");
}
/**
* Tests the get() method if waiting for the initialization is interrupted.
*
* @throws InterruptedException because we're making use of Java's concurrent API
*/
@Test
void testGetInterruptedException() throws InterruptedException {
final ExecutorService exec = Executors.newSingleThreadExecutor();
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(
exec);
final CountDownLatch latch1 = new CountDownLatch(1);
init.shouldSleep = true;
init.start();
final AtomicReference<InterruptedException> iex = new AtomicReference<>();
final Thread getThread = new Thread() {
@Override
public void run() {
try {
init.get();
} catch (final ConcurrentException cex) {
if (cex.getCause() instanceof InterruptedException) {
iex.set((InterruptedException) cex.getCause());
}
} finally {
assertTrue(isInterrupted(), "Thread not interrupted");
latch1.countDown();
}
}
};
getThread.start();
getThread.interrupt();
latch1.await();
exec.shutdownNow();
exec.awaitTermination(1, TimeUnit.SECONDS);
assertNotNull(iex.get(), "No interrupted exception");
}
/**
* Tests the get() method if background processing causes a runtime
* exception.
*/
@Test
void testGetRuntimeException() {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
final RuntimeException rex = new RuntimeException();
init.ex = rex;
init.start();
final Exception ex = assertThrows(Exception.class, init::get);
assertEquals(rex, ex, "Runtime exception not thrown");
}
/**
* Tests whether initialize() is invoked.
*/
@Test
void testInitialize() throws ConcurrentException {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
init.start();
checkInitialize(init);
}
/**
* Tests the execution of the background task if a temporary executor has to
* be created.
*/
@Test
void testInitializeTempExecutor() throws ConcurrentException {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
assertTrue(init.start(), "Wrong result of start()");
checkInitialize(init);
assertTrue(init.getActiveExecutor().isShutdown(), "Executor not shutdown");
}
/**
* Tests isInitialized() before and after the background task has finished.
*/
@Test
void testIsInitialized() throws ConcurrentException {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
init.enableLatch();
init.start();
assertTrue(init.isStarted(), "Not started"); //Started and Initialized should return opposite values
assertFalse(init.isInitialized(), "Initialized before releasing latch");
init.releaseLatch();
init.get(); //to ensure the initialize thread has completed.
assertTrue(init.isInitialized(), "Not initialized after releasing latch");
}
/**
* Tests isStarted() after the background task has finished.
*/
@Test
void testIsStartedAfterGet() throws ConcurrentException {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
init.start();
checkInitialize(init);
assertTrue(init.isStarted(), "Not started");
}
/**
* Tests isStarted() before start() was called.
*/
@Test
void testIsStartedFalse() {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
assertFalse(init.isStarted(), "Already started");
}
/**
* Tests isStarted() after start().
*/
@Test
void testIsStartedTrue() {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
init.start();
assertTrue(init.isStarted(), "Not started");
}
/**
* Tests whether an external executor can be set using the
* setExternalExecutor() method.
*/
@Test
void testSetExternalExecutor() throws ConcurrentException {
final ExecutorService exec = Executors.newCachedThreadPool();
try {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
init.setExternalExecutor(exec);
assertEquals(exec, init.getExternalExecutor(), "Wrong executor service");
assertTrue(init.start(), "Wrong result of start()");
assertSame(exec, init.getActiveExecutor(), "Wrong active executor");
checkInitialize(init);
assertFalse(exec.isShutdown(), "Executor was shutdown");
} finally {
exec.shutdown();
}
}
/**
* Tests that setting an executor after start() causes an exception.
*
* @throws org.apache.commons.lang3.concurrent.ConcurrentException because the test implementation may throw it
*/
@Test
void testSetExternalExecutorAfterStart() throws ConcurrentException, InterruptedException {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
init.start();
final ExecutorService exec = Executors.newSingleThreadExecutor();
try {
assertThrows(IllegalStateException.class, () -> init.setExternalExecutor(exec));
init.get();
} finally {
exec.shutdown();
exec.awaitTermination(1, TimeUnit.SECONDS);
}
}
/**
* Tests invoking start() multiple times. Only the first invocation should
* have an effect.
*/
@Test
void testStartMultipleTimes() throws ConcurrentException {
final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
assertTrue(init.start(), "Wrong result for start()");
for (int i = 0; i < 10; i++) {
assertFalse(init.start(), "Could start again");
}
checkInitialize(init);
}
}