001/*
002 * Copyright (C) 2006 The Guava Authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
005 * in compliance with the License. You may obtain a copy of the License at
006 *
007 * http://www.apache.org/licenses/LICENSE-2.0
008 *
009 * Unless required by applicable law or agreed to in writing, software distributed under the License
010 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
011 * or implied. See the License for the specific language governing permissions and limitations under
012 * the License.
013 */
014
015package com.google.common.util.concurrent;
016
017import static com.google.common.base.Preconditions.checkArgument;
018import static com.google.common.base.Preconditions.checkNotNull;
019
020import com.google.common.annotations.Beta;
021import com.google.common.annotations.GwtIncompatible;
022import com.google.common.collect.ObjectArrays;
023import com.google.common.collect.Sets;
024import com.google.errorprone.annotations.CanIgnoreReturnValue;
025import java.lang.reflect.InvocationHandler;
026import java.lang.reflect.InvocationTargetException;
027import java.lang.reflect.Method;
028import java.lang.reflect.Proxy;
029import java.util.Set;
030import java.util.concurrent.Callable;
031import java.util.concurrent.ExecutionException;
032import java.util.concurrent.ExecutorService;
033import java.util.concurrent.Executors;
034import java.util.concurrent.Future;
035import java.util.concurrent.TimeUnit;
036import java.util.concurrent.TimeoutException;
037
038/**
039 * A TimeLimiter that runs method calls in the background using an {@link ExecutorService}. If the
040 * time limit expires for a given method call, the thread running the call will be interrupted.
041 *
042 * @author Kevin Bourrillion
043 * @since 1.0
044 */
045@Beta
046@GwtIncompatible
047public final class SimpleTimeLimiter implements TimeLimiter {
048
049  private final ExecutorService executor;
050
051  /**
052   * Constructs a TimeLimiter instance using the given executor service to execute proxied method
053   * calls.
054   *
055   * <p><b>Warning:</b> using a bounded executor may be counterproductive! If the thread pool fills
056   * up, any time callers spend waiting for a thread may count toward their time limit, and in this
057   * case the call may even time out before the target method is ever invoked.
058   *
059   * @param executor the ExecutorService that will execute the method calls on the target objects;
060   *     for example, a {@link Executors#newCachedThreadPool()}.
061   */
062  public SimpleTimeLimiter(ExecutorService executor) {
063    this.executor = checkNotNull(executor);
064  }
065
066  /**
067   * Constructs a TimeLimiter instance using a {@link Executors#newCachedThreadPool()} to execute
068   * proxied method calls.
069   *
070   * <p><b>Warning:</b> using a bounded executor may be counterproductive! If the thread pool fills
071   * up, any time callers spend waiting for a thread may count toward their time limit, and in this
072   * case the call may even time out before the target method is ever invoked.
073   */
074  public SimpleTimeLimiter() {
075    this(Executors.newCachedThreadPool());
076  }
077
078  @Override
079  public <T> T newProxy(
080      final T target,
081      Class<T> interfaceType,
082      final long timeoutDuration,
083      final TimeUnit timeoutUnit) {
084    checkNotNull(target);
085    checkNotNull(interfaceType);
086    checkNotNull(timeoutUnit);
087    checkArgument(timeoutDuration > 0, "bad timeout: %s", timeoutDuration);
088    checkArgument(interfaceType.isInterface(), "interfaceType must be an interface type");
089
090    final Set<Method> interruptibleMethods = findInterruptibleMethods(interfaceType);
091
092    InvocationHandler handler =
093        new InvocationHandler() {
094          @Override
095          public Object invoke(Object obj, final Method method, final Object[] args)
096              throws Throwable {
097            Callable<Object> callable =
098                new Callable<Object>() {
099                  @Override
100                  public Object call() throws Exception {
101                    try {
102                      return method.invoke(target, args);
103                    } catch (InvocationTargetException e) {
104                      throw throwCause(e, false);
105                    }
106                  }
107                };
108            return callWithTimeout(
109                callable, timeoutDuration, timeoutUnit, interruptibleMethods.contains(method));
110          }
111        };
112    return newProxy(interfaceType, handler);
113  }
114
115  // TODO: should this actually throw only ExecutionException?
116  @CanIgnoreReturnValue
117  @Override
118  public <T> T callWithTimeout(
119      Callable<T> callable, long timeoutDuration, TimeUnit timeoutUnit, boolean amInterruptible)
120      throws Exception {
121    checkNotNull(callable);
122    checkNotNull(timeoutUnit);
123    checkArgument(timeoutDuration > 0, "timeout must be positive: %s", timeoutDuration);
124    Future<T> future = executor.submit(callable);
125    try {
126      if (amInterruptible) {
127        try {
128          return future.get(timeoutDuration, timeoutUnit);
129        } catch (InterruptedException e) {
130          future.cancel(true);
131          throw e;
132        }
133      } else {
134        return Uninterruptibles.getUninterruptibly(future, timeoutDuration, timeoutUnit);
135      }
136    } catch (ExecutionException e) {
137      throw throwCause(e, true);
138    } catch (TimeoutException e) {
139      future.cancel(true);
140      throw new UncheckedTimeoutException(e);
141    }
142  }
143
144  private static Exception throwCause(Exception e, boolean combineStackTraces) throws Exception {
145    Throwable cause = e.getCause();
146    if (cause == null) {
147      throw e;
148    }
149    if (combineStackTraces) {
150      StackTraceElement[] combined =
151          ObjectArrays.concat(cause.getStackTrace(), e.getStackTrace(), StackTraceElement.class);
152      cause.setStackTrace(combined);
153    }
154    if (cause instanceof Exception) {
155      throw (Exception) cause;
156    }
157    if (cause instanceof Error) {
158      throw (Error) cause;
159    }
160    // The cause is a weird kind of Throwable, so throw the outer exception.
161    throw e;
162  }
163
164  private static Set<Method> findInterruptibleMethods(Class<?> interfaceType) {
165    Set<Method> set = Sets.newHashSet();
166    for (Method m : interfaceType.getMethods()) {
167      if (declaresInterruptedEx(m)) {
168        set.add(m);
169      }
170    }
171    return set;
172  }
173
174  private static boolean declaresInterruptedEx(Method method) {
175    for (Class<?> exType : method.getExceptionTypes()) {
176      // debate: == or isAssignableFrom?
177      if (exType == InterruptedException.class) {
178        return true;
179      }
180    }
181    return false;
182  }
183
184  // TODO: replace with version in common.reflect if and when it's open-sourced
185  private static <T> T newProxy(Class<T> interfaceType, InvocationHandler handler) {
186    Object object =
187        Proxy.newProxyInstance(
188            interfaceType.getClassLoader(), new Class<?>[] {interfaceType}, handler);
189    return interfaceType.cast(object);
190  }
191}