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).
map
val 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.