How to interrupt underlying execution of CompletableFuture

17,717

Solution 1

A CompletableFuture is not related to the asynchronous action that may eventually complete it.

Since (unlike FutureTask) this class has no direct control over the computation that causes it to be completed, cancellation is treated as just another form of exceptional completion. Method cancel has the same effect as completeExceptionally(new CancellationException()).

There may not even be a separate thread working on completing it (there may even be many threads working on it). Even if there is, there's no link from a CompletableFuture to any thread that has a reference to it.

As such, there's nothing you can do through CompletableFuture to interrupt any thread that may be running some task that will complete it. You'll have to write your own logic which tracks any Thread instances which acquire a reference to the CompletableFuture with the intention to complete it.


Here's an example of the type of execution I think you could get away with.

public static void main(String[] args) throws Exception {
    ExecutorService service = Executors.newFixedThreadPool(1);
    CompletableFuture<String> completable = new CompletableFuture<>();
    Future<?> future = service.submit(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                if (Thread.interrupted()) {
                    return; // remains uncompleted
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    return; // remains uncompleted
                }
            }
            completable.complete("done");
        }
    });

    Thread.sleep(2000);

    // not atomic across the two
    boolean cancelled = future.cancel(true);
    if (cancelled)
        completable.cancel(true); // may not have been cancelled if execution has already completed
    if (completable.isCancelled()) {
        System.out.println("cancelled");
    } else if (completable.isCompletedExceptionally()) {
        System.out.println("exception");
    } else {
        System.out.println("success");
    }
    service.shutdown();
}

This assumes that the task being executed is setup to handle interruptions correctly.

Solution 2

What about this?

public static <T> CompletableFuture<T> supplyAsync(final Supplier<T> supplier) {

    final ExecutorService executorService = Executors.newFixedThreadPool(1);

    final CompletableFuture<T> cf = new CompletableFuture<T>() {
        @Override
        public boolean complete(T value) {
            if (isDone()) {
                return false;
            }
            executorService.shutdownNow();
            return super.complete(value);
        }

        @Override
        public boolean completeExceptionally(Throwable ex) {
            if (isDone()) {
                return false;
            }
            executorService.shutdownNow();
            return super.completeExceptionally(ex);
        }
    };

    // submit task
    executorService.submit(() -> {
        try {
            cf.complete(supplier.get());
        } catch (Throwable ex) {
            cf.completeExceptionally(ex);
        }
    });

    return cf;
}

Simple Test:

    CompletableFuture<String> cf = supplyAsync(() -> {
        try {
            Thread.sleep(1000L);
        } catch (Exception e) {
            System.out.println("got interrupted");
            return "got interrupted";
        }
        System.out.println("normal complete");
        return "normal complete";
    });

    cf.complete("manual complete");
    System.out.println(cf.get());

I don't like the idea of having to create an Executor service every time, but maybe you can find a way to reuse the ForkJoinPool.

Solution 3

If you use

cf.get();

instead of

cf.join();

The thread waiting on the completion can be interrupted. This bit me in the a**, so I'm just putting it out there. You'd then need to propagate this interruption further / use cf.cancel(...) to really finish the execution.

Solution 4

I had similar issue wherein I needed to simulate a InterruptedException.

I mocked the method call that is supposed to return the CompletetableFuture, and I put a spy on return value such that CompletableFuture#get will throw the exception.

It worked as I expected, and I was able to test that code handled the exception correctly.

        CompletableFuture spiedFuture = spy(CompletableFuture.completedFuture(null));
        when(spiedFuture .get()).thenThrow(new InterruptedException());

        when(servuce.getById(anyString())).thenReturn(spiedFuture );

Solution 5

Here is a ultra-short version to create a Future task that can be cancelled:

public static <T> Future<T> supplyAsync(Function<Future<T>, T> operation) {
    CompletableFuture<T> future = new CompletableFuture<>();
    return future.completeAsync(() -> operation.apply(future));
}

The CompletableFuture is passed to the operation Function to be able to check the cancel status of the Future:

Future<Result> future = supplyAsync(task -> {
   while (!task.isCancelled()) {
       // computation
   }
   return result;
});
// later you may cancel
future.cancel(false);
// or retrieve the result
Result result = future.get(5, TimeUnit.SECONDS);

This however does not interrupt the Thread running the operation. If you also want to be able to interrupt the Thread, then you have to store a reference to it and override Future.cancel(..) to interrupt it.

public static <T> Future<T> supplyAsync(Function<Future<T>, T> action) {
    return supplyAsync(action, r -> new Thread(r).start());
}

public static <T> Future<T> supplyAsync(Function<Future<T>, T> action, Executor executor) {

    AtomicReference<Thread> interruptThread = new AtomicReference<>();
    CompletableFuture<T> future = new CompletableFuture<>() {

        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            if (!interruptThread.compareAndSet(null, Thread.currentThread()) 
                   && mayInterruptIfRunning) {
                interruptThread.get().interrupt();
            }
            return super.cancel(mayInterruptIfRunning);
        }
    };

    executor.execute(() -> {
        if (interruptThread.compareAndSet(null, Thread.currentThread())) try {
            future.complete(action.apply(future));
        } catch (Throwable e) {
            future.completeExceptionally(e);
        }
    });

    return future;
}

The following test checks that the Thread executing our Function got interrupted:

@Test
void supplyAsyncWithCancelOnInterrupt() throws Exception {
    Object lock = new Object();
    CountDownLatch done = new CountDownLatch(1);
    CountDownLatch started = new CountDownLatch(1);

    Future<Object> future = supplyAsync(m -> {
        started.countDown();
        synchronized (lock) {
            try {
                lock.wait(); // let's get interrupted
            } catch (InterruptedException e) {
                done.countDown();
            }
        }
        return null;
    });

    assertFalse(future.isCancelled());
    assertFalse(future.isDone());

    assertTrue(started.await(5, TimeUnit.SECONDS));
    assertTrue(future.cancel(true));

    assertTrue(future.isCancelled());
    assertTrue(future.isDone());
    assertThrows(CancellationException.class, () -> future.get());
    assertTrue(done.await(5, TimeUnit.SECONDS));
}
Share:
17,717
vach
Author by

vach

Updated on June 28, 2022

Comments

  • vach
    vach almost 2 years

    I know that CompletableFuture design does not control its execution with interruptions, but I suppose some of you might have this problem. CompletableFutures are very good way to compose async execution, but given the case when you want the underlying execution to be interrupted or stopped when future is canceled, how do we do that? Or we must just accept that any canceled or manually completed CompletableFuture will not impact the thread working out there to complete it?

    That is, in my opinion, obviously a useless work that takes time of executor worker. I wonder what approach or design might help in this case?

    UPDATE

    Here is a simple test for this

    public class SimpleTest {
    
      @Test
      public void testCompletableFuture() throws Exception {
        CompletableFuture<Void> cf = CompletableFuture.runAsync(()->longOperation());
    
        bearSleep(1);
    
        //cf.cancel(true);
        cf.complete(null);
    
        System.out.println("it should die now already");
        bearSleep(7);
      }
    
      public static void longOperation(){
        System.out.println("started");
        bearSleep(5);
        System.out.println("completed");
      }
    
      private static void bearSleep(long seconds){
        try {
          TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
          System.out.println("OMG!!! Interrupt!!!");
        }
      }
    }