This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push: new 5d146cd06 Use LazyInitializer without subclassing. (#1123) 5d146cd06 is described below commit 5d146cd06965432812c11ea8a6b458b3d2f56f65 Author: Gary Gregory <garydgreg...@users.noreply.github.com> AuthorDate: Fri Oct 20 13:53:08 2023 -0400 Use LazyInitializer without subclassing. (#1123) * Use LazyInitializer without subclassing. - Allow a Supplier for initialized object - Allow a Consumer to close the managed object * Allow any checked exception in the argument to supplier and consumer * Make all impls of AbstractConcurrentInitializer concrete with builders and tests * add tests for closer * Use ConcurrentException as the wrapper for close * Changes requested in code review --------- Co-authored-by: Gary Gregory <gardgreg...@gmail.com> Co-authored-by: Benjamin Confino <benja...@uk.ibm.com> --- .../commons/lang3/builder/AbstractSupplier.java | 42 ++++ .../concurrent/AbstractConcurrentInitializer.java | 152 +++++++++++- .../lang3/concurrent/AtomicInitializer.java | 58 ++++- .../lang3/concurrent/AtomicSafeInitializer.java | 58 ++++- .../lang3/concurrent/BackgroundInitializer.java | 72 +++++- .../concurrent/CallableBackgroundInitializer.java | 9 + .../commons/lang3/concurrent/LazyInitializer.java | 106 ++++++--- .../concurrent/MultiBackgroundInitializer.java | 33 +++ ...oncurrentInitializerCloseAndExceptionsTest.java | 184 +++++++++++++++ .../concurrent/AtomicInitializerSupplierTest.java | 43 ++++ .../AtomicSafeInitializerSupplierTest.java | 97 ++++++++ .../BackgroundInitializerSupplierTest.java | 148 ++++++++++++ .../concurrent/BackgroundInitializerTest.java | 111 ++++++--- .../concurrent/LazyInitializerCloserTest.java | 54 +++++ .../LazyInitializerFailableCloserTest.java | 72 ++++++ .../concurrent/LazyInitializerSupplierTest.java | 43 ++++ .../MultiBackgroundInitializerSupplierTest.java | 261 +++++++++++++++++++++ .../concurrent/MultiBackgroundInitializerTest.java | 121 +++++++--- 18 files changed, 1574 insertions(+), 90 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java b/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java new file mode 100644 index 000000000..29b94b7aa --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/builder/AbstractSupplier.java @@ -0,0 +1,42 @@ +/* + * 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.lang3.builder; + +import org.apache.commons.lang3.function.FailableSupplier; + +/** + * Abstracts supplying an instance of {@code T}. Use to implement the builder pattern. + * + * @param <T> the type of instances to build. + * @param <B> the type of builder. + * @param <E> The kind of thrown exception or error. + * @since 3.14.0 + */ +public abstract class AbstractSupplier<T, B extends AbstractSupplier<T, B, E>, E extends Throwable> implements FailableSupplier<T, E> { + + /** + * Returns this instance typed as the proper subclass type. + * + * @return this instance typed as the proper subclass type. + */ + @SuppressWarnings("unchecked") + protected B asThis() { + return (B) this; + } + +} diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java index 7786c8057..fb61e778f 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializer.java @@ -17,6 +17,13 @@ package org.apache.commons.lang3.concurrent; +import java.util.Objects; + +import org.apache.commons.lang3.builder.AbstractSupplier; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * Abstracts and defines operations for ConcurrentInitializer implementations. * @@ -26,15 +33,150 @@ package org.apache.commons.lang3.concurrent; */ public abstract class AbstractConcurrentInitializer<T, E extends Exception> implements ConcurrentInitializer<T> { + /** + * Builds a new instance for subclasses. + * + * @param <T> the type of the object managed by the initializer class. + * @param <I> the type of the initializer class. + * @param <B> the type of builder. + * @param <E> The exception type thrown by {@link #initialize()}. + */ + public abstract static class AbstractBuilder<I extends AbstractConcurrentInitializer<T, E>, T, B extends AbstractBuilder<I, T, B, E>, E extends Exception> + extends AbstractSupplier<I, B, E> { + + /** + * Closer consumer called by {@link #close()}. + */ + private FailableConsumer<T, ? extends Exception> closer = FailableConsumer.nop(); + + /** + * Initializer supplier called by {@link #initialize()}. + */ + private FailableSupplier<T, ? extends Exception> initializer = FailableSupplier.nul(); + + /** + * Gets the closer consumer called by {@link #close()}. + * + * @return the closer consumer called by {@link #close()}. + */ + public FailableConsumer<T, ? extends Exception> getCloser() { + return closer; + } + + /** + * Gets the initializer supplier called by {@link #initialize()}. + * + * @return the initializer supplier called by {@link #initialize()}. + */ + public FailableSupplier<T, ? extends Exception> getInitializer() { + return initializer; + } + + /** + * Sets the closer consumer called by {@link #close()}. + * + * @param closer the consumer called by {@link #close()}. + * @return this + */ + public B setCloser(final FailableConsumer<T, ? extends Exception> closer) { + this.closer = closer != null ? closer : FailableConsumer.nop(); + return asThis(); + } + + /** + * Sets the initializer supplier called by {@link #initialize()}. + * + * @param initializer the supplier called by {@link #initialize()}. + * @return this + */ + public B setInitializer(final FailableSupplier<T, ? extends Exception> initializer) { + this.initializer = initializer != null ? initializer : FailableSupplier.nul(); + return asThis(); + } + + } + + /** + * Closer consumer called by {@link #close()}. + */ + private final FailableConsumer<? super T, ? extends Exception> closer; + + /** + * Initializer supplier called by {@link #initialize()}. + */ + private final FailableSupplier<? extends T, ? extends Exception> initializer; + + /** + * Constructs a new instance. + */ + public AbstractConcurrentInitializer() { + this(FailableSupplier.nul(), FailableConsumer.nop()); + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + AbstractConcurrentInitializer(final FailableSupplier<? extends T, ? extends Exception> initializer, final FailableConsumer<? super T, ? extends Exception> closer) { + this.closer = Objects.requireNonNull(closer, "closer"); + this.initializer = Objects.requireNonNull(initializer, "initializer"); + } + + /** + * Calls the closer with the manager object. + * + * @throws ConcurrentException Thrown by the closer. + * @since 3.14.0 + */ + public void close() throws ConcurrentException { + if (isInitialized()) { + try { + closer.accept(get()); + } catch (final Exception e) { + // This intentionally does not duplicate the logic in initialize + // or care about the generic type E. + // + // initialize may run inside a Future and it does not make sense + // to wrap an exception stored inside a Future. However close() + // always runs on the current thread so it always wraps in a + // ConcurrentException + throw new ConcurrentException(ExceptionUtils.throwUnchecked(e)); + } + } + } + /** * Creates and initializes the object managed by this {@code * ConcurrentInitializer}. This method is called by {@link #get()} when the object is accessed for the first time. An implementation can focus on the * creation of the object. No synchronization is needed, as this is already handled by {@code get()}. + * <p> + * Subclasses and clients that do not provide an initializer are expected to implement this method. + * </p> * * @return the managed data object * @throws E if an error occurs during object creation */ - protected abstract T initialize() throws E; + @SuppressWarnings("unchecked") + protected T initialize() throws E { + try { + return initializer.get(); + } catch (final Exception e) { + // Do this first so we don't pass a RuntimeException or Error into an exception constructor + ExceptionUtils.throwUnchecked(e); + + // Depending on the subclass of AbstractConcurrentInitializer E can be Exception or ConcurrentException + // if E is Exception the if statement below will always be true, and the new Exception object created + // in getTypedException will never be thrown. If E is ConcurrentException and the if statement is false + // we throw the ConcurrentException returned from getTypedException, which wraps the original exception. + final E typedException = getTypedException(e); + if (typedException.getClass().isAssignableFrom(e.getClass())) { + throw (E) e; + } + throw typedException; + } + } /** * Returns true if initialization has been completed. If initialization threw an exception this will return false, but it will return true if a subsequent @@ -45,4 +187,12 @@ public abstract class AbstractConcurrentInitializer<T, E extends Exception> impl */ protected abstract boolean isInitialized(); + /** + * Gets an Exception with a type of E as defined by a concrete subclass of this class. + * + * @param e The actual exception that was thrown + * @return a new exception with the actual type of E, that wraps e. + */ + protected abstract E getTypedException(Exception e); + } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java index 42706f79c..affd8c81d 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AtomicInitializer.java @@ -18,6 +18,9 @@ package org.apache.commons.lang3.concurrent; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * A specialized implementation of the {@link ConcurrentInitializer} interface * based on an {@link AtomicReference} variable. @@ -62,13 +65,58 @@ import java.util.concurrent.atomic.AtomicReference; * @since 3.0 * @param <T> the type of the object managed by this initializer class */ -public abstract class AtomicInitializer<T> extends AbstractConcurrentInitializer<T, RuntimeException> { +public class AtomicInitializer<T> extends AbstractConcurrentInitializer<T, ConcurrentException> { + + /** + * Builds a new instance. + * + * @param <T> the type of the object managed by the initializer. + * @param <I> the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder<I extends AtomicInitializer<T>, T> extends AbstractBuilder<I, T, Builder<I, T>, ConcurrentException> { + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new AtomicInitializer(getInitializer(), getCloser()); + } + + } private static final Object NO_INIT = new Object(); /** Holds the reference to the managed object. */ private final AtomicReference<T> reference = new AtomicReference<>(getNoInit()); + /** + * Creates a new builder. + * + * @param <T> the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static <T> Builder<AtomicInitializer<T>, T> builder() { + return new Builder<>(); + } + + /** + * Constructs a new instance. + */ + public AtomicInitializer() { + // empty + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + private AtomicInitializer(final FailableSupplier<T, ConcurrentException> initializer, final FailableConsumer<T, ConcurrentException> closer) { + super(initializer, closer); + } + /** * Returns the object managed by this initializer. The object is created if * it is not available yet and stored internally. This method always returns @@ -109,4 +157,12 @@ public abstract class AtomicInitializer<T> extends AbstractConcurrentInitializer public boolean isInitialized() { return reference.get() != NO_INIT; } + + /** + * {@inheritDoc} + */ + @Override + protected ConcurrentException getTypedException(Exception e) { + return new ConcurrentException(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java index 5e2911b50..7015c53f0 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializer.java @@ -18,6 +18,9 @@ package org.apache.commons.lang3.concurrent; import java.util.concurrent.atomic.AtomicReference; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * A specialized {@link ConcurrentInitializer} implementation which is similar * to {@link AtomicInitializer}, but ensures that the {@link #initialize()} @@ -51,7 +54,24 @@ import java.util.concurrent.atomic.AtomicReference; * @since 3.0 * @param <T> the type of the object managed by this initializer class */ -public abstract class AtomicSafeInitializer<T> extends AbstractConcurrentInitializer<T, RuntimeException> { +public class AtomicSafeInitializer<T> extends AbstractConcurrentInitializer<T, ConcurrentException> { + + /** + * Builds a new instance. + * + * @param <T> the type of the object managed by the initializer. + * @param <I> the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder<I extends AtomicSafeInitializer<T>, T> extends AbstractBuilder<I, T, Builder<I, T>, ConcurrentException> { + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new AtomicSafeInitializer(getInitializer(), getCloser()); + } + + } private static final Object NO_INIT = new Object(); @@ -61,6 +81,34 @@ public abstract class AtomicSafeInitializer<T> extends AbstractConcurrentInitial /** Holds the reference to the managed object. */ private final AtomicReference<T> reference = new AtomicReference<>(getNoInit()); + /** + * Creates a new builder. + * + * @param <T> the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static <T> Builder<AtomicSafeInitializer<T>, T> builder() { + return new Builder<>(); + } + + /** + * Constructs a new instance. + */ + public AtomicSafeInitializer() { + // empty + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + private AtomicSafeInitializer(final FailableSupplier<T, ConcurrentException> initializer, final FailableConsumer<T, ConcurrentException> closer) { + super(initializer, closer); + } + /** * Gets (and initialize, if not initialized yet) the required object * @@ -97,4 +145,12 @@ public abstract class AtomicSafeInitializer<T> extends AbstractConcurrentInitial public boolean isInitialized() { return reference.get() != NO_INIT; } + + /** + * {@inheritDoc} + */ + @Override + protected ConcurrentException getTypedException(Exception e) { + return new ConcurrentException(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java index 91ee1c015..dd95ed551 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/BackgroundInitializer.java @@ -23,6 +23,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** * A class that allows complex initialization operations in a background task. * @@ -82,7 +85,42 @@ import java.util.concurrent.Future; * @since 3.0 * @param <T> the type of the object managed by this initializer class */ -public abstract class BackgroundInitializer<T> extends AbstractConcurrentInitializer<T, Exception> { +public class BackgroundInitializer<T> extends AbstractConcurrentInitializer<T, Exception> { + + /** + * Builds a new instance. + * + * @param <T> the type of the object managed by the initializer. + * @param <I> the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder<I extends BackgroundInitializer<T>, T> extends AbstractBuilder<I, T, Builder<I, T>, Exception> { + + /** + * The external executor service for executing tasks. null is an permitted value. + */ + private ExecutorService externalExecutor; + + /** + * Sets the external executor service for executing tasks. null is an permitted value. + * + * @see org.apache.commons.lang3.concurrent.BackgroundInitializer#setExternalExecutor(ExecutorService) + * + * @param externalExecutor the {@link ExecutorService} to be used. + * @return this + */ + public Builder<I, T> setExternalExecutor(final ExecutorService externalExecutor) { + this.externalExecutor = externalExecutor; + return asThis(); + } + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new BackgroundInitializer(getInitializer(), getCloser(), externalExecutor); + } + + } /** The external executor service for executing tasks. */ private ExecutorService externalExecutor; // @GuardedBy("this") @@ -115,6 +153,29 @@ public abstract class BackgroundInitializer<T> extends AbstractConcurrentInitial setExternalExecutor(exec); } + /** + * Creates a new builder. + * + * @param <T> the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static <T> Builder<BackgroundInitializer<T>, T> builder() { + return new Builder<>(); + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + * @param exec the {@link ExecutorService} to be used @see #setExternalExecutor(ExecutorService) + */ + private BackgroundInitializer(final FailableSupplier<T, ConcurrentException> initializer, final FailableConsumer<T, ConcurrentException> closer, final ExecutorService exec) { + super(initializer, closer); + setExternalExecutor(exec); + } + /** * Returns the external {@link ExecutorService} to be used by this class. * @@ -341,4 +402,13 @@ public abstract class BackgroundInitializer<T> extends AbstractConcurrentInitial } } } + + /** + * {@inheritDoc} + */ + @Override + protected Exception getTypedException(Exception e) { + //This Exception object will be used for type comparison in AbstractConcurrentInitializer.initialize but not thrown + return new Exception(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java index cccb43d92..81d6efab0 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/CallableBackgroundInitializer.java @@ -119,4 +119,13 @@ public class CallableBackgroundInitializer<T> extends BackgroundInitializer<T> { private void checkCallable(final Callable<T> callable) { Objects.requireNonNull(callable, "callable"); } + + /** + * {@inheritDoc} + */ + @Override + protected Exception getTypedException(Exception e) { + //This Exception object will be used for type comparison in AbstractConcurrentInitializer.initialize but not thrown + return new Exception(e); + } } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java index 7a69bfd42..acdfead21 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/LazyInitializer.java @@ -16,29 +16,25 @@ */ package org.apache.commons.lang3.concurrent; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + /** - * This class provides a generic implementation of the lazy initialization - * pattern. + * This class provides a generic implementation of the lazy initialization pattern. * * <p> - * Sometimes an application has to deal with an object only under certain - * circumstances, e.g. when the user selects a specific menu item or if a - * special event is received. If the creation of the object is costly or the - * consumption of memory or other system resources is significant, it may make - * sense to defer the creation of this object until it is really needed. This is - * a use case for the lazy initialization pattern. + * Sometimes an application has to deal with an object only under certain circumstances, e.g. when the user selects a specific menu item or if a special event + * is received. If the creation of the object is costly or the consumption of memory or other system resources is significant, it may make sense to defer the + * creation of this object until it is really needed. This is a use case for the lazy initialization pattern. * </p> * <p> - * This abstract base class provides an implementation of the double-check idiom - * for an instance field as discussed in Joshua Bloch's "Effective Java", 2nd - * edition, item 71. The class already implements all necessary synchronization. - * A concrete subclass has to implement the {@code initialize()} method, which + * This abstract base class provides an implementation of the double-check idiom for an instance field as discussed in Joshua Bloch's "Effective Java", 2nd + * edition, item 71. The class already implements all necessary synchronization. A concrete subclass has to implement the {@code initialize()} method, which * actually creates the wrapped data object. * </p> * <p> - * As an usage example consider that we have a class {@code ComplexObject} whose - * instantiation is a complex operation. In order to apply lazy initialization - * to this class, a subclass of {@link LazyInitializer} has to be created: + * As an usage example consider that we have a class {@code ComplexObject} whose instantiation is a complex operation. In order to apply lazy initialization to + * this class, a subclass of {@link LazyInitializer} has to be created: * </p> * * <pre> @@ -51,9 +47,8 @@ package org.apache.commons.lang3.concurrent; * </pre> * * <p> - * Access to the data object is provided through the {@code get()} method. So, - * code that wants to obtain the {@code ComplexObject} instance would simply - * look like this: + * Access to the data object is provided through the {@code get()} method. So, code that wants to obtain the {@code ComplexObject} instance would simply look + * like this: * </p> * * <pre> @@ -65,32 +60,75 @@ package org.apache.commons.lang3.concurrent; * </pre> * * <p> - * If multiple threads call the {@code get()} method when the object has not yet - * been created, they are blocked until initialization completes. The algorithm - * guarantees that only a single instance of the wrapped object class is - * created, which is passed to all callers. Once initialized, calls to the - * {@code get()} method are pretty fast because no synchronization is needed - * (only an access to a <b>volatile</b> member field). + * If multiple threads call the {@code get()} method when the object has not yet been created, they are blocked until initialization completes. The algorithm + * guarantees that only a single instance of the wrapped object class is created, which is passed to all callers. Once initialized, calls to the {@code get()} + * method are pretty fast because no synchronization is needed (only an access to a <b>volatile</b> member field). * </p> * * @since 3.0 - * @param <T> the type of the object managed by this initializer class + * @param <T> the type of the object managed by the initializer. */ -public abstract class LazyInitializer<T> extends AbstractConcurrentInitializer<T, ConcurrentException> { +public class LazyInitializer<T> extends AbstractConcurrentInitializer<T, ConcurrentException> { + + /** + * Builds a new instance. + * + * @param <T> the type of the object managed by the initializer. + * @param <I> the type of the initializer managed by this builder. + * @since 3.14.0 + */ + public static class Builder<I extends LazyInitializer<T>, T> extends AbstractBuilder<I, T, Builder<I, T>, ConcurrentException> { + + @SuppressWarnings("unchecked") + @Override + public I get() { + return (I) new LazyInitializer(getInitializer(), getCloser()); + } + } + + /** + * A unique value indicating an un-initialzed instance. + */ private static final Object NO_INIT = new Object(); + /** + * Creates a new builder. + * + * @param <T> the type of object to build. + * @return a new builder. + * @since 3.14.0 + */ + public static <T> Builder<LazyInitializer<T>, T> builder() { + return new Builder<>(); + } + /** Stores the managed object. */ @SuppressWarnings("unchecked") private volatile T object = (T) NO_INIT; /** - * Returns the object wrapped by this instance. On first access the object - * is created. After that it is cached and can be accessed pretty fast. + * Constructs a new instance. + */ + public LazyInitializer() { + // empty + } + + /** + * Constructs a new instance. + * + * @param initializer the initializer supplier called by {@link #initialize()}. + * @param closer the closer consumer called by {@link #close()}. + */ + private LazyInitializer(final FailableSupplier<T, ConcurrentException> initializer, final FailableConsumer<T, ConcurrentException> closer) { + super(initializer, closer); + } + + /** + * Returns the object wrapped by this instance. On first access the object is created. After that it is cached and can be accessed pretty fast. * * @return the object initialized by this {@link LazyInitializer} - * @throws ConcurrentException if an error occurred during initialization of - * the object + * @throws ConcurrentException if an error occurred during initialization of the object */ @Override public T get() throws ConcurrentException { @@ -121,4 +159,12 @@ public abstract class LazyInitializer<T> extends AbstractConcurrentInitializer<T return object != NO_INIT; } + /** + * {@inheritDoc} + */ + @Override + protected ConcurrentException getTypedException(Exception e) { + return new ConcurrentException(e); + } + } diff --git a/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java b/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java index f294ef407..1357576d5 100644 --- a/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java +++ b/src/main/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializer.java @@ -215,6 +215,39 @@ public class MultiBackgroundInitializer return childInitializers.values().stream().allMatch(BackgroundInitializer::isInitialized); } + /** + * Calls the closer of all child {@code BackgroundInitializer} objects + * + * @throws ConcurrentException throws an ConcurrentException that will have all other exceptions as suppressed exceptions. ConcurrentException thrown by children will be unwrapped. + * @since 3.14.0 + */ + @Override + public void close() throws ConcurrentException { + ConcurrentException exception = null; + + for (BackgroundInitializer<?> child : childInitializers.values()) { + try { + child.close(); + } catch (Exception e) { + if (exception == null) { + exception = new ConcurrentException(); + } + + if (e instanceof ConcurrentException) { + // Because ConcurrentException is only created by classes in this package + // we can safely unwrap it. + exception.addSuppressed(e.getCause()); + } else { + exception.addSuppressed(e); + } + } + } + + if (exception != null) { + throw exception; + } + } + /** * A data class for storing the results of the background initialization * performed by {@link MultiBackgroundInitializer}. Objects of this inner diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java new file mode 100644 index 000000000..af77f3c61 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/AbstractConcurrentInitializerCloseAndExceptionsTest.java @@ -0,0 +1,184 @@ +/* + * 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.lang3.concurrent; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +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.io.IOException; +import java.sql.SQLException; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.Test; + +/** + * An abstract base class for tests of exceptions thrown during initialize and close methods + * on concrete {@code ConcurrentInitializer} implementations. + * + * This class provides some basic tests for initializer implementations. Derived + * class have to create a {@link ConcurrentInitializer} object on which the + * tests are executed. + */ +public abstract class AbstractConcurrentInitializerCloseAndExceptionsTest extends AbstractConcurrentInitializerTest { + + /** + * This method tests that if a AbstractConcurrentInitializer.initialize method catches a + * ConcurrentException it will rethrow it without wrapping it. + */ + @Test + public void testSupplierThrowsConcurrentException() { + final ConcurrentException concurrentException = new ConcurrentException(); + + @SuppressWarnings("unchecked") + final ConcurrentInitializer<CloseableObject> initializer = createInitializerThatThrowsException( + () -> { + if ("test".equals("test")) { + throw concurrentException; + } + return new CloseableObject(); + }, + FailableConsumer.NOP); + try { + initializer.get(); + fail(); + } catch (ConcurrentException e) { + assertEquals(concurrentException, e); + } + } + + /** + * This method tests that if AbstractConcurrentInitializer.initialize catches a checked + * exception it will rethrow it wrapped in a ConcurrentException + */ + @SuppressWarnings("unchecked") //for NOP + @Test + public void testSupplierThrowsCheckedException() { + final ConcurrentInitializer<CloseableObject> initializer = createInitializerThatThrowsException( + () -> methodThatThrowsException(ExceptionToThrow.IOException), + FailableConsumer.NOP); + assertThrows(ConcurrentException.class, () -> initializer.get()); + } + + /** + * This method tests that if AbstractConcurrentInitializer.initialize catches a runtime exception + * it will not be wrapped in a ConcurrentException + */ + @SuppressWarnings("unchecked") + @Test + public void testSupplierThrowsRuntimeException() { + final ConcurrentInitializer<CloseableObject> initializer = createInitializerThatThrowsException( + () -> methodThatThrowsException(ExceptionToThrow.NullPointerException), + FailableConsumer.NOP); + assertThrows(NullPointerException.class, () -> initializer.get()); + } + + /** + * This method tests that if AbstractConcurrentInitializer.close catches a + * ConcurrentException it will rethrow it wrapped in a ConcurrentException + */ + @SuppressWarnings("rawtypes") + @Test + public void testCloserThrowsCheckedException() throws ConcurrentException { + final ConcurrentInitializer<CloseableObject> initializer = createInitializerThatThrowsException( + CloseableObject::new, + (CloseableObject) -> methodThatThrowsException(ExceptionToThrow.IOException)); + try { + initializer.get(); + ((AbstractConcurrentInitializer) initializer).close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertThat(e.getCause(), instanceOf(IOException.class)); + } + } + + /** + * This method tests that if AbstractConcurrentInitializer.close catches a + * RuntimeException it will throw it withuot wrapping it in a ConcurrentException + */ + @SuppressWarnings("rawtypes") + @Test + public void testCloserThrowsRuntimeException() throws ConcurrentException { + final ConcurrentInitializer<CloseableObject> initializer = createInitializerThatThrowsException( + CloseableObject::new, + (CloseableObject) -> methodThatThrowsException(ExceptionToThrow.NullPointerException)); + + initializer.get(); + assertThrows(NullPointerException.class, () -> { + ((AbstractConcurrentInitializer) initializer).close(); + }); + } + + /** + * This method tests that if AbstractConcurrentInitializer.close actually closes the wrapped object + */ + @SuppressWarnings("rawtypes") + @Test + public void testWorkingCloser() throws Exception { + final ConcurrentInitializer<CloseableObject> initializer = createInitializerThatThrowsException( + CloseableObject::new, + CloseableObject::close); + + CloseableObject cloesableObject = initializer.get(); + assertFalse(cloesableObject.isClosed()); + ((AbstractConcurrentInitializer) initializer).close(); + assertTrue(cloesableObject.isClosed()); + } + + protected enum ExceptionToThrow { + IOException, + SQLException, + NullPointerException + } + + // The use of enums rather than accepting an Exception as the input means we can have + // multiple exception types on the method signature. + protected static CloseableObject methodThatThrowsException(ExceptionToThrow input) throws IOException, SQLException, ConcurrentException { + switch (input) { + case IOException: + throw new IOException(); + case SQLException: + throw new SQLException(); + case NullPointerException: + throw new NullPointerException(); + default: + fail(); + return new CloseableObject(); + } + } + + protected abstract ConcurrentInitializer<CloseableObject> createInitializerThatThrowsException( + FailableSupplier<CloseableObject, ? extends Exception> supplier, FailableConsumer<CloseableObject, ? extends Exception> closer); + + protected static final class CloseableObject { + boolean closed; + + public boolean isClosed() { + return closed; + } + + public void close() { + closed = true; + } + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java new file mode 100644 index 000000000..41413ff84 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicInitializerSupplierTest.java @@ -0,0 +1,43 @@ +/* + * 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.lang3.concurrent; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + +/** + * Test class for {@code AtomicInitializer}. + */ +public class AtomicInitializerSupplierTest extends AbstractConcurrentInitializerCloseAndExceptionsTest { + /** + * Returns the initializer to be tested. + * + * @return the {@code AtomicInitializer} + */ + @Override + protected ConcurrentInitializer<Object> createInitializer() { + return AtomicInitializer.<Object>builder().setInitializer(() -> new Object()).get(); + } + + @Override + protected ConcurrentInitializer<CloseableObject> createInitializerThatThrowsException( + final FailableSupplier<CloseableObject, ? extends Exception> supplier, + final FailableConsumer<CloseableObject, ? extends Exception> closer) { + return AtomicInitializer.<CloseableObject>builder().setInitializer(supplier).setCloser(closer).get(); + } + +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java new file mode 100644 index 000000000..9bbfde19b --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/AtomicSafeInitializerSupplierTest.java @@ -0,0 +1,97 @@ +/* + * 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.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.apache.commons.lang3.concurrent.AbstractConcurrentInitializerCloseAndExceptionsTest.CloseableObject; +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@code AtomicSafeInitializer} which also serves as a simple example. + */ +public class AtomicSafeInitializerSupplierTest extends AbstractConcurrentInitializerCloseAndExceptionsTest { + + /** An initCounter used in testing. Reset before each test */ + private AtomicInteger initCounter = new AtomicInteger(); + + /** A supplier method used in testing */ + private Object incAndMakeObject() { + initCounter.incrementAndGet(); + return new Object(); + } + + @Override + protected ConcurrentInitializer<CloseableObject> createInitializerThatThrowsException( + final FailableSupplier<CloseableObject, ? extends Exception> supplier, + final FailableConsumer<CloseableObject, ? extends Exception> closer) { + return AtomicSafeInitializer.<CloseableObject>builder().setInitializer(supplier).setCloser(closer).get(); + } + + @BeforeEach + public void setUp() { + initCounter = new AtomicInteger(); + } + + /** + * Creates the initializer to be tested. + * + * @return the {@code AtomicSafeInitializer} under test + */ + @Override + protected ConcurrentInitializer<Object> createInitializer() { + return AtomicSafeInitializer.<Object>builder().setInitializer(this::incAndMakeObject).get(); + } + + /** + * Tests that initialize() is called only once. + * + * @throws org.apache.commons.lang3.concurrent.ConcurrentException because {@link #testGetConcurrent()} may throw it + * @throws InterruptedException because {@link #testGetConcurrent()} may throw it + */ + @Test + public void testNumberOfInitializeInvocations() throws ConcurrentException, InterruptedException { + testGetConcurrent(); + assertEquals(1, 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()); + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java new file mode 100644 index 000000000..3a6b1da1a --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerSupplierTest.java @@ -0,0 +1,148 @@ +/* + * 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.lang3.concurrent; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.concurrent.ExecutorService; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.Test; + +public class BackgroundInitializerSupplierTest extends BackgroundInitializerTest { + + /** + * Tests that close() method closes the wrapped object + * + * @throws Exception + */ + @Test + public void testClose() throws Exception { + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); + assertFalse(init.getCloseableCounter().isClosed(), "closed without close() call"); + init.close(); + assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded before start()"); + init.start(); + init.get(); //ensure the Future has completed. + assertFalse(init.getCloseableCounter().isClosed(), "closed() succeeded after start() but before close()"); + init.close(); + assertTrue(init.getCloseableCounter().isClosed(), "closed() did not succeed"); + } + + /** + * Tests that close() wraps a checked exception in a ConcurrentException + * + * @throws Exception + */ + @Test + public void testCloseWithCheckedException() throws Exception { + + final IOException ioException = new IOException(); + final FailableConsumer<?, ?> IOExceptionConsumer = (CloseableCounter cc) -> { + throw ioException; + }; + + final AbstractBackgroundInitializerTestImpl init = new SupplierBackgroundInitializerTestImpl(IOExceptionConsumer); + init.start(); + init.get(); //ensure the Future has completed. + try { + init.close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertSame(ioException, e.getCause()); + } + } + + /** + * Tests that close() throws a runtime exception + * + * @throws Exception + */ + @Test + public void testCloseWithRuntimeException() throws Exception { + + final NullPointerException npe = new NullPointerException(); + final FailableConsumer<?, ?> NullPointerExceptionConsumer = (CloseableCounter cc) -> { + throw npe; + }; + + final AbstractBackgroundInitializerTestImpl init = new SupplierBackgroundInitializerTestImpl(NullPointerExceptionConsumer); + init.start(); + init.get(); //ensure the Future has completed. + try { + init.close(); + fail(); + } catch (Exception e) { + assertSame(npe, e); + } + } + + /** + * A concrete implementation of BackgroundInitializer. It is designed as a warpper so the test can + * use the same builder pattern that real code will. + */ + protected static final class SupplierBackgroundInitializerTestImpl extends AbstractBackgroundInitializerTestImpl { + + SupplierBackgroundInitializerTestImpl() { + super(); + setSupplierAndCloser((CloseableCounter cc) -> cc.close()); + } + + SupplierBackgroundInitializerTestImpl(FailableConsumer<?, ?> consumer) { + super(); + setSupplierAndCloser(consumer); + } + + SupplierBackgroundInitializerTestImpl(final ExecutorService exec) { + super(exec); + setSupplierAndCloser((CloseableCounter cc) -> cc.close()); + } + + private void setSupplierAndCloser(FailableConsumer<?, ?> consumer) { + try { + // Use reflection here because the constructors we need are private + FailableSupplier<?, ?> supplier = () -> initializeInternal(); + Field initializer = AbstractConcurrentInitializer.class.getDeclaredField("initializer"); + initializer.setAccessible(true); + initializer.set(this, supplier); + + Field closer = AbstractConcurrentInitializer.class.getDeclaredField("closer"); + closer.setAccessible(true); + closer.set(this, consumer); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + fail(); + } + } + } + + protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl() { + return new SupplierBackgroundInitializerTestImpl(); + } + + protected SupplierBackgroundInitializerTestImpl getBackgroundInitializerTestImpl(final ExecutorService exec) { + return new SupplierBackgroundInitializerTestImpl(exec); + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java index 90193f2af..8714beaaf 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/BackgroundInitializerTest.java @@ -29,6 +29,8 @@ 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; @@ -42,10 +44,10 @@ public class BackgroundInitializerTest extends AbstractLangTest { * * @param init the initializer to test */ - private void checkInitialize(final BackgroundInitializerTestImpl init) throws ConcurrentException { - final Integer result = init.get(); + private void checkInitialize(final AbstractBackgroundInitializerTestImpl init) throws ConcurrentException { + final Integer result = init.get().getInitializeCalls(); assertEquals(1, result.intValue(), "Wrong result"); - assertEquals(1, init.initializeCalls, "Wrong number of invocations"); + assertEquals(1, init.getCloseableCounter().getInitializeCalls(), "Wrong number of invocations"); assertNotNull(init.getFuture(), "No future"); } @@ -54,7 +56,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitialize() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); checkInitialize(init); } @@ -65,7 +67,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetActiveExecutorBeforeStart() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertNull(init.getActiveExecutor(), "Got an executor"); } @@ -76,7 +78,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { public void testGetActiveExecutorExternal() throws InterruptedException, ConcurrentException { final ExecutorService exec = Executors.newSingleThreadExecutor(); try { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl( + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl( exec); init.start(); assertSame(exec, init.getActiveExecutor(), "Wrong executor"); @@ -92,7 +94,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetActiveExecutorTemp() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); assertNotNull(init.getActiveExecutor(), "No active executor"); checkInitialize(init); @@ -104,7 +106,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitializeTempExecutor() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertTrue(init.start(), "Wrong result of start()"); checkInitialize(init); assertTrue(init.getActiveExecutor().isShutdown(), "Executor not shutdown"); @@ -118,7 +120,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { public void testSetExternalExecutor() throws ConcurrentException { final ExecutorService exec = Executors.newCachedThreadPool(); try { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.setExternalExecutor(exec); assertEquals(exec, init.getExternalExecutor(), "Wrong executor service"); assertTrue(init.start(), "Wrong result of start()"); @@ -137,7 +139,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testSetExternalExecutorAfterStart() throws ConcurrentException, InterruptedException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); final ExecutorService exec = Executors.newSingleThreadExecutor(); try { @@ -155,7 +157,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testStartMultipleTimes() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertTrue(init.start(), "Wrong result for start()"); for (int i = 0; i < 10; i++) { assertFalse(init.start(), "Could start again"); @@ -168,7 +170,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetBeforeStart() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertThrows(IllegalStateException.class, init::get); } @@ -178,7 +180,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetRuntimeException() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); final RuntimeException rex = new RuntimeException(); init.ex = rex; init.start(); @@ -192,7 +194,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testGetCheckedException() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); final Exception ex = new Exception(); init.ex = ex; init.start(); @@ -208,7 +210,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { @Test public void testGetInterruptedException() throws InterruptedException { final ExecutorService exec = Executors.newSingleThreadExecutor(); - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl( + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl( exec); final CountDownLatch latch1 = new CountDownLatch(1); init.shouldSleep = true; @@ -242,7 +244,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsStartedFalse() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); assertFalse(init.isStarted(), "Already started"); } @@ -251,7 +253,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsStartedTrue() { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); assertTrue(init.isStarted(), "Not started"); } @@ -261,7 +263,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsStartedAfterGet() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.start(); checkInitialize(init); assertTrue(init.isStarted(), "Not started"); @@ -272,7 +274,7 @@ public class BackgroundInitializerTest extends AbstractLangTest { */ @Test public void testIsInitialized() throws ConcurrentException { - final BackgroundInitializerTestImpl init = new BackgroundInitializerTestImpl(); + final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(); init.enableLatch(); init.start(); assertTrue(init.isStarted(), "Not started"); //Started and Initialized should return opposite values @@ -282,29 +284,37 @@ public class BackgroundInitializerTest extends AbstractLangTest { assertTrue(init.isInitialized(), "Not initalized after releasing latch"); } + protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl() { + return new MethodBackgroundInitializerTestImpl(); + } + + protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl(final ExecutorService exec) { + return new MethodBackgroundInitializerTestImpl(exec); + } + /** * A concrete implementation of BackgroundInitializer. It also overloads * some methods that simplify testing. */ - private static final class BackgroundInitializerTestImpl extends - BackgroundInitializer<Integer> { + 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; - /** 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() { + /** An object containing the state we are testing */ + CloseableCounter counter = new CloseableCounter(); + + AbstractBackgroundInitializerTestImpl() { } - BackgroundInitializerTestImpl(final ExecutorService exec) { + AbstractBackgroundInitializerTestImpl(final ExecutorService exec) { super(exec); } @@ -316,14 +326,17 @@ public class BackgroundInitializerTest extends AbstractLangTest { latch.countDown(); } + public CloseableCounter getCloseableCounter() { + return counter; + } + /** * Records this invocation. Optionally throws an exception or sleeps a * while. * * @throws Exception in case of an error */ - @Override - protected Integer initialize() throws Exception { + protected CloseableCounter initializeInternal() throws Exception { if (ex != null) { throw ex; } @@ -333,7 +346,47 @@ public class BackgroundInitializerTest extends AbstractLangTest { if (waitForLatch) { latch.await(); } - return Integer.valueOf(++initializeCalls); + return counter.increment(); + } + } + + protected static class MethodBackgroundInitializerTestImpl extends AbstractBackgroundInitializerTestImpl { + + MethodBackgroundInitializerTestImpl() { + } + + MethodBackgroundInitializerTestImpl(final ExecutorService exec) { + super(exec); + } + + @Override + protected CloseableCounter initialize() throws Exception { + return initializeInternal(); + } + } + + 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 CloseableCounter increment() { + initializeCalls.incrementAndGet(); + return this; + } + + public int getInitializeCalls() { + return initializeCalls.get(); + } + + public void close() { + closed.set(true); + } + + public boolean isClosed() { + return closed.get(); } } } diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java new file mode 100644 index 000000000..3bdaf0c77 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerCloserTest.java @@ -0,0 +1,54 @@ +/* + * 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.lang3.concurrent; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@code LazyInitializer}. + */ +public class LazyInitializerCloserTest extends AbstractConcurrentInitializerTest { + + private final AtomicBoolean closed = new AtomicBoolean(); + + /** + * Creates the initializer to be tested. This implementation returns the {@code LazyInitializer} created in the {@code setUp()} method. + * + * @return the initializer to be tested + */ + @Override + protected LazyInitializer<Object> createInitializer() { + return LazyInitializer.builder().setInitializer(Object::new).setCloser(e -> closed.set(true)).get(); + } + + @Test + public void testIsInitialized() throws ConcurrentException { + final LazyInitializer<Object> initializer = createInitializer(); + assertFalse(initializer.isInitialized()); + initializer.get(); + assertTrue(initializer.isInitialized()); + assertFalse(closed.get()); + initializer.close(); + assertTrue(closed.get()); + } + +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java new file mode 100644 index 000000000..c1e6137fc --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerFailableCloserTest.java @@ -0,0 +1,72 @@ +/* + * 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.lang3.concurrent; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +/** + * Tests {@code LazyInitializer}. + */ +public class LazyInitializerFailableCloserTest extends AbstractConcurrentInitializerTest { + + private final AtomicBoolean closed = new AtomicBoolean(); + + /** + * Creates the initializer to be tested. This implementation returns the {@code LazyInitializer} created in the {@code setUp()} method. + * + * @return the initializer to be tested + */ + @Override + protected LazyInitializer<Object> createInitializer() { + return LazyInitializer.builder().setInitializer(this::makeObject).setCloser(e -> throwingCloser()).get(); + } + + private Object makeObject() throws ConcurrentException { + if (closed.get()) { + // Doesn't actually throw, just for the method sig without unused warning. + throw new ConcurrentException("test", new IOException()); + } + return new Object(); + } + + @Test + public void testIsInitialized() throws ConcurrentException { + final LazyInitializer<Object> initializer = createInitializer(); + assertFalse(initializer.isInitialized()); + initializer.get(); + assertTrue(initializer.isInitialized()); + assertFalse(closed.get()); + initializer.close(); + assertTrue(closed.get()); + } + + private void throwingCloser() throws ConcurrentException { + closed.set(true); + // always false: + if (!closed.get()) { + // Doesn't actually throw, just for the method sig without unused warning. + throw new ConcurrentException("test", new IOException()); + } + } + +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java new file mode 100644 index 000000000..0ac26a4bf --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/LazyInitializerSupplierTest.java @@ -0,0 +1,43 @@ +/* + * 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.lang3.concurrent; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; + +/** + * Tests {@code LazyInitializer}. + */ +public class LazyInitializerSupplierTest extends AbstractConcurrentInitializerCloseAndExceptionsTest { + + /** + * Creates the initializer to be tested. + * + * @return the initializer to be tested + */ + @Override + protected ConcurrentInitializer<Object> createInitializer() { + return LazyInitializer.<Object>builder().setInitializer(() -> new Object()).get(); + } + + @Override + protected ConcurrentInitializer<CloseableObject> createInitializerThatThrowsException( + final FailableSupplier<CloseableObject, ? extends Exception> supplier, + final FailableConsumer<CloseableObject, ? extends Exception> closer) { + return LazyInitializer.<CloseableObject>builder().setInitializer(supplier).setCloser(closer).get(); + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java new file mode 100644 index 000000000..beca37ed5 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerSupplierTest.java @@ -0,0 +1,261 @@ +/* + * 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.lang3.concurrent; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.lang.reflect.Field; + +import org.apache.commons.lang3.function.FailableConsumer; +import org.apache.commons.lang3.function.FailableSupplier; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test class for {@link MultiBackgroundInitializer}. + */ +public class MultiBackgroundInitializerSupplierTest extends MultiBackgroundInitializerTest { + + private NullPointerException npe; + private IOException ioException; + private FailableConsumer<?, ?> ioExceptionConsumer; + private FailableConsumer<?, ?> nullPointerExceptionConsumer; + + @BeforeEach + public void setUpException() throws Exception { + npe = new NullPointerException(); + ioException = new IOException(); + ioExceptionConsumer = (CloseableCounter cc) -> { + throw ioException; + }; + nullPointerExceptionConsumer = (CloseableCounter cc) -> { + throw npe; + }; + } + + /** + * {@inheritDoc} + */ + @Override + protected AbstractChildBackgroundInitializer createChildBackgroundInitializer() { + return new SupplierChildBackgroundInitializer(); + } + + /** + * Tests that close() method closes the wrapped object + * + * @throws Exception + */ + @Test + public void testClose() + throws ConcurrentException, InterruptedException { + final AbstractChildBackgroundInitializer childOne = createChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer childTwo = createChildBackgroundInitializer(); + + assertFalse(initializer.isInitialized(), "Initalized without having anything to initalize"); + + initializer.addInitializer("child one", childOne); + initializer.addInitializer("child two", childTwo); + + assertFalse(childOne.getCloseableCounter().isClosed(), "child one closed() succeeded before start()"); + assertFalse(childTwo.getCloseableCounter().isClosed(), "child two closed() succeeded before start()"); + + 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(childOne.getCloseableCounter().isClosed(), "child one close() succeeded after start() but before close()"); + assertFalse(childTwo.getCloseableCounter().isClosed(), "child two close() succeeded after start() but before close()"); + + childOne.get(); // ensure this child finishes initializing + childTwo.get(); // ensure this child finishes initializing + + assertFalse(childOne.getCloseableCounter().isClosed(), "child one initializing succeeded after start() but before close()"); + assertFalse(childTwo.getCloseableCounter().isClosed(), "child two initializing succeeded after start() but before close()"); + + try { + initializer.close(); + } catch (Exception e) { + fail(); + } + + assertTrue(childOne.getCloseableCounter().isClosed(), "child one close() did not succeed"); + assertTrue(childOne.getCloseableCounter().isClosed(), "child two close() did not succeed"); + } + + /** + * Tests that close() wraps a checked exception from a child initializer in an ConcurrentException as the first suppressed under in an ConcurrentException + * + * @throws Exception + */ + @Test + public void testCloseWithCheckedException() throws Exception { + final AbstractChildBackgroundInitializer childOne = new SupplierChildBackgroundInitializer(ioExceptionConsumer); + + initializer.addInitializer("child one", childOne); + initializer.start(); + + long startTime = System.currentTimeMillis(); + long waitTime = 3000; + long endTime = startTime + waitTime; + //wait for the children to start + while (! childOne.isStarted()) { + if (System.currentTimeMillis() > endTime) { + fail("children never started"); + Thread.sleep(PERIOD_MILLIS); + } + } + + childOne.get(); // ensure the Future has completed. + try { + initializer.close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertSame(ioException, e.getSuppressed()[0]); + } + } + + /** + * Tests that close() wraps a runtime exception from a child initializer as the first suppressed under in an ConcurrentException + * + * @throws Exception + */ + @Test + public void testCloseWithRuntimeException() throws Exception { + final AbstractChildBackgroundInitializer childOne = new SupplierChildBackgroundInitializer(nullPointerExceptionConsumer); + + initializer.addInitializer("child one", childOne); + initializer.start(); + + long startTime = System.currentTimeMillis(); + long waitTime = 3000; + long endTime = startTime + waitTime; + //wait for the children to start + while (! childOne.isStarted()) { + if (System.currentTimeMillis() > endTime) { + fail("children never started"); + Thread.sleep(PERIOD_MILLIS); + } + } + + childOne.get(); // ensure the Future has completed. + try { + initializer.close(); + fail(); + } catch (Exception e) { + assertThat(e, instanceOf(ConcurrentException.class)); + assertSame(npe, e.getSuppressed()[0]); + } + } + + /** + * Tests that calling close() on a MultiBackgroundInitializer with two children that both throw exceptions throws + * an ConcurrentException and both the child exceptions are present + * + * @throws Exception + */ + @Test + public void testCloseWithTwoExceptions() + throws ConcurrentException, InterruptedException { + + final AbstractChildBackgroundInitializer childOne = new SupplierChildBackgroundInitializer(ioExceptionConsumer); + final AbstractChildBackgroundInitializer childTwo = new SupplierChildBackgroundInitializer(nullPointerExceptionConsumer); + + 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); + } + } + + childOne.get(); // ensure this child finishes initializing + childTwo.get(); // ensure this child finishes initializing + + try { + initializer.close(); + fail(); + } catch (Exception e) { + // We don't actually know which order the children will be closed in + boolean foundChildOneException = false; + boolean foundChildTwoException = false; + + for (Throwable t : e.getSuppressed()) { + if (t.equals(ioException)) { + foundChildOneException = true; + } + if (t.equals(npe)) { + foundChildTwoException = true; + } + } + + assertTrue(foundChildOneException); + assertTrue(foundChildTwoException); + } + } + + /** + * A concrete implementation of {@code BackgroundInitializer} used for + * defining background tasks for {@code MultiBackgroundInitializer}. + */ + private static final class SupplierChildBackgroundInitializer extends AbstractChildBackgroundInitializer { + + SupplierChildBackgroundInitializer() { + this((CloseableCounter cc) -> cc.close()); + } + + SupplierChildBackgroundInitializer(FailableConsumer<?, ?> consumer) { + try { + // Use reflection here because the constructors we need are private + final FailableSupplier<?, ?> supplier = () -> initializeInternal(); + final Field initializer = AbstractConcurrentInitializer.class.getDeclaredField("initializer"); + initializer.setAccessible(true); + initializer.set(this, supplier); + + final Field closer = AbstractConcurrentInitializer.class.getDeclaredField("closer"); + closer.setAccessible(true); + closer.set(this, consumer); + } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { + fail(); + } + } + } +} diff --git a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java index f861e0302..4260f9b61 100644 --- a/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java +++ b/src/test/java/org/apache/commons/lang3/concurrent/MultiBackgroundInitializerTest.java @@ -42,10 +42,10 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { private static final String CHILD_INIT = "childInitializer"; /** The initializer to be tested. */ - private MultiBackgroundInitializer initializer; + protected MultiBackgroundInitializer initializer; /** A short time to wait for background threads to run. */ - private static final long PERIOD_MILLIS = 50; + protected static final long PERIOD_MILLIS = 50; @BeforeEach public void setUp() { @@ -63,8 +63,8 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ private void checkChild(final BackgroundInitializer<?> child, final ExecutorService expExec) throws ConcurrentException { - final ChildBackgroundInitializer cinit = (ChildBackgroundInitializer) child; - final Integer result = cinit.get(); + 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) { @@ -78,7 +78,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ @Test public void testAddInitializerNullName() { - assertThrows(NullPointerException.class, () -> initializer.addInitializer(null, new ChildBackgroundInitializer())); + assertThrows(NullPointerException.class, () -> initializer.addInitializer(null, createChildBackgroundInitializer())); } /** @@ -117,7 +117,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { final int count = 5; for (int i = 0; i < count; i++) { initializer.addInitializer(CHILD_INIT + i, - new ChildBackgroundInitializer()); + createChildBackgroundInitializer()); } initializer.start(); final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer @@ -126,7 +126,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { for (int i = 0; i < count; i++) { final String key = CHILD_INIT + i; assertTrue(res.initializerNames().contains(key), "Name not found: " + key); - assertEquals(Integer.valueOf(1), res.getResultObject(key), "Wrong result object"); + 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()); @@ -175,8 +175,8 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { final String initExec = "childInitializerWithExecutor"; final ExecutorService exec = Executors.newSingleThreadExecutor(); try { - final ChildBackgroundInitializer c1 = new ChildBackgroundInitializer(); - final ChildBackgroundInitializer c2 = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer c1 = createChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer c2 = createChildBackgroundInitializer(); c2.setExternalExecutor(exec); initializer.addInitializer(CHILD_INIT, c1); initializer.addInitializer(initExec, c2); @@ -201,7 +201,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { initializer.start(); assertThrows( IllegalStateException.class, - () -> initializer.addInitializer(CHILD_INIT, new ChildBackgroundInitializer()), + () -> initializer.addInitializer(CHILD_INIT, createChildBackgroundInitializer()), "Could add initializer after start()!"); initializer.get(); } @@ -275,7 +275,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitializeRuntimeEx() { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); child.ex = new RuntimeException(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); @@ -291,7 +291,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { */ @Test public void testInitializeEx() throws ConcurrentException { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); child.ex = new Exception(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); @@ -312,7 +312,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { @Test public void testInitializeResultsIsSuccessfulTrue() throws ConcurrentException { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer @@ -329,7 +329,7 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { @Test public void testInitializeResultsIsSuccessfulFalse() throws ConcurrentException { - final ChildBackgroundInitializer child = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer(); child.ex = new Exception(); initializer.addInitializer(CHILD_INIT, child); initializer.start(); @@ -348,13 +348,13 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { public void testInitializeNested() throws ConcurrentException { final String nameMulti = "multiChildInitializer"; initializer - .addInitializer(CHILD_INIT, new ChildBackgroundInitializer()); + .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, - new ChildBackgroundInitializer()); + createChildBackgroundInitializer()); } initializer.addInitializer(nameMulti, mi2); initializer.start(); @@ -374,8 +374,8 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { @Test public void testIsInitialized() throws ConcurrentException, InterruptedException { - final ChildBackgroundInitializer childOne = new ChildBackgroundInitializer(); - final ChildBackgroundInitializer childTwo = new ChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer childOne = createChildBackgroundInitializer(); + final AbstractChildBackgroundInitializer childTwo = createChildBackgroundInitializer(); childOne.enableLatch(); childTwo.enableLatch(); @@ -409,14 +409,28 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { } /** - * A concrete implementation of {@code BackgroundInitializer} used for + * An overrideable method to create concrete implementations of + * {@code BackgroundInitializer} used for defining background tasks + * for {@code MultiBackgroundInitializer}. + */ + protected AbstractChildBackgroundInitializer createChildBackgroundInitializer() { + return new MethodChildBackgroundInitializer(); + } + + /** + * 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. */ - private static final class ChildBackgroundInitializer extends - BackgroundInitializer<Integer> { + 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; @@ -435,23 +449,76 @@ public class MultiBackgroundInitializerTest extends AbstractLangTest { latch.countDown(); } + public CloseableCounter getCloseableCounter() { + return counter; + } + /** * Records this invocation. Optionally throws an exception. */ - @Override - protected Integer initialize() throws Exception { - currentExecutor = getActiveExecutor(); + protected CloseableCounter initializeInternal() throws Exception { initializeCalls++; + currentExecutor = getActiveExecutor(); + + if (waitForLatch) { + latch.await(); + } if (ex != null) { throw ex; } - if (waitForLatch) { - latch.await(); + return counter.increment(); + } + } + + protected static class MethodChildBackgroundInitializer extends AbstractChildBackgroundInitializer { + @Override + protected CloseableCounter initialize() throws Exception { + return initializeInternal(); + } + } + + protected static class CloseableCounter { + /** The number of invocations of initialize(). */ + volatile int initializeCalls; + + /** Has the close consumer successfully reached this object. */ + volatile boolean closed; + + public CloseableCounter increment() { + initializeCalls++; + return this; + } + + public int getInitializeCalls() { + return initializeCalls; + } + + public CloseableCounter setInitializeCalls(int i) { + initializeCalls = i; + return this; + } + + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } + + @Override + public boolean equals(final Object other) { + if (other instanceof CloseableCounter) { + return initializeCalls == ((CloseableCounter) other).getInitializeCalls(); } + return false; + } - return Integer.valueOf(initializeCalls); + // A convenience for testing that a CloseableCounter typed as Object has a specific initializeCalls value + public static CloseableCounter wrapInteger(int i) { + return new CloseableCounter().setInitializeCalls(i); } } }