View Javadoc
1   //CHECKSTYLE:OFF
2   package org.junit.internal.runners.statements;
3   
4   import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
5   import java.lang.management.ManagementFactory;
6   import java.lang.management.ThreadMXBean;
7   import java.util.Arrays;
8   import java.util.concurrent.Callable;
9   import java.util.concurrent.CountDownLatch;
10  import java.util.concurrent.ExecutionException;
11  import java.util.concurrent.FutureTask;
12  import java.util.concurrent.TimeUnit;
13  import java.util.concurrent.TimeoutException;
14  import javax.annotation.Nullable;
15  import javax.annotation.ParametersAreNonnullByDefault;
16  
17  import org.junit.runners.model.MultipleFailureException;
18  import org.junit.runners.model.Statement;
19  import org.junit.runners.model.TestTimedOutException;
20  import org.spf4j.base.ExecutionContext;
21  import org.spf4j.base.ExecutionContexts;
22  import org.spf4j.test.log.TestUtils;
23  
24  @ParametersAreNonnullByDefault
25  @SuppressWarnings("checkstyle")
26  public final class FailOnTimeout extends Statement {
27      private final Statement originalStatement;
28      private final TimeUnit timeUnit;
29      private final long timeout;
30      private final boolean lookForStuckThread;
31      private volatile ThreadGroup threadGroup = null;
32  
33      /**
34       * Returns a new builder for building an instance.
35       *
36       * @since 4.12
37       */
38      public static Builder builder() {
39          return new Builder();
40      }
41  
42      /**
43       * Creates an instance wrapping the given statement with the given timeout in milliseconds.
44       *
45       * @param statement the statement to wrap
46       * @param timeoutMillis the timeout in milliseconds
47       * @deprecated use {@link #builder()} instead.
48       */
49      @Deprecated
50      public FailOnTimeout(final Statement statement, final long timeoutMillis) {
51          this(builder().withTimeout(timeoutMillis, TimeUnit.MILLISECONDS), statement);
52      }
53  
54      private FailOnTimeout(final Builder builder, final Statement statement) {
55          originalStatement = statement;
56          timeout = builder.timeout;
57          timeUnit = builder.unit;
58          lookForStuckThread = builder.lookForStuckThread;
59      }
60  
61      /**
62       * Builder for {@link FailOnTimeout}.
63       *
64       * @since 4.12
65       */
66      public static final class Builder {
67          private boolean lookForStuckThread = false;
68          private long timeout = 0;
69          private TimeUnit unit = TimeUnit.SECONDS;
70  
71          private Builder() {
72          }
73  
74          /**
75           * Specifies the time to wait before timing out the test.
76           *
77           * <p>If this is not called, or is called with a {@code timeout} of
78           * {@code 0}, the returned {@code Statement} will wait forever for the
79           * test to complete, however the test will still launch from a separate
80           * thread. This can be useful for disabling timeouts in environments
81           * where they are dynamically set based on some property.
82           *
83           * @param timeout the maximum time to wait
84           * @param unit the time unit of the {@code timeout} argument
85           * @return {@code this} for method chaining.
86           */
87          public Builder withTimeout(final long timeout, final TimeUnit unit) {
88              if (timeout < 0) {
89                  throw new IllegalArgumentException("timeout must be non-negative "  + timeout);
90              }
91              if (unit == null) {
92                  throw new NullPointerException("TimeUnit cannot be null for  " + timeout);
93              }
94              this.timeout = timeout;
95              this.unit = unit;
96              return this;
97          }
98  
99          /**
100          * Specifies whether to look for a stuck thread.  If a timeout occurs and this
101          * feature is enabled, the test will look for a thread that appears to be stuck
102          * and dump its backtrace.  This feature is experimental.  Behavior may change
103          * after the 4.12 release in response to feedback.
104          *
105          * @param enable {@code true} to enable the feature
106          * @return {@code this} for method chaining.
107          */
108         public Builder withLookingForStuckThread(final boolean enable) {
109             this.lookForStuckThread = enable;
110             return this;
111         }
112 
113         /**
114          * Builds a {@link FailOnTimeout} instance using the values in this builder,
115          * wrapping the given statement.
116          *
117          * @param statement
118          */
119         public FailOnTimeout build(final Statement statement) {
120             return new FailOnTimeout(this, statement);
121         }
122     }
123 
124     @Override
125     public void evaluate() throws Throwable {
126         if (TestUtils.isExecutedWithDebuggerAgent()) {
127           originalStatement.evaluate();
128           return;
129         }
130         CallableStatement callable = new CallableStatement();
131         callable.setCtx(ExecutionContexts.current());
132         FutureTask<Throwable> task = new FutureTask<Throwable>(callable);
133         threadGroup = new ThreadGroup("FailOnTimeoutGroup");
134         Thread thread = new Thread(threadGroup, task, "Time-limited test");
135         thread.setDaemon(true);
136         thread.start();
137         callable.awaitStarted();
138         Throwable throwable = getResult(task, thread);
139         if (throwable != null) {
140             throw throwable;
141         }
142     }
143 
144     /**
145      * Wait for the test task, returning the exception thrown by the test if the
146      * test failed, an exception indicating a timeout if the test timed out, or
147      * {@code null} if the test passed.
148      */
149     private Throwable getResult(FutureTask<Throwable> task, Thread thread) {
150         try {
151             if (timeout > 0) {
152                 return task.get(timeout, timeUnit);
153             } else {
154                 return task.get();
155             }
156         } catch (InterruptedException e) {
157             return e; // caller will re-throw; no need to call Thread.interrupt()
158         } catch (ExecutionException e) {
159             // test failed; have caller re-throw the exception thrown by the test
160             return e.getCause();
161         } catch (TimeoutException e) {
162             return createTimeoutException(thread);
163         }
164     }
165 
166     private Exception createTimeoutException(Thread thread) {
167         StackTraceElement[] stackTrace = thread.getStackTrace();
168         final Thread stuckThread = lookForStuckThread ? getStuckThread(thread) : null;
169         Exception currThreadException = new TestTimedOutException(timeout, timeUnit);
170         if (stackTrace != null) {
171             currThreadException.setStackTrace(stackTrace);
172             thread.interrupt();
173         }
174         if (stuckThread != null) {
175             Exception stuckThreadException =
176                 new Exception ("Appears to be stuck in thread " +
177                                stuckThread.getName());
178             stuckThreadException.setStackTrace(getStackTrace(stuckThread));
179             return new MultipleFailureException(
180                 Arrays.<Throwable>asList(currThreadException, stuckThreadException));
181         } else {
182             return currThreadException;
183         }
184     }
185 
186     /**
187      * Retrieves the stack trace for a given thread.
188      * @param thread The thread whose stack is to be retrieved.
189      * @return The stack trace; returns a zero-length array if the thread has
190      * terminated or the stack cannot be retrieved for some other reason.
191      */
192     private StackTraceElement[] getStackTrace(Thread thread) {
193         try {
194             return thread.getStackTrace();
195         } catch (SecurityException e) {
196             return new StackTraceElement[0];
197         }
198     }
199 
200     /**
201      * Determines whether the test appears to be stuck in some thread other than
202      * the "main thread" (the one created to run the test).  This feature is experimental.
203      * Behavior may change after the 4.12 release in response to feedback.
204      * @param mainThread The main thread created by {@code evaluate()}
205      * @return The thread which appears to be causing the problem, if different from
206      * {@code mainThread}, or {@code null} if the main thread appears to be the
207      * problem or if the thread cannot be determined.  The return value is never equal
208      * to {@code mainThread}.
209      */
210     @Nullable
211     private Thread getStuckThread(Thread mainThread) {
212         if (threadGroup == null) {
213             return null;
214         }
215         Thread[] threadsInGroup = getThreadArray(threadGroup);
216         if (threadsInGroup == null) {
217             return null;
218         }
219 
220         // Now that we have all the threads in the test's thread group: Assume that
221         // any thread we're "stuck" in is RUNNABLE.  Look for all RUNNABLE threads.
222         // If just one, we return that (unless it equals threadMain).  If there's more
223         // than one, pick the one that's using the most CPU time, if this feature is
224         // supported.
225         Thread stuckThread = null;
226         long maxCpuTime = 0;
227         for (Thread thread : threadsInGroup) {
228             if (thread.getState() == Thread.State.RUNNABLE) {
229                 long threadCpuTime = cpuTime(thread);
230                 if (stuckThread == null || threadCpuTime > maxCpuTime) {
231                     stuckThread = thread;
232                     maxCpuTime = threadCpuTime;
233                 }
234             }
235         }
236         return (stuckThread == mainThread) ? null : stuckThread;
237     }
238 
239     /**
240      * Returns all active threads belonging to a thread group.
241      * @param group The thread group.
242      * @return The active threads in the thread group.  The result should be a
243      * complete list of the active threads at some point in time.  Returns {@code null}
244      * if this cannot be determined, e.g. because new threads are being created at an
245      * extremely fast rate.
246      */
247     @Nullable
248     @SuppressFBWarnings("PZLA_PREFER_ZERO_LENGTH_ARRAYS")
249     private Thread[] getThreadArray(final ThreadGroup group) {
250         final int count = group.activeCount(); // this is just an estimate
251         int enumSize = Math.max(count * 2, 100);
252         int enumCount;
253         Thread[] threads;
254         int loopCount = 0;
255         while (true) {
256             threads = new Thread[enumSize];
257             enumCount = group.enumerate(threads);
258             if (enumCount < enumSize) {
259                 break;
260             }
261             // if there are too many threads to fit into the array, enumerate's result
262             // is >= the array's length; therefore we can't trust that it returned all
263             // the threads.  Try again.
264             enumSize += 100;
265             if (++loopCount >= 5) {
266                 return null;
267             }
268             // threads are proliferating too fast for us.  Bail before we get into
269             // trouble.
270         }
271         return copyThreads(threads, enumCount);
272     }
273 
274     /**
275      * Returns an array of the first {@code count} Threads in {@code threads}.
276      * (Use instead of Arrays.copyOf to maintain compatibility with Java 1.5.)
277      * @param threads The source array.
278      * @param count The maximum length of the result array.
279      * @return The first {@count} (at most) elements of {@code threads}.
280      */
281     private Thread[] copyThreads(Thread[] threads, int count) {
282         int length = Math.min(count, threads.length);
283         return Arrays.copyOf(threads, length);
284     }
285 
286     /**
287      * Returns the CPU time used by a thread, if possible.
288      * @param thr The thread to query.
289      * @return The CPU time used by {@code thr}, or 0 if it cannot be determined.
290      */
291     private long cpuTime (Thread thr) {
292         ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
293         if (mxBean.isThreadCpuTimeSupported()) {
294             try {
295                 return mxBean.getThreadCpuTime(thr.getId());
296             } catch (UnsupportedOperationException e) {
297             }
298         }
299         return 0;
300     }
301 
302     private class CallableStatement implements Callable<Throwable> {
303         private final CountDownLatch startLatch = new CountDownLatch(1);
304 
305         private  ExecutionContext  ctx;
306 
307         public void setCtx(ExecutionContext ctx) {
308           this.ctx = ctx;
309         }
310 
311         @Nullable
312         public Throwable call() throws Exception {
313 
314             try (ExecutionContext aCtx = ExecutionContexts.start("asyncTest", ctx)) {
315                 startLatch.countDown();
316                 originalStatement.evaluate();
317             } catch (Exception e) {
318                 throw e;
319             } catch (Throwable e) {
320                 return e;
321             }
322             return null;
323         }
324 
325         public void awaitStarted() throws InterruptedException {
326             startLatch.await();
327         }
328     }
329 
330   @Override
331   public String toString() {
332     return "FailOnTimeout{" + "originalStatement=" + originalStatement
333             + ", timeUnit=" + timeUnit + ", timeout=" + timeout + ", lookForStuckThread="
334             + lookForStuckThread + ", threadGroup=" + threadGroup + '}';
335   }
336 
337 
338 }