You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by oh...@apache.org on 2009/10/31 20:48:53 UTC

svn commit: r831586 - in /commons/proper/lang/trunk/src: java/org/apache/commons/lang/concurrent/BackgroundInitializer.java test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java

Author: oheger
Date: Sat Oct 31 19:48:52 2009
New Revision: 831586

URL: http://svn.apache.org/viewvc?rev=831586&view=rev
Log:
[LANG-501] Added BackgroundInitializer class with JUnit tests.

Added:
    commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java   (with props)
    commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java   (with props)

Added: commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java
URL: http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java?rev=831586&view=auto
==============================================================================
--- commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java (added)
+++ commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java Sat Oct 31 19:48:52 2009
@@ -0,0 +1,331 @@
+/*
+ * 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.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * <p>
+ * A class that allows complex initialization operations in a background task.
+ * </p>
+ * <p>
+ * Applications often have to do some expensive initialization steps when they
+ * are started, e.g. constructing a connection to a database, reading a
+ * configuration file, etc. Doing these things in parallel can enhance
+ * performance as the CPU load can be improved. However, when access to the
+ * resources initialized in a background thread is actually required,
+ * synchronization has to be performed to ensure that their initialization is
+ * complete.
+ * </p>
+ * <p>
+ * This abstract base class provides support for this use case. A concrete
+ * subclass must implement the {@link #initialize()} method. Here an arbitrary
+ * initialization can be implemented, and a result object can be returned. With
+ * this method in place the basic usage of this class is as follows (where
+ * {@code MyBackgroundInitializer} is a concrete subclass):
+ *
+ * <pre>
+ * MyBackgroundInitializer initializer = new MyBackgroundInitializer();
+ * initializer.start();
+ * // Now do some other things. Initialization runs in a parallel thread
+ * ...
+ * // Wait for the end of initialization and access the result object
+ * Object result = initializer.get();
+ * </pre>
+ *
+ * </p>
+ * <p>
+ * After the construction of a {@code BackgroundInitializer} object its
+ * {@link #start()} method has to be called. This starts the background
+ * processing. The application can now continue to do other things. When it
+ * needs access to the object produced by the {@code BackgroundInitializer} it
+ * calls its {@link #get()} method. If initialization is already complete,
+ * {@link #get()} returns the result object immediately. Otherwise it blocks
+ * until the result object is fully constructed.
+ * </p>
+ * <p>
+ * {@code BackgroundInitializer} is a thin wrapper around a {@code Future}
+ * object and uses an {@code ExecutorService} for running the background
+ * initialization task. It is possible to pass in an {@code ExecutorService} at
+ * construction time or set one using {@code setExternalExecutor()} before
+ * {@code start()} was called. Then this object is used to spawn the background
+ * task. If no {@code ExecutorService} has been provided, {@code
+ * BackgroundInitializer} creates a temporary {@code ExecutorService} and
+ * destroys it when initialization is complete.
+ * </p>
+ * <p>
+ * The methods provided by {@code BackgroundInitializer} provide for minimal
+ * interaction with the wrapped {@code Future} object. It is also possible to
+ * obtain the {@code Future} object directly. Then the enhanced functionality
+ * offered by {@code Future} can be used, e.g. to check whether the background
+ * operation is complete or to cancel the operation.
+ * </p>
+ *
+ * @version $Id$
+ * @param <T> the type of the object managed by this initializer class
+ */
+public abstract class BackgroundInitializer<T> {
+    /** The external executor service for executing tasks. */
+    private ExecutorService externalExecutor;
+
+    /** A reference to the executor service that is actually used. */
+    private ExecutorService executor;
+
+    /** Stores the handle to the background task. */
+    private Future<T> future;
+
+    /**
+     * Creates a new instance of {@code BackgroundInitializer}. No external
+     * {@code ExecutorService} is used.
+     */
+    protected BackgroundInitializer() {
+        this(null);
+    }
+
+    /**
+     * Creates a new instance of {@code BackgroundInitializer} and initializes
+     * it with the given {@code ExecutorService}. If the {@code ExecutorService}
+     * is not null, the background task for initializing this object will be
+     * scheduled at this service. Otherwise a new temporary {@code
+     * ExecutorService} is created.
+     *
+     * @param exec an external {@code ExecutorService} to be used for task
+     * execution
+     */
+    protected BackgroundInitializer(ExecutorService exec) {
+        setExternalExecutor(exec);
+    }
+
+    /**
+     * Returns the external {@code ExecutorService} to be used by this class.
+     *
+     * @return the {@code ExecutorService}
+     */
+    public final synchronized ExecutorService getExternalExecutor() {
+        return externalExecutor;
+    }
+
+    /**
+     * Returns a flag whether this {@code BackgroundInitializer} has already
+     * been started.
+     *
+     * @return a flag whether the {@link #start()} method has already been
+     * called
+     */
+    public synchronized boolean isStarted() {
+        return future != null;
+    }
+
+    /**
+     * Sets an {@code ExecutorService} to be used by this class. The {@code
+     * ExecutorService} passed to this method is used for executing the
+     * background task. Thus it is possible to re-use an already existing
+     * {@code ExecutorService} or to use a specially configured one. If no
+     * {@code ExecutorService} is set, this instance creates a temporary one and
+     * destroys it after background initialization is complete. Note that this
+     * method must be called before {@link #start()}; otherwise an exception is
+     * thrown.
+     *
+     * @param externalExecutor the {@code ExecutorService} to be used
+     * @throws IllegalStateException if this initializer has already been
+     * started
+     */
+    public final synchronized void setExternalExecutor(
+            ExecutorService externalExecutor) {
+        if (isStarted()) {
+            throw new IllegalStateException(
+                    "Cannot set ExecutorService after start()!");
+        }
+
+        this.externalExecutor = externalExecutor;
+    }
+
+    /**
+     * Starts the background initialization. With this method the initializer
+     * becomes active and invokes the {@link #initialize()} method in a
+     * background task. A {@code BackgroundInitializer} can be started exactly
+     * once. The return value of this method determines whether the start was
+     * successful: only the first invocation of this method returns <b>true</b>,
+     * following invocations will return <b>false</b>.
+     *
+     * @return a flag whether the initializer could be started successfully
+     */
+    public synchronized boolean start() {
+        // Not yet started?
+        if (!isStarted()) {
+
+            // Determine the executor to use and whether a temporary one has to
+            // be created
+            ExecutorService tempExec;
+            executor = getExternalExecutor();
+            if (executor == null) {
+                executor = tempExec = createExecutor();
+            } else {
+                tempExec = null;
+            }
+
+            future = executor.submit(createTask(tempExec));
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the result of the background initialization. This method blocks
+     * until initialization is complete. If the background processing caused a
+     * runtime exception, it is directly thrown by this method. Checked
+     * exceptions, including {@code InterruptedException} are wrapped in a
+     * {@link ConcurrentException}. Calling this method before {@link #start()}
+     * was called causes an {@code IllegalStateException} exception to be
+     * thrown.
+     *
+     * @return the object produced by this initializer
+     * @throws ConcurrentException if a checked exception occurred during
+     * background processing
+     * @throws IllegalStateException if {@link #start()} has not been called
+     */
+    public T get() throws ConcurrentException {
+        try {
+            return getFuture().get();
+        } catch (ExecutionException execex) {
+            ConcurrentUtils.handleCause(execex);
+            return null; // should not be reached
+        } catch (InterruptedException iex) {
+            // reset interrupted state
+            Thread.currentThread().interrupt();
+            throw new ConcurrentException(iex);
+        }
+    }
+
+    /**
+     * Returns the {@code Future} object that was created when {@link #start()}
+     * was called. Therefore this method can only be called after {@code
+     * start()}.
+     *
+     * @return the {@code Future} object wrapped by this initializer
+     * @throws IllegalStateException if {@link #start()} has not been called
+     */
+    public synchronized Future<T> getFuture() {
+        if (future == null) {
+            throw new IllegalStateException("start() must be called first!");
+        }
+
+        return future;
+    }
+
+    /**
+     * Returns the {@code ExecutorService} that is actually used for executing
+     * the background task. This method can be called after {@link #start()}
+     * (before {@code start()} it returns <b>null</b>). If an external executor
+     * was set, this is also the active executor. Otherwise this method returns
+     * the temporary executor that was created by this object.
+     *
+     * @return the {@code ExecutorService} for executing the background task
+     */
+    protected synchronized final ExecutorService getActiveExecutor() {
+        return executor;
+    }
+
+    /**
+     * Returns the number of background tasks to be created for this
+     * initializer. This information is evaluated when a temporary {@code
+     * ExecutorService} is created. This base implementation returns 1. Derived
+     * classes that do more complex background processing can override it. This
+     * method is called from a synchronized block by the {@link #start()}
+     * method. Therefore overriding methods should be careful with obtaining
+     * other locks and return as fast as possible.
+     *
+     * @return the number of background tasks required by this initializer
+     */
+    protected int getTaskCount() {
+        return 1;
+    }
+
+    /**
+     * Performs the initialization. This method is called in a background task
+     * when this {@code BackgroundInitializer} is started. It must be
+     * implemented by a concrete subclass. An implementation is free to perform
+     * arbitrary initialization. The object returned by this method can be
+     * queried using the {@link #get()} method.
+     *
+     * @return a result object
+     * @throws Exception if an error occurs
+     */
+    protected abstract T initialize() throws Exception;
+
+    /**
+     * Creates a task for the background initialization. The {@code Callable}
+     * object returned by this method is passed to the {@code ExecutorService}.
+     * This implementation returns a task that invokes the {@link #initialize()}
+     * method. If a temporary {@code ExecutorService} is used, it is destroyed
+     * at the end of the task.
+     *
+     * @param execDestory the {@code ExecutorService} to be destroyed by the
+     * task
+     * @return a task for the background initialization
+     */
+    private Callable<T> createTask(ExecutorService execDestroy) {
+        return new InitializationTask(execDestroy);
+    }
+
+    /**
+     * Creates the {@code ExecutorService} to be used. This method is called if
+     * no {@code ExecutorService} was provided at construction time.
+     *
+     * @return the {@code ExecutorService} to be used
+     */
+    private ExecutorService createExecutor() {
+        return Executors.newFixedThreadPool(getTaskCount());
+    }
+
+    private class InitializationTask implements Callable<T> {
+        /** Stores the executor service to be destroyed at the end. */
+        private final ExecutorService executor;
+
+        /**
+         * Creates a new instance of {@code InitializationTask} and initializes
+         * it with the {@code ExecutorService} to be destroyed at the end.
+         *
+         * @param exec the {@code ExecutorService}
+         */
+        public InitializationTask(ExecutorService exec) {
+            executor = exec;
+        }
+
+        /**
+         * Initiates initialization and returns the result.
+         *
+         * @return the result object
+         * @throws Exception if an error occurs
+         */
+        public T call() throws Exception {
+            try {
+                return initialize();
+            } finally {
+                if (executor != null) {
+                    executor.shutdown();
+                }
+            }
+        }
+    }
+}

Propchange: commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: commons/proper/lang/trunk/src/java/org/apache/commons/lang/concurrent/BackgroundInitializer.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java
URL: http://svn.apache.org/viewvc/commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java?rev=831586&view=auto
==============================================================================
--- commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java (added)
+++ commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java Sat Oct 31 19:48:52 2009
@@ -0,0 +1,294 @@
+/*
+ * 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.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import junit.framework.TestCase;
+
+public class BackgroundInitializerTest extends TestCase {
+    /**
+     * 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(BackgroundInitializerTestImpl init) {
+        try {
+            Integer result = init.get();
+            assertEquals("Wrong result", 1, result.intValue());
+            assertEquals("Wrong number of invocations", 1, init.initializeCalls);
+            assertNotNull("No future", init.getFuture());
+        } catch (ConcurrentException cex) {
+            fail("Unexpected exception: " + cex);
+        }
+    }
+
+    /**
+     * Tests whether initialize() is invoked.
+     */
+    public void testInitialize() throws ConcurrentException {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        init.start();
+        checkInitialize(init);
+    }
+
+    /**
+     * Tries to obtain the executor before start(). It should not have been
+     * initialized yet.
+     */
+    public void testGetActiveExecutorBeforeStart() {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        assertNull("Got an executor", init.getActiveExecutor());
+    }
+
+    /**
+     * Tests whether an external executor is correctly detected.
+     */
+    public void testGetActiveExecutorExternal() {
+        ExecutorService exec = Executors.newSingleThreadExecutor();
+        try {
+            BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(
+                    exec);
+            init.start();
+            assertSame("Wrong executor", exec, init.getActiveExecutor());
+            checkInitialize(init);
+        } finally {
+            exec.shutdown();
+        }
+    }
+
+    /**
+     * Tests getActiveExecutor() for a temporary executor.
+     */
+    public void testGetActiveExecutorTemp() {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        init.start();
+        assertNotNull("No active executor", init.getActiveExecutor());
+        checkInitialize(init);
+    }
+
+    /**
+     * Tests the execution of the background task if a temporary executor has to
+     * be created.
+     */
+    public void testInitializeTempExecutor() {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        assertTrue("Wrong result of start()", init.start());
+        checkInitialize(init);
+        assertTrue("Executor not shutdown", init.getActiveExecutor()
+                .isShutdown());
+    }
+
+    /**
+     * Tests whether an external executor can be set using the
+     * setExternalExecutor() method.
+     */
+    public void testSetExternalExecutor() throws Exception {
+        ExecutorService exec = Executors.newCachedThreadPool();
+        try {
+            BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+            init.setExternalExecutor(exec);
+            assertEquals("Wrong executor service", exec, init
+                    .getExternalExecutor());
+            assertTrue("Wrong result of start()", init.start());
+            assertSame("Wrong active executor", exec, init.getActiveExecutor());
+            checkInitialize(init);
+            assertFalse("Executor was shutdown", exec.isShutdown());
+        } finally {
+            exec.shutdown();
+        }
+    }
+
+    /**
+     * Tests that setting an executor after start() causes an exception.
+     */
+    public void testSetExternalExecutorAfterStart() throws ConcurrentException {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        init.start();
+        try {
+            init.setExternalExecutor(Executors.newSingleThreadExecutor());
+            fail("Could set executor after start()!");
+        } catch (IllegalStateException istex) {
+            init.get();
+        }
+    }
+
+    /**
+     * Tests invoking start() multiple times. Only the first invocation should
+     * have an effect.
+     */
+    public void testStartMultipleTimes() {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        assertTrue("Wrong result for start()", init.start());
+        for (int i = 0; i < 10; i++) {
+            assertFalse("Could start again", init.start());
+        }
+        checkInitialize(init);
+    }
+
+    /**
+     * Tests calling get() before start(). This should cause an exception.
+     */
+    public void testGetBeforeStart() throws ConcurrentException {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        try {
+            init.get();
+            fail("Could call get() before start()!");
+        } catch (IllegalStateException istex) {
+            // ok
+        }
+    }
+
+    /**
+     * Tests the get() method if background processing causes a runtime
+     * exception.
+     */
+    public void testGetRuntimeException() throws Exception {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        RuntimeException rex = new RuntimeException();
+        init.ex = rex;
+        init.start();
+        try {
+            init.get();
+            fail("Exception not thrown!");
+        } catch (Exception ex) {
+            assertEquals("Runtime exception not thrown", rex, ex);
+        }
+    }
+
+    /**
+     * Tests the get() method if background processing causes a checked
+     * exception.
+     */
+    public void testGetCheckedException() throws Exception {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        Exception ex = new Exception();
+        init.ex = ex;
+        init.start();
+        try {
+            init.get();
+            fail("Exception not thrown!");
+        } catch (ConcurrentException cex) {
+            assertEquals("Exception not thrown", ex, cex.getCause());
+        }
+    }
+
+    /**
+     * Tests the get() method if waiting for the initialization is interrupted.
+     */
+    public void testGetInterruptedException() throws Exception {
+        ExecutorService exec = Executors.newSingleThreadExecutor();
+        final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(
+                exec);
+        final CountDownLatch latch1 = new CountDownLatch(1);
+        init.shouldSleep = true;
+        init.start();
+        final AtomicReference<InterruptedException> iex = new AtomicReference<InterruptedException>();
+        Thread getThread = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    init.get();
+                } catch (ConcurrentException cex) {
+                    if (cex.getCause() instanceof InterruptedException) {
+                        iex.set((InterruptedException) cex.getCause());
+                    }
+                } finally {
+                    assertTrue("Thread not interrupted", isInterrupted());
+                    latch1.countDown();
+                }
+            }
+        };
+        getThread.start();
+        getThread.interrupt();
+        latch1.await();
+        exec.shutdownNow();
+        exec.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
+        assertNotNull("No interrupted exception", iex.get());
+    }
+
+    /**
+     * Tests isStarted() before start() was called.
+     */
+    public void testIsStartedFalse() {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        assertFalse("Already started", init.isStarted());
+    }
+
+    /**
+     * Tests isStarted() after start().
+     */
+    public void testIsStartedTrue() {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        init.start();
+        assertTrue("Not started", init.isStarted());
+    }
+
+    /**
+     * Tests isStarted() after the background task has finished.
+     */
+    public void testIsStartedAfterGet() {
+        BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl();
+        init.start();
+        checkInitialize(init);
+        assertTrue("Not started", init.isStarted());
+    }
+
+    /**
+     * A concrete implementation of BackgroundInitializer. It also overloads
+     * some methods that simplify testing.
+     */
+    private static class BackgroundInitializerTestImpl extends
+            BackgroundInitializer<Integer> {
+        /** An exception to be thrown by initialize(). */
+        Exception ex;
+
+        /** A flag whether the background task should sleep a while. */
+        boolean shouldSleep;
+
+        /** The number of invocations of initialize(). */
+        volatile int initializeCalls;
+
+        public BackgroundInitializerTestImpl() {
+            super();
+        }
+
+        public BackgroundInitializerTestImpl(ExecutorService exec) {
+            super(exec);
+        }
+
+        /**
+         * Records this invocation. Optionally throws an exception or sleeps a
+         * while.
+         */
+        @Override
+        protected Integer initialize() throws Exception {
+            if (ex != null) {
+                throw ex;
+            }
+            if (shouldSleep) {
+                Thread.sleep(60000L);
+            }
+            return ++initializeCalls;
+        }
+    }
+}

Propchange: commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Propchange: commons/proper/lang/trunk/src/test/org/apache/commons/lang/concurrent/BackgroundInitializerTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain