pgjones / hypercorn

Hypercorn is an ASGI and WSGI Server based on Hyper libraries and inspired by Gunicorn.
MIT License
1.07k stars 95 forks source link

Cannot start WSGI application using Dispatcher middleware to support both FastAPI and Flash app at the same time. #240

Open nabheet opened 1 month ago

nabheet commented 1 month ago

Hi,

I am not sure if our use case is supported. We have FastAPI application that exposes our core API. I am hoping to add the RQ Dashboard to the same API but at /rq path. When I use the Dispatcher middleware, Hypercorn reports a timeout exception. However, if I try to only start the Flash app that is wrapped in AsyncioWSGIMiddleware, it starts up correctly.

install packages (we use pipenv, I am assuming you can replace it with pip easily)

pipenv install 'hypercorn[uvloop]==0.17.2' rq-dashboard

Here is the minimal code to reproduce the issue:

from hypercorn.middleware import (
    DispatcherMiddleware,
    AsyncioWSGIMiddleware,
)
from flask import Flask
import rq_dashboard

class RqSettings2:
    def __init__(self) -> None:
        self.RQ_DASHBOARD_REDIS_URL = "redis://redis/"

def setup_rq_dashboard() -> AsyncioWSGIMiddleware:
    app_rq_settings = RqSettings2()
    dashboard = Flask(__name__)
    dashboard.config.from_object(app_rq_settings)
    rq_dashboard.web.setup_rq_connection(dashboard)
    dashboard.register_blueprint(rq_dashboard.blueprint, url_prefix="/rq")
    return AsyncioWSGIMiddleware(dashboard)

app1 = setup_rq_dashboard()
app2 = DispatcherMiddleware(
    {
        "/rq": setup_rq_dashboard(),
        #"/": setup_fast_api() -> I am hoping to host both in the same instance 🤞 
    }
)

This works:

$ pipenv run hypercorn dash:app1
[2024-06-05 21:19:20 +0000] [751745] [INFO] Running on http://127.0.0.1:8000 (CTRL + C to quit)

This does not:

$ pipenv run hypercorn dash:app2
Process SpawnProcess-1:
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/asyncio/tasks.py", line 500, in wait_for
    return fut.result()
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/locks.py", line 213, in wait
    await fut
asyncio.exceptions.CancelledError

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

Traceback (most recent call last):
  File "/workspaces/business-api/.venv/lib/python3.11/site-packages/hypercorn/asyncio/lifespan.py", line 84, in wait_for_startup
    await asyncio.wait_for(self.startup.wait(), timeout=self.config.startup_timeout)
  File "/usr/local/lib/python3.11/asyncio/tasks.py", line 502, in wait_for
    raise exceptions.TimeoutError() from exc
TimeoutError

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

Traceback (most recent call last):
  File "/usr/local/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
    self.run()
  File "/usr/local/lib/python3.11/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/workspaces/business-api/.venv/lib/python3.11/site-packages/hypercorn/asyncio/run.py", line 196, in asyncio_worker
    _run(
  File "/workspaces/business-api/.venv/lib/python3.11/site-packages/hypercorn/asyncio/run.py", line 234, in _run
    runner.run(main(shutdown_trigger=shutdown_trigger))
  File "/usr/local/lib/python3.11/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/base_events.py", line 654, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/workspaces/business-api/.venv/lib/python3.11/site-packages/hypercorn/asyncio/run.py", line 84, in worker_serve
    await lifespan.wait_for_startup()
  File "/workspaces/business-api/.venv/lib/python3.11/site-packages/hypercorn/asyncio/lifespan.py", line 86, in wait_for_startup
    raise LifespanTimeoutError("startup") from error
hypercorn.utils.LifespanTimeoutError: Timeout whilst awaiting startup. Your application may not support the ASGI Lifespan protocol correctly, alternatively the startup_timeout configuration is incorrect.

Please advise on how best to proceed.

npt commented 1 month ago

I have a similar-looking issue: when I use DispatcherMiddleware to run a Quart app that uses before_serving (with only the one app in the routes), and the before_serving method throws an exception, the exception is logged, the server hangs (not responding to anything except SIGKILL) for several seconds, and then I get the same chain of CancelledError / TimeoutError / LifespanTimeoutError. When DispatcherMiddleware isn't involved, the program immediately terminates as expected.

Aside from the issue when a startup routine throws an exception, when I use DispatcherMiddleware wrapping the one app and exit the server with Ctrl-C, I receive this error:

Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File ".../__main__.py", line 69, in <module>
    asyncio.run(serve(dispatcher, config))
  File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/asyncio/base_events.py", line 664, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File ".../.venv/lib/python3.12/site-packages/hypercorn/asyncio/__init__.py", line 44, in serve
    await worker_serve(
  File ".../.venv/lib/python3.12/site-packages/hypercorn/asyncio/run.py", line 181, in worker_serve
    await lifespan_task
  File ".../.venv/lib/python3.12/site-packages/hypercorn/asyncio/lifespan.py", line 55, in handle_lifespan
    await self.app(
  File ".../.venv/lib/python3.12/site-packages/hypercorn/app_wrappers.py", line 34, in __call__
    await self.app(scope, receive, send)
  File ".../src/connector/__main__.py", line 57, in __call__
    await self.app(scope, receive, send)
  File ".../.venv/lib/python3.12/site-packages/hypercorn/middleware/dispatcher.py", line 19, in __call__
    await self._handle_lifespan(scope, receive, send)
  File ".../.venv/lib/python3.12/site-packages/hypercorn/middleware/dispatcher.py", line 46, in _handle_lifespan
    async with TaskGroup(asyncio.get_event_loop()) as task_group:
  File ".../.venv/lib/python3.12/site-packages/hypercorn/asyncio/task_group.py", line 74, in __aexit__
    await self._task_group.__aexit__(exc_type, exc_value, tb)
  File "/usr/lib/python3.12/asyncio/taskgroups.py", line 136, in __aexit__
    raise propagate_cancellation_error
  File "/usr/lib/python3.12/asyncio/taskgroups.py", line 112, in __aexit__
    await self._on_completed_fut
asyncio.exceptions.CancelledError

No such error occurs when not using DispatcherMiddleware.