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