Home | History | Annotate | Download | only in concurrent
      1 /*
      2  * Copyright (C) 2006 The Guava Authors
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  * http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.google.common.util.concurrent;
     18 
     19 import static com.google.common.base.Preconditions.checkArgument;
     20 import static com.google.common.base.Preconditions.checkNotNull;
     21 
     22 import com.google.common.annotations.Beta;
     23 import com.google.common.collect.ObjectArrays;
     24 import com.google.common.collect.Sets;
     25 
     26 import java.lang.reflect.InvocationHandler;
     27 import java.lang.reflect.InvocationTargetException;
     28 import java.lang.reflect.Method;
     29 import java.lang.reflect.Proxy;
     30 import java.util.Set;
     31 import java.util.concurrent.Callable;
     32 import java.util.concurrent.ExecutionException;
     33 import java.util.concurrent.ExecutorService;
     34 import java.util.concurrent.Executors;
     35 import java.util.concurrent.Future;
     36 import java.util.concurrent.TimeUnit;
     37 import java.util.concurrent.TimeoutException;
     38 
     39 /**
     40  * A TimeLimiter that runs method calls in the background using an
     41  * {@link ExecutorService}.  If the time limit expires for a given method call,
     42  * the thread running the call will be interrupted.
     43  *
     44  * @author Kevin Bourrillion
     45  * @since 1.0
     46  */
     47 @Beta
     48 public final class SimpleTimeLimiter implements TimeLimiter {
     49 
     50   private final ExecutorService executor;
     51 
     52   /**
     53    * Constructs a TimeLimiter instance using the given executor service to
     54    * execute proxied method calls.
     55    * <p>
     56    * <b>Warning:</b> using a bounded executor
     57    * may be counterproductive!  If the thread pool fills up, any time callers
     58    * spend waiting for a thread may count toward their time limit, and in
     59    * this case the call may even time out before the target method is ever
     60    * invoked.
     61    *
     62    * @param executor the ExecutorService that will execute the method calls on
     63    *     the target objects; for example, a {@link
     64    *     Executors#newCachedThreadPool()}.
     65    */
     66   public SimpleTimeLimiter(ExecutorService executor) {
     67     this.executor = checkNotNull(executor);
     68   }
     69 
     70   /**
     71    * Constructs a TimeLimiter instance using a {@link
     72    * Executors#newCachedThreadPool()} to execute proxied method calls.
     73    *
     74    * <p><b>Warning:</b> using a bounded executor may be counterproductive! If
     75    * the thread pool fills up, any time callers spend waiting for a thread may
     76    * count toward their time limit, and in this case the call may even time out
     77    * before the target method is ever invoked.
     78    */
     79   public SimpleTimeLimiter() {
     80     this(Executors.newCachedThreadPool());
     81   }
     82 
     83   @Override
     84   public <T> T newProxy(final T target, Class<T> interfaceType,
     85       final long timeoutDuration, final TimeUnit timeoutUnit) {
     86     checkNotNull(target);
     87     checkNotNull(interfaceType);
     88     checkNotNull(timeoutUnit);
     89     checkArgument(timeoutDuration > 0, "bad timeout: " + timeoutDuration);
     90     checkArgument(interfaceType.isInterface(),
     91         "interfaceType must be an interface type");
     92 
     93     final Set<Method> interruptibleMethods
     94         = findInterruptibleMethods(interfaceType);
     95 
     96     InvocationHandler handler = new InvocationHandler() {
     97       @Override
     98       public Object invoke(Object obj, final Method method, final Object[] args)
     99           throws Throwable {
    100         Callable<Object> callable = new Callable<Object>() {
    101           @Override
    102           public Object call() throws Exception {
    103             try {
    104               return method.invoke(target, args);
    105             } catch (InvocationTargetException e) {
    106               throwCause(e, false);
    107               throw new AssertionError("can't get here");
    108             }
    109           }
    110         };
    111         return callWithTimeout(callable, timeoutDuration, timeoutUnit,
    112             interruptibleMethods.contains(method));
    113       }
    114     };
    115     return newProxy(interfaceType, handler);
    116   }
    117 
    118   // TODO: should this actually throw only ExecutionException?
    119   @Override
    120   public <T> T callWithTimeout(Callable<T> callable, long timeoutDuration,
    121       TimeUnit timeoutUnit, boolean amInterruptible) throws Exception {
    122     checkNotNull(callable);
    123     checkNotNull(timeoutUnit);
    124     checkArgument(timeoutDuration > 0, "timeout must be positive: %s",
    125         timeoutDuration);
    126     Future<T> future = executor.submit(callable);
    127     try {
    128       if (amInterruptible) {
    129         try {
    130           return future.get(timeoutDuration, timeoutUnit);
    131         } catch (InterruptedException e) {
    132           future.cancel(true);
    133           throw e;
    134         }
    135       } else {
    136         return Uninterruptibles.getUninterruptibly(future,
    137             timeoutDuration, timeoutUnit);
    138       }
    139     } catch (ExecutionException e) {
    140       throw throwCause(e, true);
    141     } catch (TimeoutException e) {
    142       future.cancel(true);
    143       throw new UncheckedTimeoutException(e);
    144     }
    145   }
    146 
    147   private static Exception throwCause(Exception e, boolean combineStackTraces)
    148       throws Exception {
    149     Throwable cause = e.getCause();
    150     if (cause == null) {
    151       throw e;
    152     }
    153     if (combineStackTraces) {
    154       StackTraceElement[] combined = ObjectArrays.concat(cause.getStackTrace(),
    155           e.getStackTrace(), StackTraceElement.class);
    156       cause.setStackTrace(combined);
    157     }
    158     if (cause instanceof Exception) {
    159       throw (Exception) cause;
    160     }
    161     if (cause instanceof Error) {
    162       throw (Error) cause;
    163     }
    164     // The cause is a weird kind of Throwable, so throw the outer exception.
    165     throw e;
    166   }
    167 
    168   private static Set<Method> findInterruptibleMethods(Class<?> interfaceType) {
    169     Set<Method> set = Sets.newHashSet();
    170     for (Method m : interfaceType.getMethods()) {
    171       if (declaresInterruptedEx(m)) {
    172         set.add(m);
    173       }
    174     }
    175     return set;
    176   }
    177 
    178   private static boolean declaresInterruptedEx(Method method) {
    179     for (Class<?> exType : method.getExceptionTypes()) {
    180       // debate: == or isAssignableFrom?
    181       if (exType == InterruptedException.class) {
    182         return true;
    183       }
    184     }
    185     return false;
    186   }
    187 
    188   // TODO: replace with version in common.reflect if and when it's open-sourced
    189   private static <T> T newProxy(
    190       Class<T> interfaceType, InvocationHandler handler) {
    191     Object object = Proxy.newProxyInstance(interfaceType.getClassLoader(),
    192         new Class<?>[] { interfaceType }, handler);
    193     return interfaceType.cast(object);
    194   }
    195 }
    196