_This issue was originally filed by @seaneagan_
Future should allow canceling of callbacks added with Future#then. Might look like:
void cancel(void onComplete(T value));
example:
future.then(onComplete);
//...
future.cancel(onComplete);
Siggi, this is your area.
cc @kasperl.
_Set owner to @sigmundch._
_Added Area-Library, Triaged labels._
_This comment was originally written by @seaneagan_
A couple of use cases for this:
I believe the only reason for the Timer class to exist now is that a Future.delayed(...) callback cannot be canceled.
and the only reason for dart:html.requestAnimationFrame to exist is that a dart:html.animationFrame callback cannot be canceled.
However, I think the API for cancellation should look similar to how it does for Streams, where the callback subscription method returns an object which can be used to cancel the callback. It would probably look just like Future.then, except return such an object:
Canceler maybe(onValue(T value), {onError(AsyncError asyncError)});
where dart:async defines:
typedef void Canceler();
+florian who's now actively maintaining this code.
cc @floitschG.
cc @blois.
_Removed the owner._
cc @lrhn.
I don't like this proposal, for a couple reasons. From a semantic standpoint, I think Futures work best when they're consistently thought of as a way of declaring a chain of computations, rather than registering callbacks on an emitter, which is more of a Stream thing. A cancelable callback goes against this idea. From a more pragmatic angle, I've found it to be useful in various cases to create classes that implement Future, and adding more methods to it makes these classes harder to write.
Why not just use future.asStream().listen and get all the benefits of the StreamSubscription API?
_This comment was originally written by @seaneagan_
It seems to make just as much sense to be able to cancel
callbacks on a Future as a Stream. In either case something can
happen before the completion of the Future/Stream which makes you no
longer want to take action. Consider a user clicking on one data row,
resulting in a fetch (Future), then clicking on another row before the
first fetch completes. Now you have a race condition, so you want to
be able to cancel the first fetch callback before starting the second. It's like an async version of "if"(/"else"?).
It seems like implementing extra methods on Future should be handled
pretty well by a mixin of some sort, just as with Iterable, Stream,
etc.
That workaround works, but it's not quite as convenient or discoverable:
future.asStream().listen(callback)
vs:
future.maybe(callback);
I can't think of any reason to "pause" / "resume" a Future subscription, but there probably are some out there.
Listeners on futures are not "subscribers".
They are computations that expect a result of a previous computation before they can begin. You can't cancel a computation, but you are free to write your computation to do nothing in some cases.
A utility function doing what you want would be:
class Canceler {
bool _canceled = false;
Canceler(this.result);
final Future result;
void cancel() { _canceled = true; }
}
Canceler maybe(Future future, continuation(var result), {onError(error)}) {
Canceler canceler;
Completer completer = new Completer();
Future future = future.then((v) {
if (!canceler._canceled) {
return continuation(v);
} else {
return new Completer().future; // Never completes.
}
}, onError: (e) {
if (!canceler._canceled) {
if (onError != null) return onError(e);
throw e;
}
return new Completer().future;
});
canceler = new Canceler(future);
return canceler;
}
(Obviously not tested!)
I don't think we want subscriptions on futures.
We have no current plans to add canceling to futures, or to allow removing then-handlers from futures.
_Removed Type-Defect label._
_Added Type-Enhancement, NotPlanned labels._
@kwalrath This is the cancel future ticket.
@lrhn in a chat with developers today, one said that having lots of "open" futures slows down apps and is hard to diagnose. Do we have any plans to revisit this, or are there tools devs can use to hunt down these futures and fix their code?
There is no cost to having an "open" future that doesn't do computation. It's just an object, like any other object.
What costs is having computations running, and the ability to cancel that computation is not something that belongs on Future. It's something the computation itself must cooperate with.
There is no easy way to integrate cancelabilty with async functions, so most async functions would not be cancelable. The functions which are cancelable have to be written for that, and they cannot be async functions.
We can help you write such functions. I still recommend using CancelableOperation from package:async or something similar, perhaps building on that class.
import "dart:async";
import "package:async/async.dart";
CancelableOperation<T> runWithCancel<T>(
Future<T> action(Future<void> onCancel)) {
var cancel = Completer<void>();
return CancelableOperation<T>.fromFuture(action(cancel.future), onCancel: () {
cancel.complete();
return null;
});
}
main() {
var running = runWithCancel<int>((cancelFuture) async {
bool isCancelled = false;
cancelFuture.then((_) {
isCancelled = true;
});
int i = 0;
while (true) {
if (isCancelled) return -1;
print(i++);
await Future.delayed(Duration(milliseconds: 250));
}
return 42;
});
var timer = Timer(Duration(seconds: 2), running.cancel);
running.value.then(print).whenComplete(timer.cancel);
}
We still have no current plans to add cancel to Future. It's not the right place for that functionality.
As a Dart user, I think there may be a situation where a cancelable future is very useful.
When using Stream in combination with the async* syntax, it's quite normal to break the flow with a Future at some lines:
Stream<int> load10Data(Future<int> loadAt(int index)) async* {
for (var i = 0; i < 10; i++) {
yield await loadAt(i);
}
}
When a Stream from load10Data is listened to then canceled before it finishes, if await accepts something like CancelableFuture (and forward the cancellation down to the await stack), the pending loadAt would know it should do a graceful shutdown instead of hanging there.
Allowing such syntax will also make StreamTransformer.bind much easier to use, since one would be able to await on Stream#singleWhere in the passed bind function. The literal promise provided by Future may not be an obstruct here if we consider every CancelableFuture as an alternate future timeline, which is cool IMO :)
CancelableOperations will not help here because awaits only work with Futures. Of course, there's nothing stopping a programmer from replace the yield await Future to a yield* Stream call.
But Stream won't solve a bigger problem: since Future is always the first choice for library authors (lightweight and catchable), the lack of a CancelableFuture may accidentally make many cancelable actions to be non-cancelable across 3rd-parth libraries in the future, which is happening in the JavaScript community. IMHO the fetch API in browsers may be a good counterexample for weirdly making network requests non-cancelable, I really hope Dart's ecosystem would go the other way.
@pinyin For what it's worth, you can write:
Stream<int> load10Data(CancelableOperation<int> loadAt(int index)) async* {
for (var i = 0; i < 10; i++) {
yield* loadAt(i).asStream();
}
}
and it should have the behavior you want when you cancel the load10Data() stream. But that certainly is more cumbersome, and CancelableOperation living outside the core library means that many operations that are notionally cancelable don't "just work" in this way.
Your example of yield await something; where a cancel on the stream would somehow cancel the something operation is hard to define in a consistent way. It is currently completely equivalent to
var tmp = await something; yield tmp;. If cancelling the stream should affect them the same, then it means that cancelling the stream of an async* function should be able to interrupt any await in the body. Interrupting an expression is tricky, because expressions can only complete in two ways: with a value or with a throw. Neither seems right here.
If it completes with a value, then computation will continue with that value. If you have var x = await something; x.update(whatnot); yield x.whatever;, then completing with null will make the code fail badly. Completing with any other value is likely just as bad, because it STILL won't be the correct value that would allow the code to continue without issues. Throwing immediately is also unlikely to be what you want, then you would have to defensively wrap the entire body of any async* function with a catch to avoid it throwing on a cancel.
One alternative is to make it throw an AsyncCancelException and make async* function bodies implicitly catch that and complete the stream normally. That would allow cancelling an async* method at any await, not just at yields.
This was actually considered originally, but was rejected because it made it incredibly hard to reason about the code. Something as simple as:
var res = await allocateResource();
try {
. ..
} finally {
res.dispose();
}
(which is perfectly good async code, if allocation throws, nothing happens, if not, the allocated resource is definitely disposed) would not be safe for async* code. If the await cancel-throws after allocating the resource, but before entering the try/finally, then the resource is not disposed correctly.
If any await can throw (or perform any other control flow), independently of the awaited future, then it is practically impossible to write safe code.
That is why the model is that an async* method only checks for cancel at yield statements. A yield is documented as being able to bail out when the stream is cancelled, but it never happens in the middle of another expression, so expression evaluation stays rational and consistent. If you await a future, the result you get is the result of that future, not anything else injected from the side.
The only semantically reasonable option is to make a cancelled future never complete. Sadly, that's too dangerous to allow. If the future never completes, the method body is stuck at the await, and won't execute finally blocks. Example:
var res = allocateResource();
try {
while (await res.fetchNextPage()) {
for (var entry in res.currentPage) yield entry;
}
} finally {
await res.dispose();
}
Now, if this async* body was cancelled, and it cancelled the awaited future returned by res.fetchNextPage(), then execution would not continue. It would block at that await.
Which means that the resource is never disposed.
Blocking non-completing futures can already happen by accident, but when it does, it's usually a hard-to-debug error caused by some future accidentally never completing. Allowing it to happen for every await in an async* body is far too scary to contemplate.
This is all an aside from cancelable futures, saying why even if we had them, an async* stream cancel would not cancel an awaited future in the method body anyway.
It also describes some of the issues with canceling futures. If only some futures were cancelable (say, all futures had a cancel method to request a cancel, but no promise that it would do anything), then those futures would still have to figure out how to behave after cancel: complete with a default value/provided value/null (won't work when we have non-null types), throw or block.
All in all, we are not planing to make futures cancelable. The API is all wrong for that.
If we had designed futures differently from the beginning, maybe, and only maybe, would we have had a way to make them cancelable in a meaningful way.
Thanks for the replies & clear explanations.
If making Future cancelable is not an option, would moving CancelableOperation to dart:async be possible? So the problems cannot be solved by cancelable promises, but the need of something that:
async* in some way and can be automatically canceled (as @nex3 demonstrated)dart:io, to encourage 3rd-party usagemay still exist.
I'm not sure if it still belongs to this issue, but CancelableOperation seems to be covering most potential use cases for cancelable promsies, except not being in the core libraries.
This may be relevant to https://github.com/dart-lang/sdk/issues/33713.