View Javadoc

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