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