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.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertSame;
24  import static org.junit.jupiter.api.Assertions.assertThrows;
25  import static org.junit.jupiter.api.Assertions.assertTrue;
26  
27  import java.time.Duration;
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  import java.util.concurrent.atomic.AtomicBoolean;
33  import java.util.concurrent.atomic.AtomicInteger;
34  import java.util.concurrent.atomic.AtomicReference;
35  
36  import org.apache.commons.lang3.AbstractLangTest;
37  import org.apache.commons.lang3.ThreadUtils;
38  import org.junit.jupiter.api.Test;
39  
40  class BackgroundInitializerTest extends AbstractLangTest {
41      /**
42       * A concrete implementation of BackgroundInitializer. It also overloads
43       * some methods that simplify testing.
44       */
45      protected static class AbstractBackgroundInitializerTestImpl extends
46              BackgroundInitializer<CloseableCounter> {
47          /** An exception to be thrown by initialize(). */
48          Exception ex;
49  
50          /** A flag whether the background task should sleep a while. */
51          boolean shouldSleep;
52  
53          /** A latch tests can use to control when initialize completes. */
54          final CountDownLatch latch = new CountDownLatch(1);
55          boolean waitForLatch;
56  
57          /** An object containing the state we are testing */
58          CloseableCounter counter = new CloseableCounter();
59  
60          AbstractBackgroundInitializerTestImpl() {
61          }
62  
63          AbstractBackgroundInitializerTestImpl(final ExecutorService exec) {
64              super(exec);
65          }
66  
67          public void enableLatch() {
68              waitForLatch = true;
69          }
70  
71          public CloseableCounter getCloseableCounter() {
72              return counter;
73          }
74  
75          /**
76           * Records this invocation. Optionally throws an exception or sleeps a
77           * while.
78           *
79           * @throws Exception in case of an error
80           */
81          protected CloseableCounter initializeInternal() throws Exception {
82              if (ex != null) {
83                  throw ex;
84              }
85              if (shouldSleep) {
86                  ThreadUtils.sleep(Duration.ofMinutes(1));
87              }
88              if (waitForLatch) {
89                  latch.await();
90              }
91              return counter.increment();
92          }
93  
94          public void releaseLatch() {
95              latch.countDown();
96          }
97      }
98  
99      protected static class CloseableCounter {
100         /** The number of invocations of initialize(). */
101         AtomicInteger initializeCalls = new AtomicInteger();
102 
103         /** Has the close consumer successfully reached this object. */
104         AtomicBoolean closed = new AtomicBoolean();
105 
106         public void close() {
107             closed.set(true);
108         }
109 
110         public int getInitializeCalls() {
111             return initializeCalls.get();
112         }
113 
114         public CloseableCounter increment() {
115             initializeCalls.incrementAndGet();
116             return this;
117         }
118 
119         public boolean isClosed() {
120             return closed.get();
121         }
122     }
123 
124     protected static class MethodBackgroundInitializerTestImpl extends AbstractBackgroundInitializerTestImpl {
125 
126         MethodBackgroundInitializerTestImpl() {
127         }
128 
129         MethodBackgroundInitializerTestImpl(final ExecutorService exec) {
130             super(exec);
131         }
132 
133         @Override
134         protected CloseableCounter initialize() throws Exception {
135             return initializeInternal();
136         }
137     }
138 
139     /**
140      * Helper method for checking whether the initialize() method was correctly
141      * called. start() must already have been invoked.
142      *
143      * @param init the initializer to test
144      */
145     private void checkInitialize(final AbstractBackgroundInitializerTestImpl init) throws ConcurrentException {
146         final Integer result = init.get().getInitializeCalls();
147         assertEquals(1, result.intValue(), "Wrong result");
148         assertEquals(1, init.getCloseableCounter().getInitializeCalls(), "Wrong number of invocations");
149         assertNotNull(init.getFuture(), "No future");
150     }
151 
152     protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl() {
153         return new MethodBackgroundInitializerTestImpl();
154     }
155 
156     protected AbstractBackgroundInitializerTestImpl getBackgroundInitializerTestImpl(final ExecutorService exec) {
157         return new MethodBackgroundInitializerTestImpl(exec);
158     }
159 
160     @Test
161     void testBuilder() throws ConcurrentException {
162         // @formatter:off
163         final BackgroundInitializer<Object> backgroundInitializer = BackgroundInitializer.builder()
164             .setCloser(null)
165             .setExternalExecutor(null)
166             .setInitializer(null)
167             .get();
168         // @formatter:on
169         assertNull(backgroundInitializer.getExternalExecutor());
170         assertFalse(backgroundInitializer.isInitialized());
171         assertFalse(backgroundInitializer.isStarted());
172         assertThrows(IllegalStateException.class, backgroundInitializer::getFuture);
173     }
174 
175     @Test
176     void testBuilderThenGetFailures() throws ConcurrentException {
177         // @formatter:off
178         final BackgroundInitializer<Object> backgroundInitializer = BackgroundInitializer.builder()
179             .setCloser(null)
180             .setExternalExecutor(null)
181             .setInitializer(() -> {
182                 throw new IllegalStateException("test");
183             })
184             .get();
185         // @formatter:on
186         assertNull(backgroundInitializer.getExternalExecutor());
187         assertFalse(backgroundInitializer.isInitialized());
188         assertFalse(backgroundInitializer.isStarted());
189         assertThrows(IllegalStateException.class, backgroundInitializer::getFuture);
190         // start
191         backgroundInitializer.start();
192         assertEquals("test", assertThrows(IllegalStateException.class, backgroundInitializer::get).getMessage());
193     }
194 
195     /**
196      * Tries to obtain the executor before start(). It should not have been
197      * initialized yet.
198      */
199     @Test
200     void testGetActiveExecutorBeforeStart() {
201         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
202         assertNull(init.getActiveExecutor(), "Got an executor");
203     }
204 
205     /**
206      * Tests whether an external executor is correctly detected.
207      */
208     @Test
209     void testGetActiveExecutorExternal() throws InterruptedException, ConcurrentException {
210         final ExecutorService exec = Executors.newSingleThreadExecutor();
211         try {
212             final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(exec);
213             init.start();
214             assertSame(exec, init.getActiveExecutor(), "Wrong executor");
215             checkInitialize(init);
216         } finally {
217             exec.shutdown();
218             exec.awaitTermination(1, TimeUnit.SECONDS);
219         }
220     }
221 
222     /**
223      * Tests getActiveExecutor() for a temporary executor.
224      */
225     @Test
226     void testGetActiveExecutorTemp() throws ConcurrentException {
227         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
228         init.start();
229         assertNotNull(init.getActiveExecutor(), "No active executor");
230         checkInitialize(init);
231     }
232 
233     /**
234      * Tests calling get() before start(). This should cause an exception.
235      */
236     @Test
237     void testGetBeforeStart() {
238         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
239         assertThrows(IllegalStateException.class, init::get);
240     }
241 
242     /**
243      * Tests the get() method if background processing causes a checked
244      * exception.
245      */
246     @Test
247     void testGetCheckedException() {
248         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
249         final Exception ex = new Exception();
250         init.ex = ex;
251         init.start();
252         final ConcurrentException cex = assertThrows(ConcurrentException.class, init::get);
253         assertEquals(ex, cex.getCause(), "Exception not thrown");
254     }
255 
256     /**
257      * Tests the get() method if waiting for the initialization is interrupted.
258      *
259      * @throws InterruptedException because we're making use of Java's concurrent API
260      */
261     @Test
262     void testGetInterruptedException() throws InterruptedException {
263         final ExecutorService exec = Executors.newSingleThreadExecutor();
264         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl(
265                 exec);
266         final CountDownLatch latch1 = new CountDownLatch(1);
267         init.shouldSleep = true;
268         init.start();
269         final AtomicReference<InterruptedException> iex = new AtomicReference<>();
270         final Thread getThread = new Thread() {
271             @Override
272             public void run() {
273                 try {
274                     init.get();
275                 } catch (final ConcurrentException cex) {
276                     if (cex.getCause() instanceof InterruptedException) {
277                         iex.set((InterruptedException) cex.getCause());
278                     }
279                 } finally {
280                     assertTrue(isInterrupted(), "Thread not interrupted");
281                     latch1.countDown();
282                 }
283             }
284         };
285         getThread.start();
286         getThread.interrupt();
287         latch1.await();
288         exec.shutdownNow();
289         exec.awaitTermination(1, TimeUnit.SECONDS);
290         assertNotNull(iex.get(), "No interrupted exception");
291     }
292 
293     /**
294      * Tests the get() method if background processing causes a runtime
295      * exception.
296      */
297     @Test
298     void testGetRuntimeException() {
299         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
300         final RuntimeException rex = new RuntimeException();
301         init.ex = rex;
302         init.start();
303         final Exception ex = assertThrows(Exception.class, init::get);
304         assertEquals(rex, ex, "Runtime exception not thrown");
305     }
306 
307     /**
308      * Tests whether initialize() is invoked.
309      */
310     @Test
311     void testInitialize() throws ConcurrentException {
312         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
313         init.start();
314         checkInitialize(init);
315     }
316 
317     /**
318      * Tests the execution of the background task if a temporary executor has to
319      * be created.
320      */
321     @Test
322     void testInitializeTempExecutor() throws ConcurrentException {
323         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
324         assertTrue(init.start(), "Wrong result of start()");
325         checkInitialize(init);
326         assertTrue(init.getActiveExecutor().isShutdown(), "Executor not shutdown");
327     }
328 
329     /**
330      * Tests isInitialized() before and after the background task has finished.
331      */
332     @Test
333     void testIsInitialized() throws ConcurrentException {
334         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
335         init.enableLatch();
336         init.start();
337         assertTrue(init.isStarted(), "Not started"); //Started and Initialized should return opposite values
338         assertFalse(init.isInitialized(), "Initialized before releasing latch");
339         init.releaseLatch();
340         init.get(); //to ensure the initialize thread has completed.
341         assertTrue(init.isInitialized(), "Not initialized after releasing latch");
342     }
343 
344     /**
345      * Tests isStarted() after the background task has finished.
346      */
347     @Test
348     void testIsStartedAfterGet() throws ConcurrentException {
349         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
350         init.start();
351         checkInitialize(init);
352         assertTrue(init.isStarted(), "Not started");
353     }
354 
355     /**
356      * Tests isStarted() before start() was called.
357      */
358     @Test
359     void testIsStartedFalse() {
360         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
361         assertFalse(init.isStarted(), "Already started");
362     }
363 
364     /**
365      * Tests isStarted() after start().
366      */
367     @Test
368     void testIsStartedTrue() {
369         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
370         init.start();
371         assertTrue(init.isStarted(), "Not started");
372     }
373 
374     /**
375      * Tests whether an external executor can be set using the
376      * setExternalExecutor() method.
377      */
378     @Test
379     void testSetExternalExecutor() throws ConcurrentException {
380         final ExecutorService exec = Executors.newCachedThreadPool();
381         try {
382             final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
383             init.setExternalExecutor(exec);
384             assertEquals(exec, init.getExternalExecutor(), "Wrong executor service");
385             assertTrue(init.start(), "Wrong result of start()");
386             assertSame(exec, init.getActiveExecutor(), "Wrong active executor");
387             checkInitialize(init);
388             assertFalse(exec.isShutdown(), "Executor was shutdown");
389         } finally {
390             exec.shutdown();
391         }
392     }
393 
394     /**
395      * Tests that setting an executor after start() causes an exception.
396      *
397      * @throws org.apache.commons.lang3.concurrent.ConcurrentException because the test implementation may throw it
398      */
399     @Test
400     void testSetExternalExecutorAfterStart() throws ConcurrentException, InterruptedException {
401         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
402         init.start();
403         final ExecutorService exec = Executors.newSingleThreadExecutor();
404         try {
405             assertThrows(IllegalStateException.class, () -> init.setExternalExecutor(exec));
406             init.get();
407         } finally {
408             exec.shutdown();
409             exec.awaitTermination(1, TimeUnit.SECONDS);
410         }
411     }
412 
413     /**
414      * Tests invoking start() multiple times. Only the first invocation should
415      * have an effect.
416      */
417     @Test
418     void testStartMultipleTimes() throws ConcurrentException {
419         final AbstractBackgroundInitializerTestImpl init = getBackgroundInitializerTestImpl();
420         assertTrue(init.start(), "Wrong result for start()");
421         for (int i = 0; i < 10; i++) {
422             assertFalse(init.start(), "Could start again");
423         }
424         checkInitialize(init);
425     }
426 }