I have two tasks: The first task (work
) is reoccurring and the second task (cleanup
) is releases some resources. The cleanup
task should be run exactly once after the reoccurring work
task has completed and will not be run again.
My first instinct was something like this:
ScheduledExecutorService service = ...;
ScheduledFuture<?> future = service.scheduleAtFixedRate(work, ...);
// other stuff happens
future.cancel(false);
cleanup.run();
The problem here is that cancel()
returns immediately. So if work
happens to be running, then cleanup
will overlap it.
Ideally I would use something like Guava's Futures.addCallback(ListenableFuture future, FutureCallback callback)
. (Guava 15 may have something like that).
In the meantime, how can fire a callback when future
is cancelled and work
no longer running?
This is the solution that I've come up with. It seems to be pretty simple, but I still assume there's a more common and/or elegant solution out there. I'd really like to see one in a library like Guava...
First I create a wrapper to impose mutual exclusion on my Runnables:
private static final class SynchronizedRunnable implements Runnable {
private final Object monitor;
private final Runnable delegate;
private SynchronizedRunnable(Object monitor, Runnable delegate) {
this.monitor = monitor;
this.delegate = delegate;
}
@Override
public void run() {
synchronized (monitor) {
delegate.run();
}
}
}
Then I create a wrapper to fire my callback on successful invokations of cancel
:
private static final class FutureWithCancelCallback<V> extends ForwardingFuture.SimpleForwardingFuture<V> {
private final Runnable callback;
private FutureWithCancelCallback(Future<V> delegate, Runnable callback) {
super(delegate);
this.callback = callback;
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
boolean cancelled = super.cancel(mayInterruptIfRunning);
if (cancelled) {
callback.run();
}
return cancelled;
}
}
Then I roll it all together in my own method:
private Future<?> scheduleWithFixedDelayAndCallback(ScheduledExecutorService service, Runnable work, long initialDelay, long delay, TimeUnit unit, Runnable cleanup) {
Object monitor = new Object();
Runnable monitoredWork = new SynchronizedRunnable(monitor, work);
Runnable monitoredCleanup = new SynchronizedRunnable(monitor, cleanup);
Future<?> rawFuture = service.scheduleAtFixedRate(monitoredWork, initialDelay, delay, unit);
Future<?> wrappedFuture = new FutureWithCancelCallback(rawFuture, monitoredCleanup);
return wrappedFuture;
}
I'll give it another shot then. Either you may enhance the command or you may wrap the executed Runnable
/Callable
. Look at this:
public static class RunnableWrapper implements Runnable {
private final Runnable original;
private final Lock lock = new ReentrantLock();
public RunnableWrapper(Runnable original) {
this.original = original;
}
public void run() {
lock.lock();
try {
this.original.run();
} finally {
lock.unlock();
}
}
public void awaitTermination() {
lock.lock();
try {
} finally {
lock.unlock();
}
}
}
So you can change your code to
ScheduledExecutorService service = ...;
RunnableWrapper wrapper = new RunnableWrapper(work);
ScheduledFuture<?> future = service.scheduleAtFixedRate(wrapper, ...);
// other stuff happens
future.cancel(false);
wrapper.awaitTermination();
cleanup.run();
After calling cancel
, either work
is no longer running and awaitTermination()
returns immediately, or it is running and awaitTermination()
blocks until it's done.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With