View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.util.concurrent;
8   
9   import org.mule.tck.junit4.AbstractMuleTestCase;
10  import org.mule.util.StringUtils;
11  
12  import java.util.ArrayList;
13  import java.util.Collections;
14  import java.util.Iterator;
15  import java.util.LinkedList;
16  import java.util.List;
17  import java.util.Map;
18  
19  import edu.emory.mathcs.backport.java.util.concurrent.LinkedBlockingQueue;
20  import edu.emory.mathcs.backport.java.util.concurrent.RejectedExecutionException;
21  import edu.emory.mathcs.backport.java.util.concurrent.ThreadPoolExecutor;
22  import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
23  import edu.emory.mathcs.backport.java.util.concurrent.atomic.AtomicInteger;
24  import edu.emory.mathcs.backport.java.util.concurrent.locks.ReentrantLock;
25  import org.junit.After;
26  import org.junit.Before;
27  import org.junit.Test;
28  
29  import static junit.framework.Assert.assertFalse;
30  import static org.junit.Assert.assertEquals;
31  import static org.junit.Assert.assertSame;
32  import static org.junit.Assert.assertTrue;
33  
34  public class WaitPolicyTestCase extends AbstractMuleTestCase
35  {
36      private ExceptionCollectingThreadGroup threadGroup;
37      ThreadPoolExecutor executor;
38      ReentrantLock executorLock;
39  
40      @Before
41      public void startExecutor()
42      {
43          // allow 1 active & 1 queued Thread
44          executor = new ThreadPoolExecutor(1, 1, 10000L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(1));
45          executor.prestartAllCoreThreads();
46  
47          // the lock must be fair to guarantee FIFO access to the executor;
48          // 'synchronized' on a monitor is not good enough.
49          executorLock = new ReentrantLock(true);
50  
51          // this is a ThreadGroup that collects uncaught exceptions. Necessary for JDK
52          // 1.4.x only.
53          threadGroup = new ExceptionCollectingThreadGroup();
54  
55          // reset counter of active SleepyTasks
56          SleepyTask.activeTasks.set(0);
57      }
58  
59      @After
60      public void shutDownExecutor()
61      {
62          executor.shutdown();
63          threadGroup.destroy();
64      }
65  
66      // Submit the given Runnables to an ExecutorService, but do so in separate
67      // threads to avoid blocking the test case when the Executors queue is full.
68      // Returns control to the caller when the threads have been started in
69      // order to avoid OS-dependent delays in the control flow.
70      // A submitting thread waits on a fair Lock to guarantee FIFO ordering.
71      // At the time of return the Runnables may or may not be in the queue,
72      // rejected, running or already finished. :-)
73      protected LinkedList<Thread> execute(final List<Runnable> tasks) throws InterruptedException
74      {
75          if (tasks == null || tasks.isEmpty())
76          {
77              throw new IllegalArgumentException("List<Runnable> must not be empty");
78          }
79  
80          LinkedList<Thread> submitters = new LinkedList<Thread>();
81  
82          executorLock.lock();
83  
84          for (Iterator<Runnable> i = tasks.iterator(); i.hasNext();)
85          {
86              final Runnable task = i.next();
87  
88              Runnable submitterAction = new Runnable()
89              {
90                  public void run()
91                  {
92                      // the lock is important because otherwise two submitters might
93                      // stumble over each other, submitting their runnables out-of-order
94                      // and causing test failures.
95                      try
96                      {
97                          executorLock.lock();
98                          executor.execute(task);
99                      }
100                     finally
101                     {
102                         executorLock.unlock();
103                     }
104                 }
105             };
106 
107             Thread submitter = new Thread(threadGroup, submitterAction);
108             submitter.setDaemon(true);
109             submitters.add(submitter);
110             submitter.start();
111 
112             // wait until the thread is actually enqueued on the lock
113             while (submitter.isAlive() && !executorLock.hasQueuedThread(submitter))
114             {
115                 Thread.sleep(10);
116             }
117         }
118 
119         executorLock.unlock();
120         return submitters;
121     }
122 
123     @Test
124     public void testWaitPolicyWithShutdownExecutor() throws Exception
125     {
126         assertEquals(0, SleepyTask.activeTasks.get());
127 
128         // wants to wait forever, but will fail immediately
129         executor.setRejectedExecutionHandler(new LastRejectedWaitPolicy());
130         executor.shutdown();
131 
132         // create a task
133         List<Runnable> tasks = new ArrayList<Runnable>();
134         tasks.add(new SleepyTask("rejected", 1000));
135 
136         // should fail and return immediately
137         LinkedList<Thread> submitters = this.execute(tasks);
138         assertFalse(submitters.isEmpty());
139 
140         // let submitted tasks run
141         Thread.sleep(1000);
142 
143         LinkedList<Map<Thread, Throwable>> exceptions = threadGroup.collectedExceptions();
144         assertEquals(1, exceptions.size());
145 
146         Map.Entry<Thread, Throwable> threadFailure = exceptions.getFirst().entrySet().iterator().next();
147         assertEquals(submitters.getFirst(), threadFailure.getKey());
148         assertEquals(RejectedExecutionException.class, threadFailure.getValue().getClass());
149         assertEquals(0, SleepyTask.activeTasks.get());
150     }
151 
152     @Test
153     public void testWaitPolicyForever() throws Exception
154     {
155         assertEquals(0, SleepyTask.activeTasks.get());
156 
157         // tasks wait forever
158         LastRejectedWaitPolicy policy = new LastRejectedWaitPolicy(-1, TimeUnit.SECONDS);
159         executor.setRejectedExecutionHandler(policy);
160 
161         // create tasks
162         List<Runnable> tasks = new ArrayList<Runnable>();
163         // task 1 runs immediately
164         tasks.add(new SleepyTask("run", 1000));
165         // task 2 is queued
166         tasks.add(new SleepyTask("queued", 1000));
167         // task 3 is initially rejected but waits forever
168         Runnable waiting = new SleepyTask("waitingForever", 1000);
169         tasks.add(waiting);
170 
171         // submit tasks
172         LinkedList<Thread> submitters = this.execute(tasks);
173         assertFalse(submitters.isEmpty());
174 
175         // the last task should have been queued
176         assertFalse(executor.awaitTermination(4000, TimeUnit.MILLISECONDS));
177         assertSame(waiting, policy.lastRejectedRunnable());
178         assertEquals(0, SleepyTask.activeTasks.get());
179     }
180 
181     @Test
182     public void testWaitPolicyWithTimeout() throws Exception
183     {
184         assertEquals(0, SleepyTask.activeTasks.get());
185 
186         // set a reasonable retry interval
187         LastRejectedWaitPolicy policy = new LastRejectedWaitPolicy(2500, TimeUnit.MILLISECONDS);
188         executor.setRejectedExecutionHandler(policy);
189 
190         // create tasks
191         List<Runnable> tasks = new ArrayList<Runnable>();
192         // task 1 runs immediately
193         tasks.add(new SleepyTask("run", 1000));
194         // task 2 is queued
195         tasks.add(new SleepyTask("queued", 1000));
196         // task 3 is initially rejected but will eventually succeed
197         Runnable waiting = new SleepyTask("waiting", 1000);
198         tasks.add(waiting);
199 
200         // submit tasks
201         LinkedList<Thread> submitters = this.execute(tasks);
202         assertFalse(submitters.isEmpty());
203 
204         assertFalse(executor.awaitTermination(5000, TimeUnit.MILLISECONDS));
205         assertSame(waiting, policy.lastRejectedRunnable());
206         assertEquals(0, SleepyTask.activeTasks.get());
207     }
208 
209     @Test
210     public void testWaitPolicyWithTimeoutFailure() throws Exception
211     {
212         assertEquals(0, SleepyTask.activeTasks.get());
213 
214         // set a really short wait interval
215         long failureInterval = 100L;
216         LastRejectedWaitPolicy policy = new LastRejectedWaitPolicy(failureInterval, TimeUnit.MILLISECONDS);
217         executor.setRejectedExecutionHandler(policy);
218 
219         // create tasks
220         List<Runnable> tasks = new ArrayList<Runnable>();
221         // task 1 runs immediately
222         tasks.add(new SleepyTask("run", 1000));
223         // task 2 is queued
224         tasks.add(new SleepyTask("queued", 1000));
225         // task 3 is initially rejected & will retry but should fail quickly
226         Runnable failedTask = new SleepyTask("waitAndFail", 1000);
227         tasks.add(failedTask);
228 
229         // submit tasks
230         LinkedList<Thread> submitters = this.execute(tasks);
231         assertFalse(submitters.isEmpty());
232 
233         // give failure a chance
234         Thread.sleep(failureInterval * 10);
235 
236         // make sure there was one failure
237         LinkedList<Map<Thread, Throwable>>  exceptions = threadGroup.collectedExceptions();
238         assertEquals(1, exceptions.size());
239 
240         // make sure the failed task was the right one
241         Map.Entry<Thread, Throwable> threadFailure = exceptions.getFirst().entrySet().iterator().next();
242         assertEquals(submitters.getLast(), threadFailure.getKey());
243         assertEquals(RejectedExecutionException.class, threadFailure.getValue().getClass());
244 
245         executor.shutdown();
246         assertTrue(executor.awaitTermination(2500, TimeUnit.MILLISECONDS));
247         assertSame(failedTask, policy.lastRejectedRunnable());
248         assertEquals(0, SleepyTask.activeTasks.get());
249     }
250 }
251 
252 class LastRejectedWaitPolicy extends WaitPolicy
253 {
254     // needed to hand the last rejected Runnable back to the TestCase
255     private volatile Runnable _rejected;
256 
257     public LastRejectedWaitPolicy()
258     {
259         super();
260     }
261 
262     public LastRejectedWaitPolicy(long time, TimeUnit timeUnit)
263     {
264         super(time, timeUnit);
265     }
266 
267     public Runnable lastRejectedRunnable()
268     {
269         return _rejected;
270     }
271 
272     @Override
273     public void rejectedExecution(Runnable r, ThreadPoolExecutor e)
274     {
275         _rejected = r;
276         super.rejectedExecution(r, e);
277     }
278 }
279 
280 // task to execute - just sleeps for the given interval
281 class SleepyTask extends Object implements Runnable
282 {
283     public static final AtomicInteger activeTasks = new AtomicInteger(0);
284 
285     private final String name;
286     private final long sleepTime;
287 
288     public SleepyTask(String name, long sleepTime)
289     {
290         if (StringUtils.isEmpty(name))
291         {
292             throw new IllegalArgumentException("SleepyTask needs a name!");
293         }
294 
295         this.name = name;
296         this.sleepTime = sleepTime;
297     }
298 
299     @Override
300     public String toString()
301     {
302         return this.getClass().getName() + '{' + name + ", " + sleepTime + '}';
303     }
304 
305     public void run()
306     {
307         activeTasks.incrementAndGet();
308 
309         try
310         {
311             Thread.sleep(sleepTime);
312         }
313         catch (InterruptedException iex)
314         {
315             Thread.currentThread().interrupt();
316         }
317         finally
318         {
319             activeTasks.decrementAndGet();
320         }
321     }
322 
323 }
324 
325 // ThreadGroup wrapper that collects uncaught exceptions
326 class ExceptionCollectingThreadGroup extends ThreadGroup
327 {
328     private final LinkedList<Map<Thread, Throwable>> exceptions = new LinkedList<Map<Thread, Throwable>>();
329 
330     public ExceptionCollectingThreadGroup()
331     {
332         super("ExceptionCollectingThreadGroup");
333     }
334 
335     // collected Map(Thread, Throwable) associations
336     public LinkedList<Map<Thread, Throwable>> collectedExceptions()
337     {
338         return exceptions;
339     }
340 
341     // all uncaught Thread exceptions end up here
342     @Override
343     public void uncaughtException(Thread t, Throwable e)
344     {
345         synchronized (exceptions)
346         {
347             exceptions.add(Collections.singletonMap(t, e));
348         }
349     }
350 }