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

StopIteration in a generator in a thread behaves differently in asyncio and Trio #475

Closed
tiangolo opened this issue Oct 17, 2022 · 6 comments · Fixed by #477
Closed

StopIteration in a generator in a thread behaves differently in asyncio and Trio #475

tiangolo opened this issue Oct 17, 2022 · 6 comments · Fixed by #477
Labels
bug Something isn't working

Comments

@tiangolo
Copy link
Contributor

StopIteration in a generator in a thread behaves differently in asyncio and Trio

Asyncio

This code will show an error on the terminal and hang there forever:

import anyio

def hang():
    return next(item for item in [])

async def main():
    return await anyio.to_thread.run_sync(hang)

result = anyio.run(main)

The error shows:

Exception in callback WorkerThread._report_result(<Future pendi...ask_wakeup()]>, None, StopIteration())
handle: <Handle WorkerThread._report_result(<Future pendi...ask_wakeup()]>, None, StopIteration())>
Traceback (most recent call last):
  File "/Users/user/miniconda3/envs/python3.10/lib/python3.10/asyncio/events.py", line 80, in _run
    self._context.run(self._callback, *self._args)
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 736, in _report_result
    future.set_exception(exc)
TypeError: StopIteration interacts badly with generators and cannot be raised into a Future

But I suspect the exception is being printed but not really raised, I'm not even sure. If I run it through the VS Code debugger the exception is not shown in the debugger, only printed in the terminal.

Trio

Changing only the backend to Trio:

import anyio

def hang():
    return next(item for item in [])

async def main():
    return await anyio.to_thread.run_sync(hang)

result = anyio.run(main, backend="trio")

It raises a RuntimeError wrapping (raise from) the StopIteration error, and the program stops with that error instead of hanging:

Traceback (most recent call last):
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/trio/_core/_traps.py", line 166, in wait_task_rescheduled
    return (await _async_yield(WaitTaskRescheduled(abort_func))).unwrap()
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/outcome/_impl.py", line 138, in unwrap
    raise captured_error
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/trio/_threads.py", line 157, in do_release_then_return_result
    return result.unwrap()
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/outcome/_impl.py", line 138, in unwrap
    raise captured_error
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/trio/_threads.py", line 170, in worker_fn
    ret = sync_fn(*args)
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/anyio/_backends/_trio.py", line 175, in wrapper
    return func(*args)
  File "/Users/user/code/starlette/sandt_no_ex.py", line 4, in hang
    return next(item for item in [])
StopIteration

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/user/code/starlette/sandt_no_ex.py", line 9, in <module>
    result = anyio.run(main, backend="trio")
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/anyio/_core/_eventloop.py", line 56, in run
    return asynclib.run(func, *args, **backend_options)
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/trio/_core/_run.py", line 1932, in run
    raise runner.main_task_outcome.error
  File "/Users/user/code/starlette/sandt_no_ex.py", line 7, in main
    return await anyio.to_thread.run_sync(hang)
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/anyio/to_thread.py", line 28, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(func, *args, cancellable=cancellable,
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/anyio/_backends/_trio.py", line 180, in run_sync_in_worker_thread
    return await run_sync(context.run, wrapper, cancellable=cancellable, limiter=limiter)
  File "/Users/user/code/starlette/env3.10/lib/python3.10/site-packages/trio/_threads.py", line 207, in to_thread_run_sync
    return await trio.lowlevel.wait_task_rescheduled(abort)
RuntimeError: coroutine raised StopIteration

Problem

This is a problem in FastAPI (via Starlette) because a sync (def) function that raises a StopIteration error hangs in there and never returns the response (it should return a 500 Server Error).

from fastapi import FastAPI


app = FastAPI()

@app.get("/hang")
def hang():
    return next(item for item in [])

What to do

I see several places where this could be handled.

  • On asyncio directly. Not sure how feasible that is, and it would still only work for some future Python, so the fix would still be needed in other places for current Python versions.

  • On AnyIO: it could replicate the Trio behavior, even on asyncio. I guess that would mean AnyIO would have a more predictable behavior (in this specific corner case) independent of backend. But I understand if you would prefer to keep that backend-specific and not touch that.

  • On Starlette: if you would rather not have it on AnyIO, the next place to put it would be on Starlette. Although I guess it's possible they would not want to do that and expect users to clean up their own code.

  • On FastAPI: if none of the previous places would like to have that changed.

So, here's the question. Would you like a fix/change for that here on AnyIO?

@agronholm agronholm added the bug Something isn't working label Oct 17, 2022
@agronholm
Copy link
Owner

I think the correct fix would be to replicate the Trio behavior in the asyncio backend of AnyIO.

@agronholm
Copy link
Owner

The above PR ensures identical behavior on both backends. Feel free to review.

@tiangolo
Copy link
Contributor Author

That was very fast! Amazing, thank you! 🚀 🙇 🍰

@tiangolo
Copy link
Contributor Author

tiangolo commented Nov 4, 2022

Hey there!

I hate it when people come and "ping" pushing for something open source that we do in our free time. ...and still, here I am. 🙈

Is there anything I could help with to get this included in a release?

@agronholm
Copy link
Owner

Is this a critical issue that cannot wait for AnyIO 4.0? If so, I will consider making a 3.6.3 patch release.

@tiangolo
Copy link
Contributor Author

tiangolo commented Nov 5, 2022

I just realized this is part of a bigger 4.x release. No worries, I figured out a way to workaround internally to be able to install from a fixed AnyIO git commit and monkey patch the needed parts.

agronholm added a commit that referenced this issue May 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants