ListenableFuture, FutureCallback and timeouts

Nick Campion picture Nick Campion · Sep 12, 2012 · Viewed 16.1k times · Source

Based on the examples of guava I've seen I've been looking for elegant solutions to my problem. Specifically, I like the way Futures.addCallback(ListenableFuture, FutureCallback) works, but I'd like to be able to set a timeout on the length of time that can expire before the FutureCallback is invoked. Optimally It would be nice if breaching the timeout just caused an the failure condition of FutureCallback to be called.

Does Guava have something like this already? Is it just not recommended to try to couple timeouts with the callbacks?

EDIT: Including example of the code that led me to this point. Obviously, I stripped out the meaningful bits to get a minimum example.

@Test
public void testFuture()
{
    Callable<Boolean> callable = new Callable<Boolean>()
    {

        @Override
        public Boolean call() throws Exception
        {
            while(true);
        }
    };

    ListenableFuture<Boolean> callableFuture = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()).submit(callable);

    Futures.addCallback(callableFuture, new FutureCallback<Boolean>()
    {

        @Override
        public void onFailure(Throwable arg0)
        {
            System.out.println("onFailure:"+arg0); 
        }

        @Override
        public void onSuccess(Boolean arg0)
        {
            System.out.println("onSuccess:"+arg0);
        }
    }); 

    try
    {
        callableFuture.get(1000, TimeUnit.MILLISECONDS);
    }catch(Throwable t)
    {
        System.out.println("catch:"+t);
    }
}

This code will only print catch:java.util.concurrent.TimeoutException.

Answer

Chris Povirk picture Chris Povirk · Sep 12, 2012

Update: This has been added to Guava as Futures.withTimeout().


Internally, we have a makeTimeoutFuture method that takes a Future as input and returns a new Future that will have the same result unless the original hasn't completed by a given deadline. If the deadline expires, the output Future has its result set to a TimeoutException. So, you could call makeTimeoutFuture and attach listeners to the output Future.

makeTimeoutFuture isn't the most natural solution for your problem. In fact, I think that the method was created primarily to set a hard timeout on no-arg get() calls, since it can be a pain to propagate the desired deadline to all callers. A more natural solution is to reason that get() is to get(long, TimeUnit) as addCallback(ListenableFuture, FutureCallback) is to addCallback(ListenableFuture, FutureCallback, long, TimeUnit, SchededuledExecutorService). That's a little clumsy, albeit less so than makeTimeoutFuture. I'd want to give this more thought before committing to anything. Would you file a feature request?

(Here's what we have internally:)

public static <V> ListenableFuture<V> makeTimeoutFuture(
    ListenableFuture<V> delegate,
    Duration duration,
    ScheduledExecutorService scheduledExecutor)

Returns a future that delegates to another but will finish early (via a TimeoutException wrapped in an ExecutionException) if the specified duration expires. The delegate future is not cancelled in this case.

scheduledExecutor.schedule(new Runnable() {
  @Override public void run() {
    TimeoutFuture.this.setException(new TimeoutException("Future timed out"));
  }
}, duration.getMillis(), TimeUnit.MILLISECONDS);