diff --git a/src/java/org/apache/commons/lang/concurrent/MultiBackgroundInitializer.java b/src/java/org/apache/commons/lang/concurrent/MultiBackgroundInitializer.java new file mode 100644 index 000000000..c5771cb98 --- /dev/null +++ b/src/java/org/apache/commons/lang/concurrent/MultiBackgroundInitializer.java @@ -0,0 +1,340 @@ +/* + * 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.commons.lang.concurrent; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ExecutorService; + +/** + *

+ * A specialized {@link BackgroundInitializer} implementation that can deal with + * multiple background initialization tasks. + *

+ *

+ * This class has a similar purpose as {@link BackgroundInitializer}. However, + * it is not limited to a single background initialization task. Rather it + * manages an arbitrary number of {@code BackgroundInitializer} objects, + * executes them, and waits until they are completely initialized. This is + * useful for applications that have to perform multiple initialization tasks + * that can run in parallel (i.e. that do not depend on each other). This class + * takes care about the management of an {@code ExecutorService} and shares it + * with the {@code BackgroundInitializer} objects it is responsible for; so the + * using application need not bother with these details. + *

+ *

+ * The typical usage scenario for {@code MultiBackgroundInitializer} is as + * follows: + *

+ *

+ *

+ * {@code MultiBackgroundInitializer} starts a special controller task that + * starts all {@code BackgroundInitializer} objects added to the instance. + * Before the an initializer is started it is checked whether this initializer + * already has an {@code ExecutorService} set. If this is the case, this {@code + * ExecutorService} is used for running the background task. Otherwise the + * current {@code ExecutorService} of this {@code MultiBackgroundInitializer} is + * shared with the initializer. + *

+ *

+ * The easiest way of using this class is to let it deal with the management of + * an {@code ExecutorService} itself: If no external {@code ExecutorService} is + * provided, the class creates a temporary {@code ExecutorService} (that is + * capable of executing all background tasks in parallel) and destroys it at the + * end of background processing. + *

+ *

+ * Alternatively an external {@code ExecutorService} can be provided - either at + * construction time or later by calling the + * {@link #setExternalExecutor(ExecutorService)} method. In this case all + * background tasks are scheduled at this external {@code ExecutorService}. + * Important note: When using an external {@code + * ExecutorService} be sure that the number of threads managed by the service is + * large enough. Otherwise a deadlock can happen! This is the case in the + * following scenario: {@code MultiBackgroundInitializer} starts a task that + * starts all registered {@code BackgroundInitializer} objects and waits for + * their completion. If for instance a single threaded {@code ExecutorService} + * is used, none of the background tasks can be executed, and the task created + * by {@code MultiBackgroundInitializer} waits forever. + *

+ * + * @version $Id$ + */ +public class MultiBackgroundInitializer + extends + BackgroundInitializer { + /** A map with the child initializers. */ + private final Map> childInitializers = new HashMap>(); + + /** + * Creates a new instance of {@code MultiBackgroundInitializer}. + */ + public MultiBackgroundInitializer() { + super(); + } + + /** + * Creates a new instance of {@code MultiBackgroundInitializer} and + * initializes it with the given external {@code ExecutorService}. + * + * @param exec the {@code ExecutorService} for executing the background + * tasks + */ + public MultiBackgroundInitializer(ExecutorService exec) { + super(exec); + } + + /** + * Adds a new {@code BackgroundInitializer} to this object. When this + * {@code MultiBackgroundInitializer} is started, the given initializer will + * be processed. This method must not be called after {@link #start()} has + * been invoked. + * + * @param name the name of the initializer (must not be null) + * @param init the {@code BackgroundInitializer} to add (must not be + * null) + * @throws IllegalArgumentException if a required parameter is missing + * @throws IllegalStateException if {@code start()} has already been called + */ + public void addInitializer(String name, BackgroundInitializer init) { + if (name == null) { + throw new IllegalArgumentException( + "Name of child initializer must not be null!"); + } + if (init == null) { + throw new IllegalArgumentException( + "Child initializer must not be null!"); + } + + synchronized (this) { + if (isStarted()) { + throw new IllegalStateException( + "addInitializer() must not be called after start()!"); + } + childInitializers.put(name, init); + } + } + + /** + * Returns the number of tasks needed for executing all child {@code + * BackgroundInitializer} objects in parallel. This implementation sums up + * the required tasks for all child initializers (which is necessary if one + * of the child initializers is itself a {@code MultiBackgroundInitializer} + * ). Then it adds 1 for the control task that waits for the completion of + * the children. + * + * @return the number of tasks required for background processing + */ + @Override + protected int getTaskCount() { + int result = 1; + + for (BackgroundInitializer bi : childInitializers.values()) { + result += bi.getTaskCount(); + } + + return result; + } + + /** + * Creates the results object. This implementation starts all child {@code + * BackgroundInitializer} objects. Then it collects their results and + * creates a {@code MultiBackgroundInitializerResults} object with this + * data. If a child initializer throws a checked exceptions, it is added to + * the results object. Unchecked exceptions are propagated. + * + * @return the results object + * @throws Exception if an error occurs + */ + @Override + protected MultiBackgroundInitializerResults initialize() throws Exception { + Map> inits; + synchronized (this) { + // create a snapshot to operate on + inits = new HashMap>( + childInitializers); + } + + // start the child initializers + ExecutorService exec = getActiveExecutor(); + for (BackgroundInitializer bi : inits.values()) { + if (bi.getExternalExecutor() == null) { + // share the executor service if necessary + bi.setExternalExecutor(exec); + } + bi.start(); + } + + // collect the results + Map results = new HashMap(); + Map excepts = new HashMap(); + for (Map.Entry> e : inits.entrySet()) { + try { + results.put(e.getKey(), e.getValue().get()); + } catch (ConcurrentException cex) { + excepts.put(e.getKey(), cex); + } + } + + return new MultiBackgroundInitializerResults(inits, results, excepts); + } + + /** + * A data class for storing the results of the background initialization + * performed by {@code MultiBackgroundInitializer}. Objects of this inner + * class are returned by {@link MultiBackgroundInitializer#initialize()}. + * They allow access to all result objects produced by the + * {@link BackgroundInitializer} objects managed by the owning instance. It + * is also possible to retrieve status information about single + * {@link BackgroundInitializer}s, i.e. whether they completed normally or + * caused an exception. + */ + public static class MultiBackgroundInitializerResults { + /** A map with the child initializers. */ + private final Map> initializers; + + /** A map with the result objects. */ + private final Map resultObjects; + + /** A map with the exceptions. */ + private final Map exceptions; + + /** + * Creates a new instance of {@code MultiBackgroundInitializerResults} + * and initializes it with maps for the {@code BackgroundInitializer} + * objects, their result objects and the exceptions thrown by them. + * + * @param inits the {@code BackgroundInitializer} objects + * @param results the result objects + * @param excepts the exceptions + */ + private MultiBackgroundInitializerResults( + Map> inits, + Map results, + Map excepts) { + initializers = inits; + resultObjects = results; + exceptions = excepts; + } + + /** + * Returns the {@code BackgroundInitializer} with the given name. If the + * name cannot be resolved, an exception is thrown. + * + * @param name the name of the {@code BackgroundInitializer} + * @return the {@code BackgroundInitializer} with this name + * @throws NoSuchElementException if the name cannot be resolved + */ + public BackgroundInitializer getInitializer(String name) { + return checkName(name); + } + + /** + * Returns the result object produced by the {@code + * BackgroundInitializer} with the given name. This is the object + * returned by the initializer's {@code initialize()} method. If this + * {@code BackgroundInitializer} caused an exception, null is + * returned. If the name cannot be resolved, an exception is thrown. + * + * @param name the name of the {@code BackgroundInitializer} + * @return the result object produced by this {@code + * BackgroundInitializer} + * @throws NoSuchElementException if the name cannot be resolved + */ + public Object getResultObject(String name) { + checkName(name); + return resultObjects.get(name); + } + + /** + * Returns a flag whether the {@code BackgroundInitializer} with the + * given name caused an exception. + * + * @param name the name of the {@code BackgroundInitializer} + * @return a flag whether this initializer caused an exception + * @throws NoSuchElementException if the name cannot be resolved + */ + public boolean isException(String name) { + checkName(name); + return exceptions.containsKey(name); + } + + /** + * Returns the {@code ConcurrentException} object that was thrown by the + * {@code BackgroundInitializer} with the given name. If this + * initializer did not throw an exception, the return value is + * null. If the name cannot be resolved, an exception is thrown. + * + * @param name the name of the {@code BackgroundInitializer} + * @return the exception thrown by this initializer + * @throws NoSuchElementException if the name cannot be resolved + */ + public ConcurrentException getException(String name) { + checkName(name); + return exceptions.get(name); + } + + /** + * Returns a set with the names of all {@code BackgroundInitializer} + * objects managed by the {@code MultiBackgroundInitializer}. + * + * @return an (unmodifiable) set with the names of the managed {@code + * BackgroundInitializer} objects + */ + public Set initializerNames() { + return Collections.unmodifiableSet(initializers.keySet()); + } + + /** + * Checks whether an initializer with the given name exists. If not, + * throws an exception. If it exists, the associated child initializer + * is returned. + * + * @param name the name to check + * @return the initializer with this name + * @throws NoSuchElementException if the name is unknown + */ + private BackgroundInitializer checkName(String name) { + BackgroundInitializer init = initializers.get(name); + if (init == null) { + throw new NoSuchElementException( + "No child initializer with name " + name); + } + + return init; + } + } +} diff --git a/src/test/org/apache/commons/lang/concurrent/MultiBackgroundInitializerTest.java b/src/test/org/apache/commons/lang/concurrent/MultiBackgroundInitializerTest.java new file mode 100644 index 000000000..753a7e724 --- /dev/null +++ b/src/test/org/apache/commons/lang/concurrent/MultiBackgroundInitializerTest.java @@ -0,0 +1,364 @@ +/* + * 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.commons.lang.concurrent; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import junit.framework.TestCase; + +/** + * Test class for {@link MultiBackgroundInitializer}. + * + * @version $Id$ + */ +public class MultiBackgroundInitializerTest extends TestCase { + /** Constant for the names of the child initializers. */ + private static final String CHILD_INIT = "childInitializer"; + + /** The initializer to be tested. */ + private MultiBackgroundInitializer initializer; + + @Override + protected void setUp() throws Exception { + super.setUp(); + initializer = new MultiBackgroundInitializer(); + } + + /** + * 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(BackgroundInitializer child, + ExecutorService expExec) throws ConcurrentException { + ChildBackgroundInitializer cinit = (ChildBackgroundInitializer) child; + Integer result = cinit.get(); + assertEquals("Wrong result", 1, result.intValue()); + assertEquals("Wrong number of executions", 1, cinit.initializeCalls); + if (expExec != null) { + assertEquals("Wrong executor service", expExec, + cinit.currentExecutor); + } + } + + /** + * Tests addInitializer() if a null name is passed in. This should cause an + * exception. + */ + public void testAddInitializerNullName() { + try { + initializer.addInitializer(null, new ChildBackgroundInitializer()); + fail("Null name not detected!"); + } catch (IllegalArgumentException iex) { + // ok + } + } + + /** + * Tests addInitializer() if a null initializer is passed in. This should + * cause an exception. + */ + public void testAddInitializerNullInit() { + try { + initializer.addInitializer(CHILD_INIT, null); + fail("Could add null initializer!"); + } catch (IllegalArgumentException iex) { + // ok + } + } + + /** + * Tests the background processing if there are no child initializers. + */ + public void testInitializeNoChildren() throws ConcurrentException { + assertTrue("Wrong result of start()", initializer.start()); + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer + .get(); + assertTrue("Got child initializers", res.initializerNames().isEmpty()); + assertTrue("Executor not shutdown", initializer.getActiveExecutor() + .isShutdown()); + } + + /** + * 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 + */ + private MultiBackgroundInitializer.MultiBackgroundInitializerResults checkInitialize() + throws ConcurrentException { + final int count = 5; + for (int i = 0; i < count; i++) { + initializer.addInitializer(CHILD_INIT + i, + new ChildBackgroundInitializer()); + } + initializer.start(); + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer + .get(); + assertEquals("Wrong number of child initializers", count, res + .initializerNames().size()); + for (int i = 0; i < count; i++) { + String key = CHILD_INIT + i; + assertTrue("Name not found: " + key, res.initializerNames() + .contains(key)); + assertEquals("Wrong result object", Integer.valueOf(1), res + .getResultObject(key)); + assertFalse("Exception flag", res.isException(key)); + assertNull("Got an exception", res.getException(key)); + checkChild(res.getInitializer(key), initializer.getActiveExecutor()); + } + return res; + } + + /** + * Tests background processing if a temporary executor is used. + */ + public void testInitializeTempExec() throws ConcurrentException { + checkInitialize(); + assertTrue("Executor not shutdown", initializer.getActiveExecutor() + .isShutdown()); + } + + /** + * Tests background processing if an external executor service is provided. + */ + public void testInitializeExternalExec() throws ConcurrentException { + ExecutorService exec = Executors.newCachedThreadPool(); + try { + initializer = new MultiBackgroundInitializer(exec); + checkInitialize(); + assertEquals("Wrong executor", exec, initializer + .getActiveExecutor()); + assertFalse("Executor was shutdown", exec.isShutdown()); + } finally { + exec.shutdown(); + } + } + + /** + * Tests the behavior of initialize() if a child initializer has a specific + * executor service. Then this service should not be overridden. + */ + public void testInitializeChildWithExecutor() throws ConcurrentException { + final String initExec = "childInitializerWithExecutor"; + ExecutorService exec = Executors.newSingleThreadExecutor(); + try { + ChildBackgroundInitializer c1 = new ChildBackgroundInitializer(); + ChildBackgroundInitializer c2 = new ChildBackgroundInitializer(); + 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(); + } + } + + /** + * Tries to add another child initializer after the start() method has been + * called. This should not be allowed. + */ + public void testAddInitializerAfterStart() throws ConcurrentException { + initializer.start(); + try { + initializer.addInitializer(CHILD_INIT, + new ChildBackgroundInitializer()); + fail("Could add initializer after start()!"); + } catch (IllegalStateException istex) { + initializer.get(); + } + } + + /** + * Tries to query an unknown child initializer from the results object. This + * should cause an exception. + */ + public void testResultGetInitializerUnknown() throws ConcurrentException { + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize(); + try { + res.getInitializer("unknown"); + fail("Could obtain unknown child initializer!"); + } catch (NoSuchElementException nex) { + // ok + } + } + + /** + * Tries to query the results of an unknown child initializer from the + * results object. This should cause an exception. + */ + public void testResultGetResultObjectUnknown() throws ConcurrentException { + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize(); + try { + res.getResultObject("unknown"); + fail("Could obtain results from unknown child initializer!"); + } catch (NoSuchElementException nex) { + // ok + } + } + + /** + * Tries to query the exception of an unknown child initializer from the + * results object. This should cause an exception. + */ + public void testResultGetExceptionUnknown() throws ConcurrentException { + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize(); + try { + res.getException("unknown"); + fail("Could obtain exception from unknown child initializer!"); + } catch (NoSuchElementException nex) { + // ok + } + } + + /** + * Tries to query the exception flag of an unknown child initializer from + * the results object. This should cause an exception. + */ + public void testResultIsExceptionUnknown() throws ConcurrentException { + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize(); + try { + res.isException("unknown"); + fail("Could obtain exception status from unknown child initializer!"); + } catch (NoSuchElementException nex) { + // ok + } + } + + /** + * Tests that the set with the names of the initializers cannot be modified. + */ + public void testResultInitializerNamesModify() throws ConcurrentException { + checkInitialize(); + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer + .get(); + Iterator it = res.initializerNames().iterator(); + it.next(); + try { + it.remove(); + fail("Could modify set with initializer names!"); + } catch (UnsupportedOperationException uex) { + // ok + } + } + + /** + * Tests the behavior of the initializer if one of the child initializers + * throws a runtime exception. + */ + public void testInitializeRuntimeEx() throws ConcurrentException { + ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + child.ex = new RuntimeException(); + initializer.addInitializer(CHILD_INIT, child); + initializer.start(); + try { + initializer.get(); + fail("Runtime exception not thrown!"); + } catch (Exception ex) { + assertEquals("Wrong exception", child.ex, ex); + } + } + + /** + * Tests the behavior of the initializer if one of the child initializers + * throws a checked exception. + */ + public void testInitializeEx() throws ConcurrentException { + ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + child.ex = new Exception(); + initializer.addInitializer(CHILD_INIT, child); + initializer.start(); + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer + .get(); + assertTrue("No exception flag", res.isException(CHILD_INIT)); + assertNull("Got a results object", res.getResultObject(CHILD_INIT)); + ConcurrentException cex = res.getException(CHILD_INIT); + assertEquals("Wrong cause", child.ex, cex.getCause()); + } + + /** + * Tests whether MultiBackgroundInitializers can be combined in a nested + * way. + */ + public void testInitializeNested() throws ConcurrentException { + final String nameMulti = "multiChildInitializer"; + initializer + .addInitializer(CHILD_INIT, new ChildBackgroundInitializer()); + MultiBackgroundInitializer mi2 = new MultiBackgroundInitializer(); + final int count = 3; + for (int i = 0; i < count; i++) { + mi2 + .addInitializer(CHILD_INIT + i, + new ChildBackgroundInitializer()); + } + initializer.addInitializer(nameMulti, mi2); + initializer.start(); + MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer + .get(); + ExecutorService exec = initializer.getActiveExecutor(); + checkChild(res.getInitializer(CHILD_INIT), exec); + MultiBackgroundInitializer.MultiBackgroundInitializerResults res2 = (MultiBackgroundInitializer.MultiBackgroundInitializerResults) res + .getResultObject(nameMulti); + assertEquals("Wrong number of initializers", count, res2 + .initializerNames().size()); + for (int i = 0; i < count; i++) { + checkChild(res2.getInitializer(CHILD_INIT + i), exec); + } + assertTrue("Executor not shutdown", exec.isShutdown()); + } + + /** + * A concrete implementation of {@code BackgroundInitializer} used for + * defining background tasks for {@code MultiBackgroundInitializer}. + */ + private static class ChildBackgroundInitializer extends + BackgroundInitializer { + /** Stores the current executor service. */ + volatile ExecutorService currentExecutor; + + /** A counter for the invocations of initialize(). */ + volatile int initializeCalls; + + /** An exception to be thrown by initialize(). */ + Exception ex; + + /** + * Records this invocation. Optionally throws an exception. + */ + @Override + protected Integer initialize() throws Exception { + currentExecutor = getActiveExecutor(); + initializeCalls++; + + if (ex != null) { + throw ex; + } + + return initializeCalls; + } + } +}