Skip to content
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

Cancelling Async Generator Hangs Indefinitely #59793

Open
tustvold opened this issue Dec 22, 2024 · 2 comments
Open

Cancelling Async Generator Hangs Indefinitely #59793

tustvold opened this issue Dec 22, 2024 · 2 comments
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)

Comments

@tustvold
Copy link

tustvold commented Dec 22, 2024

Problem

Given the following stream

Stream<String> foo() {
  final controller = StreamController<String>();
  controller.onListen = () => controller.add("FOO");
  controller.onCancel = () => print("Shutting down");
  controller.onPause = () => print("Pause");
  controller.onResume = () => print("Resume");
  return controller.stream;
}

If we then wrap this in

Stream<String> watch() async* {
  final response = foo();
  final mapped = response.map((response) {
    return response.toUpperCase();
  });
  yield* mapped;
}

Everything works correctly

Future main() async {
  final stream = watch().listen((x) => print(x));
  
  await Future.delayed(Duration(seconds: 1));
  
  await stream.cancel();
  print("finished");
}

Prints as expected

FOO
Shutting down
finished

If, however, we change watch to be

Stream<String> watch() async* {
  final response = foo();
  await for (var response in response) {
    yield response.toUpperCase();
  }
}

Cancellation hangs indefinitely.

The specification has the following to say on the topic.

The stream associated with an asynchronous generator could be canceled
by any code with a reference to that stream at any point
where the generator was passivated.
Such a cancellation constitutes an irretrievable error for the generator.
At this point, the only plausible action for the generator is
to clean up after itself via its \FINALLY{} clauses.%

I'm not entirely sure what passivated means, but I think hanging indefinitely is at least not an obvious behaviour if it is intentional.

Platform

I have tested this on Linux (flutter), Android (flutter) and DartPad and the behaviour seems to be consistent

@dart-github-bot
Copy link
Collaborator

Summary: Async generator using await for with a cancellable stream hangs indefinitely when the stream is cancelled, contrary to expected behavior and specification. The oncancel callback isn't triggered.

@dart-github-bot dart-github-bot added area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Dec 22, 2024
@tustvold
Copy link
Author

tustvold commented Dec 22, 2024

Interestingly if you modify the reproducer to be

Stream<String> foo() {
  final controller = StreamController<String>();
  controller.onListen = () {
    controller.add("FOO");
    Future.delayed(Duration(seconds: 2), () => controller.add("BAR"));
  };
  controller.onCancel = () => print("Shutting down");
  controller.onPause = () => print("Pause");
  controller.onResume = () => print("Resume");
  return controller.stream;
}

The generator shuts down once BAR is produced, in which case this may be related to, or a duplicate of, #45645.

In particular

Our async* functions are implemented incorrectly on some platforms (maybe all, not sure any more). They do not check after the yield has synchronously delivered an event whether they have been cancelled or paused, they just continue execution.
That means that the subscription.cancel call must wait until the next yield to exit the async* function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-core-library SDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries. triage-automation See https://github.com/dart-lang/ecosystem/tree/main/pkgs/sdk_triage_bot. type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)
Projects
None yet
Development

No branches or pull requests

2 participants