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