There is a reason why standard Future,
as well as many other implementations, don't provide cancel() method. It's very hard, or impossible, to create general purpose solution,
providing reliable cancellation handling for everyone. But we can create
CancellableFuture
for code following some specific constraints (see previous post).
Being able to cancel some execution is very important for interactive applications. It's especially important on Android, we expect the app
to be responsive, and at the same time don't have much resources to spare. There are several places in typical application, where we want to
stop some processing. In general this happens every time user goes to another screen or just scrolls away from loading content.
Most of the time this will be image loading (or downloading), but in some cases also other computations. CancellableFuture let's easily cancel our
asynchronous, non-blocking application.
CancelException failure result.map, flatMap) will also be cancelled.map or flatMap cancels any base future that was not yet completed.cancel logic is executed.Future. Due to API limitations (signature of map, flatMap, etc.), we are not able to extend scala Future, but we provide
easy conversions between Future and CancellableFuture.map / flatMap)Cancelling of chained futures should behave in predictable way, let's see couple examples.
val f = CancellableFuture { /* comp_1 */ }
...
f.cancel()
In this simple case future f will be cancelled (unless it completes before cancel is called) and computation comp_1
will either never happen or will be completed but its result will be discarded (depending on a time when cancellation is requested).
mapval f = CancellableFuture { /* comp_1 */ } map { _ => /* comp_2 */ }
...
f.cancel()
Again, f is cancelled if it didn't yet complete, and depending of the timing computations could either be both executed, or only comp_1 or none of them.
flatMap with already started futuresval f1 = CancellableFuture { ... }
val f2 = CancellableFuture { ... }
val f3 = f1.flatMap(_ => f2)
...
f3.cancel()
This is more interesting example, both futures (f1 and f2) start executing at the same time, but what happens when we call f3.cancel()?
f3 is cancelled (assuming it didn't yet complete)f1 is cancelled if it didn't complete before cancel() was calledf2 is either:
f1 was completed before cancel was calledcancel() was cancelled after f2 completedf1 was cancelledThe interesting part is that f2 completes successfully if f1 is cancelled, reasoning here is that we only cancel as much as we need to cancel f3,
if f1 was cancelled (returns CancelException) then f3 is cancelled regardless of the result of f2.
Actually f3 doesn't event know anything about f2 at all, since its execution never even executed the body of flatMap our execution does
def recurse(): CancellableFuture[_] =
CancellableFuture {
...
} flatMap { _ =>
recurse()
}
val f = recurse()
...
f.cancel()
We could chain futures in possibly infinite chain with recursion. Cancellation should follow the same pattern as with regular flatMap in previous example,
that means that currently executing future should be cancelled and thus recursion breaks with CancelException.
Full CancellableFuture implementation
is available on github in my utils project. It currently contains only basic methods, doesn't provide complete Future
API, but will be extended when needed.
The main implementation complexity arises from the last (recursive) use case example. CancellableFuture created by flatMap needs to keep references to
its base futures, in recursive case that means we need to keep whole chain of references to all base futures, and it's growing all the time.
Cancel request has to be propagated through this chain, and it can easily lead to stack overflow, so we need to use trampolining to prevent that.
Another issue is that we should not keep any references to completed futures and its results, as this would easily cause memory leaks.
All this concerns, together with use cases can be seen in created tests:
CancellableFutureSpec.
Aside from couple implementation issues this is quite simple class, and together with LimitedExecutionContext,
provides basic building blocks for responsive application development. It gives us great possibilities, and control of what is actually executing.
But it also requires us to strictly follow the rules described here and in previous post.
It's important to remember that cancellation is non-deterministic
and it's almost impossible to use it in any kind of transactions. Whenever we use CancellableFuture we need to remember that execution can be cancelled
at any step, and we need to make sure that it doesn't lead to inconsistent data.