View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.concurrent;
18  
19  import static org.apache.commons.lang3.LangAssertions.assertNullPointerException;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertFalse;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  import static org.junit.jupiter.api.Assertions.fail;
26  
27  import java.util.Iterator;
28  import java.util.NoSuchElementException;
29  import java.util.concurrent.CountDownLatch;
30  import java.util.concurrent.ExecutorService;
31  import java.util.concurrent.Executors;
32  import java.util.concurrent.TimeUnit;
33  
34  import org.apache.commons.lang3.AbstractLangTest;
35  import org.junit.jupiter.api.BeforeEach;
36  import org.junit.jupiter.api.Test;
37  
38  /**
39   * Test class for {@link MultiBackgroundInitializer}.
40   */
41  class MultiBackgroundInitializerTest extends AbstractLangTest {
42      /**
43       * A mostly complete implementation of {@code BackgroundInitializer} used for
44       * defining background tasks for {@code MultiBackgroundInitializer}.
45       *
46       * Subclasses will contain the initializer, either as an method implementation
47       * or by using a supplier.
48       */
49      protected static class AbstractChildBackgroundInitializer extends BackgroundInitializer<CloseableCounter> {
50          /** Stores the current executor service. */
51          volatile ExecutorService currentExecutor;
52  
53          /** An object containing the state we are testing */
54          CloseableCounter counter = new CloseableCounter();
55  
56          /** A counter for the invocations of initialize(). */
57          volatile int initializeCalls;
58  
59          /** An exception to be thrown by initialize(). */
60          Exception ex;
61  
62          /** A latch tests can use to control when initialize completes. */
63          final CountDownLatch latch = new CountDownLatch(1);
64          boolean waitForLatch;
65  
66          public void enableLatch() {
67              waitForLatch = true;
68          }
69  
70          public CloseableCounter getCloseableCounter() {
71              return counter;
72          }
73  
74          /**
75           * Records this invocation. Optionally throws an exception.
76           */
77          protected CloseableCounter initializeInternal() throws Exception {
78              initializeCalls++;
79              currentExecutor = getActiveExecutor();
80  
81              if (waitForLatch) {
82                  latch.await();
83              }
84  
85              if (ex != null) {
86                  throw ex;
87              }
88  
89              return counter.increment();
90          }
91  
92          public void releaseLatch() {
93              latch.countDown();
94          }
95      }
96  
97      protected static class CloseableCounter {
98          // A convenience for testing that a CloseableCounter typed as Object has a specific initializeCalls value
99          public static CloseableCounter wrapInteger(final int i) {
100             return new CloseableCounter().setInitializeCalls(i);
101         }
102 
103         /** The number of invocations of initialize(). */
104         volatile int initializeCalls;
105 
106         /** Has the close consumer successfully reached this object. */
107         volatile boolean closed;
108 
109         public void close() {
110             closed = true;
111         }
112 
113         @Override
114         public boolean equals(final Object other) {
115             if (other instanceof CloseableCounter) {
116                 return initializeCalls == ((CloseableCounter) other).getInitializeCalls();
117             }
118             return false;
119         }
120 
121         public int getInitializeCalls() {
122             return initializeCalls;
123         }
124 
125         @Override
126         public int hashCode() {
127             return initializeCalls;
128         }
129 
130         public CloseableCounter increment() {
131             initializeCalls++;
132             return this;
133         }
134 
135         public boolean isClosed() {
136             return closed;
137         }
138 
139         public CloseableCounter setInitializeCalls(final int i) {
140             initializeCalls = i;
141             return this;
142         }
143     }
144 
145     protected static class MethodChildBackgroundInitializer extends AbstractChildBackgroundInitializer {
146         @Override
147         protected CloseableCounter initialize() throws Exception {
148             return initializeInternal();
149         }
150     }
151 
152     /** Constant for the names of the child initializers. */
153     private static final String CHILD_INIT = "childInitializer";
154 
155     /** A short time to wait for background threads to run. */
156     protected static final long PERIOD_MILLIS = 50;
157 
158     /** The initializer to be tested. */
159     protected MultiBackgroundInitializer initializer;
160 
161     /**
162      * Tests whether a child initializer has been executed. Optionally the
163      * expected executor service can be checked, too.
164      *
165      * @param child the child initializer
166      * @param expExec the expected executor service (null if the executor should
167      * not be checked)
168      * @throws ConcurrentException if an error occurs
169      */
170     private void checkChild(final BackgroundInitializer<?> child,
171             final ExecutorService expExec) throws ConcurrentException {
172         final AbstractChildBackgroundInitializer cinit = (AbstractChildBackgroundInitializer) child;
173         final Integer result = cinit.get().getInitializeCalls();
174         assertEquals(1, result.intValue(), "Wrong result");
175         assertEquals(1, cinit.initializeCalls, "Wrong number of executions");
176         if (expExec != null) {
177             assertEquals(expExec, cinit.currentExecutor, "Wrong executor service");
178         }
179     }
180 
181     /**
182      * Helper method for testing the initialize() method. This method can
183      * operate with both an external and a temporary executor service.
184      *
185      * @return the result object produced by the initializer
186      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
187      */
188     private MultiBackgroundInitializer.MultiBackgroundInitializerResults checkInitialize()
189             throws ConcurrentException {
190         final int count = 5;
191         for (int i = 0; i < count; i++) {
192             initializer.addInitializer(CHILD_INIT + i,
193                     createChildBackgroundInitializer());
194         }
195         initializer.start();
196         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
197                 .get();
198         assertEquals(count, res.initializerNames().size(), "Wrong number of child initializers");
199         for (int i = 0; i < count; i++) {
200             final String key = CHILD_INIT + i;
201             assertTrue(res.initializerNames().contains(key), "Name not found: " + key);
202             assertEquals(CloseableCounter.wrapInteger(1), res.getResultObject(key), "Wrong result object");
203             assertFalse(res.isException(key), "Exception flag");
204             assertNull(res.getException(key), "Got an exception");
205             checkChild(res.getInitializer(key), initializer.getActiveExecutor());
206         }
207         return res;
208     }
209 
210     /**
211      * An overrideable method to create concrete implementations of
212      * {@code BackgroundInitializer} used for defining background tasks
213      * for {@code MultiBackgroundInitializer}.
214      */
215     protected AbstractChildBackgroundInitializer createChildBackgroundInitializer() {
216         return new MethodChildBackgroundInitializer();
217     }
218 
219     @BeforeEach
220     public void setUp() {
221         initializer = new MultiBackgroundInitializer();
222     }
223 
224     /**
225      * Tries to add another child initializer after the start() method has been
226      * called. This should not be allowed.
227      *
228      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
229      */
230     @Test
231     void testAddInitializerAfterStart() throws ConcurrentException {
232         initializer.start();
233         assertThrows(IllegalStateException.class, () -> initializer.addInitializer(CHILD_INIT, createChildBackgroundInitializer()),
234                 "Could add initializer after start()!");
235         initializer.get();
236     }
237 
238     /**
239      * Tests addInitializer() if a null initializer is passed in. This should
240      * cause an exception.
241      */
242     @Test
243     void testAddInitializerNullInit() {
244         assertNullPointerException(() -> initializer.addInitializer(CHILD_INIT, null));
245     }
246 
247     /**
248      * Tests addInitializer() if a null name is passed in. This should cause an
249      * exception.
250      */
251     @Test
252     void testAddInitializerNullName() {
253         assertNullPointerException(() -> initializer.addInitializer(null, createChildBackgroundInitializer()));
254     }
255 
256     /**
257      * Tests the behavior of initialize() if a child initializer has a specific
258      * executor service. Then this service should not be overridden.
259      *
260      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
261      */
262     @Test
263     void testInitializeChildWithExecutor() throws ConcurrentException, InterruptedException {
264         final String initExec = "childInitializerWithExecutor";
265         final ExecutorService exec = Executors.newSingleThreadExecutor();
266         try {
267             final AbstractChildBackgroundInitializer c1 = createChildBackgroundInitializer();
268             final AbstractChildBackgroundInitializer c2 = createChildBackgroundInitializer();
269             c2.setExternalExecutor(exec);
270             initializer.addInitializer(CHILD_INIT, c1);
271             initializer.addInitializer(initExec, c2);
272             initializer.start();
273             initializer.get();
274             checkChild(c1, initializer.getActiveExecutor());
275             checkChild(c2, exec);
276         } finally {
277             exec.shutdown();
278             exec.awaitTermination(1, TimeUnit.SECONDS);
279         }
280     }
281 
282     /**
283      * Tests the behavior of the initializer if one of the child initializers
284      * throws a checked exception.
285      *
286      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
287      */
288     @Test
289     void testInitializeEx() throws ConcurrentException {
290         final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
291         child.ex = new Exception();
292         initializer.addInitializer(CHILD_INIT, child);
293         initializer.start();
294         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
295                 .get();
296         assertTrue(res.isException(CHILD_INIT), "No exception flag");
297         assertNull(res.getResultObject(CHILD_INIT), "Got a results object");
298         final ConcurrentException cex = res.getException(CHILD_INIT);
299         assertEquals(child.ex, cex.getCause(), "Wrong cause");
300     }
301 
302     /**
303      * Tests background processing if an external executor service is provided.
304      *
305      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
306      */
307     @Test
308     void testInitializeExternalExec() throws ConcurrentException, InterruptedException {
309         final ExecutorService exec = Executors.newCachedThreadPool();
310         try {
311             initializer = new MultiBackgroundInitializer(exec);
312             checkInitialize();
313             assertEquals(exec, initializer.getActiveExecutor(), "Wrong executor");
314             assertFalse(exec.isShutdown(), "Executor was shutdown");
315         } finally {
316             exec.shutdown();
317             exec.awaitTermination(1, TimeUnit.SECONDS);
318         }
319     }
320 
321     /**
322      * Tests whether MultiBackgroundInitializers can be combined in a nested
323      * way.
324      *
325      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
326      */
327     @Test
328     void testInitializeNested() throws ConcurrentException {
329         final String nameMulti = "multiChildInitializer";
330         initializer
331                 .addInitializer(CHILD_INIT, createChildBackgroundInitializer());
332         final MultiBackgroundInitializer mi2 = new MultiBackgroundInitializer();
333         final int count = 3;
334         for (int i = 0; i < count; i++) {
335             mi2
336                     .addInitializer(CHILD_INIT + i,
337                             createChildBackgroundInitializer());
338         }
339         initializer.addInitializer(nameMulti, mi2);
340         initializer.start();
341         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
342                 .get();
343         final ExecutorService exec = initializer.getActiveExecutor();
344         checkChild(res.getInitializer(CHILD_INIT), exec);
345         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res2 = (MultiBackgroundInitializer.MultiBackgroundInitializerResults) res
346                 .getResultObject(nameMulti);
347         assertEquals(count, res2.initializerNames().size(), "Wrong number of initializers");
348         for (int i = 0; i < count; i++) {
349             checkChild(res2.getInitializer(CHILD_INIT + i), exec);
350         }
351         assertTrue(exec.isShutdown(), "Executor not shutdown");
352     }
353 
354     /**
355      * Tests the background processing if there are no child initializers.
356      *
357      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
358      */
359     @Test
360     void testInitializeNoChildren() throws ConcurrentException {
361         assertTrue(initializer.start(), "Wrong result of start()");
362         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
363                 .get();
364         assertTrue(res.initializerNames().isEmpty(), "Got child initializers");
365         assertTrue(initializer.getActiveExecutor().isShutdown(), "Executor not shutdown");
366     }
367 
368     /**
369      * Tests the isSuccessful() method of the result object if at least one
370      * child initializer has thrown an exception.
371      *
372      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
373      */
374     @Test
375     void testInitializeResultsIsSuccessfulFalse()
376             throws ConcurrentException {
377         final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
378         child.ex = new Exception();
379         initializer.addInitializer(CHILD_INIT, child);
380         initializer.start();
381         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
382                 .get();
383         assertFalse(res.isSuccessful(), "Wrong success flag");
384     }
385 
386     /**
387      * Tests the isSuccessful() method of the result object if no child
388      * initializer has thrown an exception.
389      *
390      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
391      */
392     @Test
393     void testInitializeResultsIsSuccessfulTrue()
394             throws ConcurrentException {
395         final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
396         initializer.addInitializer(CHILD_INIT, child);
397         initializer.start();
398         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
399                 .get();
400         assertTrue(res.isSuccessful(), "Wrong success flag");
401     }
402 
403     /**
404      * Tests the behavior of the initializer if one of the child initializers
405      * throws a runtime exception.
406      */
407     @Test
408     void testInitializeRuntimeEx() {
409         final AbstractChildBackgroundInitializer child = createChildBackgroundInitializer();
410         child.ex = new RuntimeException();
411         initializer.addInitializer(CHILD_INIT, child);
412         initializer.start();
413         final Exception ex = assertThrows(Exception.class, initializer::get);
414         assertEquals(child.ex, ex, "Wrong exception");
415     }
416 
417     /**
418      * Tests background processing if a temporary executor is used.
419      *
420      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
421      */
422     @Test
423     void testInitializeTempExec() throws ConcurrentException {
424         checkInitialize();
425         assertTrue(initializer.getActiveExecutor().isShutdown(), "Executor not shutdown");
426     }
427 
428     @Test
429     void testIsInitialized()
430             throws ConcurrentException, InterruptedException {
431         final AbstractChildBackgroundInitializer childOne = createChildBackgroundInitializer();
432         final AbstractChildBackgroundInitializer childTwo = createChildBackgroundInitializer();
433 
434         childOne.enableLatch();
435         childTwo.enableLatch();
436 
437         assertFalse(initializer.isInitialized(), "Initialized without having anything to initialize");
438 
439         initializer.addInitializer("child one", childOne);
440         initializer.addInitializer("child two", childTwo);
441         initializer.start();
442 
443         final long startTime = System.currentTimeMillis();
444         final long waitTime = 3000;
445         final long endTime = startTime + waitTime;
446         //wait for the children to start
447         while (! childOne.isStarted() || ! childTwo.isStarted()) {
448             if (System.currentTimeMillis() > endTime) {
449                 fail("children never started");
450                 Thread.sleep(PERIOD_MILLIS);
451             }
452         }
453 
454         assertFalse(initializer.isInitialized(), "Initialized with two children running");
455 
456         childOne.releaseLatch();
457         childOne.get(); //ensure this child finishes initializing
458         assertFalse(initializer.isInitialized(), "Initialized with one child running");
459 
460         childTwo.releaseLatch();
461         childTwo.get(); //ensure this child finishes initializing
462         assertTrue(initializer.isInitialized(), "Not initialized with no children running");
463     }
464 
465     /**
466      * Tries to query the exception of an unknown child initializer from the
467      * results object. This should cause an exception.
468      *
469      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
470      */
471     @Test
472     void testResultGetExceptionUnknown() throws ConcurrentException {
473         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
474         assertThrows(NoSuchElementException.class, () -> res.getException("unknown"));
475     }
476 
477     /**
478      * Tries to query an unknown child initializer from the results object. This
479      * should cause an exception.
480      *
481      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
482      */
483     @Test
484     void testResultGetInitializerUnknown() throws ConcurrentException {
485         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
486         assertThrows(NoSuchElementException.class, () -> res.getInitializer("unknown"));
487     }
488 
489     /**
490      * Tries to query the results of an unknown child initializer from the
491      * results object. This should cause an exception.
492      *
493      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
494      */
495     @Test
496     void testResultGetResultObjectUnknown() throws ConcurrentException {
497         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
498         assertThrows(NoSuchElementException.class, () -> res.getResultObject("unknown"));
499     }
500 
501     /**
502      * Tests that the set with the names of the initializers cannot be modified.
503      *
504      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
505      */
506     @Test
507     void testResultInitializerNamesModify() throws ConcurrentException {
508         checkInitialize();
509         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = initializer
510                 .get();
511         final Iterator<String> it = res.initializerNames().iterator();
512         it.next();
513         assertThrows(UnsupportedOperationException.class, it::remove);
514     }
515 
516     /**
517      * Tries to query the exception flag of an unknown child initializer from
518      * the results object. This should cause an exception.
519      *
520      * @throws org.apache.commons.lang3.concurrent.ConcurrentException so we don't have to catch it
521      */
522     @Test
523     void testResultIsExceptionUnknown() throws ConcurrentException {
524         final MultiBackgroundInitializer.MultiBackgroundInitializerResults res = checkInitialize();
525         assertThrows(NoSuchElementException.class, () -> res.isException("unknown"));
526     }
527 }