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