MethDamon
MethDamon

Reputation: 141

Detecting a timeout exception on a Java Future without calling get() on it

I am building a library that needs to some bluetooth operations on Android. I want to return a Future instance, so whoever is using my library can call .get() on the future returned and can handle ExecutionException, TimeoutException and InterruptedException themselves. However, I want to detect a timeout myself because I need to some cleanup logic like disconnecting from the device and so on. How can I achieve this?

Upvotes: 1

Views: 1519

Answers (2)

Holger
Holger

Reputation: 298579

The timeout is relevant to the caller of the get method with timeout and only to that caller. A timeout is nowhere meant to imply a cancellation. E.g., the following code is a legitimate usage of the Future API:

ExecutorService es = Executors.newSingleThreadExecutor();

Future<String> f = es.submit(() -> {
    Thread.sleep(3000);
    return "hello";
});

for(;;) try {
    String s = f.get(500, TimeUnit.MILLISECONDS);
    System.out.println("got "+s);
    break;
}
catch(TimeoutException ex) {
    // perhaps, do some other work
    System.out.println("will wait something more");
}
catch (ExecutionException ex) {
    System.out.println("failed with "+ex);
    break;
}

es.shutdown();

Tying the cleanup to the methods actually intended to query the result, is not a useful approach. The timeout provided by the caller(s) of that method do not relate to the actual operation. There’s not even a guaranty that the result will be queried before the operations ends or that it gets queried at all.

The cleanup should happen when either, the operation finished or when the future gets cancelled explicitly. If the caller intends a cancellation after a timeout, the caller only needs to invoke cancel after catching a TimeoutException.
One approach, often pointed to, is to use a CompletionService, e.g.

static final ExecutorService MY__EXECUTOR = Executors.newCachedThreadPool();
static final CompletionService<String> COMPLETION_SERVICE
    = new ExecutorCompletionService<>(MY__EXECUTOR);
static final Future<?> CLEANER = MY__EXECUTOR.submit(() -> {
        for(;;) try {
            Future<String> completed = COMPLETION_SERVICE.take();
            System.out.println("cleanup "+completed);
        } catch(InterruptedException ex) {
            if(MY__EXECUTOR.isShutdown()) break;
        }
    });

public static Future<String> doSomeWork() {
    return COMPLETION_SERVICE.submit(() -> {
        Thread.sleep(3000);
        return "hello";
    });
}

You are in control over when to poll the completed futures, like in another background thread, as shown in the example, or right before commencing new jobs.

You can test it like

Future<String> f = doSomeWork();
try {
    String s = f.get(500, TimeUnit.MILLISECONDS);
    System.out.println("got "+s);
}
catch(TimeoutException ex) {
    System.out.println("no result after 500ms");
}
catch (ExecutionException ex) {
    System.out.println("failed with "+ex);
}

if(f.cancel(true)) System.out.println("canceled");

f = doSomeWork();
// never calling get() at all

But honestly, I never understood why such complicated things are actually necessary. If you want a cleanup at the right time, you can use

static final ExecutorService MY__EXECUTOR = Executors.newCachedThreadPool();

public static Future<String> doSomeWork() {
    Callable<String> actualJob = () -> {
        Thread.sleep(3000);
        return "hello";
    };
    FutureTask<String> ft = new FutureTask<>(actualJob) {
        @Override
        protected void done() {
            System.out.println("cleanup "+this);
        }
    };
    MY__EXECUTOR.execute(ft);
    return ft;
}

to achieve the same.

Or even simpler

static final ExecutorService MY__EXECUTOR = Executors.newCachedThreadPool();

public static Future<String> doSomeWork() {
    Callable<String> actualJob = () -> {
        Thread.sleep(3000);
        return "hello";
    };
    return MY__EXECUTOR.submit(() -> {
        try {
            return actualJob.call();
        }
        finally {
            // perform cleanup
            System.out.println("cleanup");
        }
    });
}

In either case, the cleanup will be performed whether the job was completed successfully, failed, or got canceled. If cancel(true) was used and the actual job supports interruption, the cleanup also will be performed immediately after.

Upvotes: 0

BeUndead
BeUndead

Reputation: 3638

You could implement a wrapper class around Future which delegates to a different one (the one returned by wherever you're getting your Future at the moment). Something like:

final class DelegatingFuture<T> implements Future<T> {

    private final Future<T> delegate;

    DelegatingFuture(final Future<T> delegate) {
        this.delegate = Objects.requireNonNull(delegate);
    }

    // All other methods simply delegate to 'delegate'
    @Override
    public T get() 
            throws InterruptedException, ExecutionException {
        try {
            return this.delegate.get();
        } catch (final Exception ex) {
            // Handle cleanup...
            throw ex;
        }
    }

    // Something similar for get(long timeout, TimeUnit unit)
}

And then simply return new DelegatingFuture<>(currentFuture); wherever your handing these out.

Upvotes: 1

Related Questions