I'm using a ScheduledExecutorService and submitting a task like that:
future = scheduledExecutorService.schedule(myRunnableTask, delay, timeunit)
However a certain event might occur after indefinite amount of time, which signals that this task is no longer needed. And so I need to cancel this task, and I am using
boolean cancelled = future.cancel(false)
line.
After cancelling, I have to take different actions depending on whether the submitted runnable actually ran or not. And here lets first jump into Oracle Documentation and read what cancelled
flag means:
https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html#cancel(boolean)
Returns: false if the task could not be cancelled, typically because it has already completed normally; true otherwise
That's all it says about the return value. Seems like the person who wrote this text line was uncertain about false
return value here, but I think I can take it.
Lets now focus on the case, when it returns true
. There are two possibilities here:
I am okay with both cases occurring, but I want to KNOW which one actually occurred and take actions accordingly. If the runnable is in the process, then I am okay with it finishing it's job, I want to wait for it's completion and then do one thing. But if it was cancelled and never going to run at all, I want to do another thing.
Can you please recommend an approach to this? Am I missing something?
I ended up writing something like this for this issue. Source code and some unit tests can be found at https://github.com/nuzayats/cancellabletaskexecutor
public class CancellableTaskExecutor {
private final ScheduledExecutorService es;
private final Logger log;
/**
* For a unit test to replicate a particular timing
*/
private final Runnable hookBetweenCancels;
public CancellableTaskExecutor(ScheduledExecutorService es, Logger log) {
this(es, log, () -> {
// nop
});
}
// For unit tests
CancellableTaskExecutor(ScheduledExecutorService es, Logger log, Runnable hookBetweenCancels) {
this.es = es;
this.log = log;
this.hookBetweenCancels = hookBetweenCancels;
}
public Execution schedule(Runnable task, long delay, TimeUnit unit) {
CancellableRunnable runnable = new CancellableRunnable(task);
ScheduledFuture<?> future = es.schedule(runnable, delay, unit);
return new Execution(future, runnable);
}
public class Execution {
private final ScheduledFuture<?> future;
private final CancellableRunnable runnable;
private Execution(ScheduledFuture<?> future, CancellableRunnable runnable) {
this.future = future;
this.runnable = runnable;
}
/**
* @return true when the task has been successfully cancelled and it's guaranteed that
* the task won't get executed. otherwise false
*/
public boolean cancel() {
boolean cancelled = runnable.cancel();
hookBetweenCancels.run();
// the return value of this call is unreliable; see https://stackoverflow.com/q/55922874/3591946
future.cancel(false);
return cancelled;
}
}
private class CancellableRunnable implements Runnable {
private final AtomicBoolean cancelledOrStarted = new AtomicBoolean();
private final Runnable task;
private CancellableRunnable(Runnable task) {
this.task = task;
}
@Override
public void run() {
if (!cancelledOrStarted.compareAndSet(false, true)) {
return; // cancelled, forget about the task
}
try {
task.run();
} catch (Throwable e) {
log.log(Level.WARNING, "Uncaught Exception", e);
}
}
boolean cancel() {
return cancelledOrStarted.compareAndSet(false, true);
}
}
}