-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Infinite loop in the async state machine when the zones async error handler throws an error #45616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
As far as I can see, we are assuming zone functions are zone agnostic (they should use their Because of that, we do not change the zone they run in, we just run them in whichever zone is current at the time they are triggered, and there is no There are several options, all potentially breaking if someone depends on the current underspecified behavior:
We can even do the usual trick of checking whether the thrown exception is the same as the incoming exception, and if so consider it a rethrow. This would be the only way for an async error to leave its error zone, by rethrowing in the error zone's error handler (because that handler should be running in the parent zone). I think we should consider documentation and the double-fault handler, and not touch the rest unless necessary. |
If the error handler causes an async error it causes an infinite cycle on all platforms. In this case there is at least user visible behavior to make it more clear what the bug is, instead of being in a cycle that doesn't involve user code. I think the second option above to run all zone functions in the parent zone would also resolve this case and could be a bit more user friendly. To repro change the import 'dart:async';
void main() {
runZoned(() {
somethingAsync();
}, zoneSpecification: ZoneSpecification(handleUncaughtError: handleError));
}
void handleError(
Zone _, ZoneDelegate __, Zone ___, Object error, StackTrace stackTrace) {
print('I see the error: $error');
Future.error('extra error');
}
void somethingAsync() async {
await Future.delayed(const Duration(seconds: 1));
throw 'sad';
} |
Running all Zone functions in the parent zone is a better design, but it's also a bigger change (almost certainly breaking some code, people tend to make assumptions about which zone their code runs in far too often), and it introduces a bigger overhead. Changing zone before calling the function, then setting it back, would be something like: T result;
var currentZone = Zone._current;
Zone._current = parentDelegate._zone;
try {
result = zoneFunction.function(zoneFunction.zone, parentDelegate, currentZone, arg1, arg2);
} finally {
Zone._current = currentZone;
}
return result; instead of just return zoneFunction.function(zoneFunction.zone, parentDelegate, Zone._current, arg1, arg2); This overhead would happen on each intercepted zone function. We'd probably add more tests to avoid the overhead for the root zone (but likely we already do that anyway): if (identical(zoneFunction.zone, Zone._root)) {
return rootZoneFunction(arg1, arg2);
}
T result;
var currentZone = Zone._current;
Zone._current = parentDelegate._zone;
try {
result = zoneFunction.function(zoneFunction.zone, parentDelegate, currentZone, arg1, arg2);
} finally {
Zone._current = currentZone;
}
return result; Changing only the uncaught error handler reduces the breaking change risk, and still avoids the infinite loop risk. |
Thanks for all the details Lasse and Nate! As for next steps. It sounds like you are leaning towards option (4): to only fix the calls to I will update this bug's label and assignment assuming that. However, if there there are changes in the dart2js lowering strategy that we need to revise to properly handle this, let me know so we can jump back on this. |
I'll just fix it in the zone itself instead of finding all the call points. |
…r parent zone. Avoids infinite recursion when the uncaught error is handled by the same, potentially still failing, uncaught error handler. Bug: #45616, #45617 Change-Id: I60ee0f1220b7345f4a41e1f1b323b8da47ed326e Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/194402 Reviewed-by: Nate Bosch <[email protected]> Reviewed-by: Sigmund Cherem <[email protected]> Auto-Submit: Lasse R.H. Nielsen <[email protected]> Commit-Queue: Lasse R.H. Nielsen <[email protected]>
Thanks again Lasse and Nate, I'll mark this issue as closed that that the change landed and relanded in 5776d57 :) |
An
async
method gets compiled to a state machine. In Dart2js this uses an exception which is thrown and caught here for some state transitions.In the case where the
Future
of anasync
is going to complete as an error, this falls through to_propagateToListeners
as it tries to complete the future. There we try to handle the error in that future's error handling zone.sdk/sdk/lib/async/future_impl.dart
Lines 620 to 621 in b5d4d26
If this throws, the exception ends up in the
catch
block for_wrapJsFunctionForAsync
which was only supposed to catch the exceptions thrown for the sake of moving through states.This ends up in an infinite loop in dart2js. The state variables aren't updated before the exception is thrown like they would be for the intended exceptions - and we end up back in the same state trying to complete the
_Future
as an error again. This time it's already complete and we (depending on compile options) either will hit anassert
about completing an already complete future, or a type error whenlistenerOrValue
isn't a listener, or another more vague error stemming from trying to use an error value as if it were a future listener. In any case the error reaches the problematiccatch
again to keep the infinite loop going.This can be demonstrated with the following. In both DDC and the VM this will not loop forever. With dart2js this will loop forever.
cc @lrhn - Do you think there anything we should be looking at in our implementation of the zone error handling, or futures, to prevent this?
The text was updated successfully, but these errors were encountered: