Skip to content

Recreate ASGIMiddleware's internal event loop after forked WSGI startup#82

Draft
Copilot wants to merge 3 commits intomasterfrom
copilot/fix-asgi-app-timeout
Draft

Recreate ASGIMiddleware's internal event loop after forked WSGI startup#82
Copilot wants to merge 3 commits intomasterfrom
copilot/fix-asgi-app-timeout

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 22, 2026

ASGIMiddleware could hang under pre-fork WSGI servers such as Passenger when the middleware was instantiated before worker fork. In that case, requests could be dispatched against an inherited asyncio loop whose backing thread no longer existed, so the ASGI app never started.

  • Root cause

    • ASGIMiddleware created and retained a background event loop at initialization time.
    • In a pre-fork deployment, worker processes inherited that loop object, but not the running thread behind it.
    • The first request then blocked waiting on a stale loop.
  • Behavior change

    • ASGIMiddleware now verifies that its internally managed loop is still valid at call time.
    • If the current PID differs from the loop creator PID, or the loop thread is no longer alive, it creates a fresh loop for the current process before building the responder.
    • Explicitly supplied loops are left untouched.
  • Regression coverage

    • Added a test that simulates pre-fork reuse by changing the observed PID and verifies the request completes.
    • Added a test that simulates a dead internal loop thread and verifies the middleware recreates the loop.
  • Implementation sketch

    def _ensure_loop(self) -> asyncio.AbstractEventLoop:
        if not self._own_loop:
            return self.loop
    
        with self._loop_lock:
            if (
                self._loop_pid != os.getpid()
                or self.loop.is_closed()
                or self._loop_thread_dead()
            ):
                self.loop = self._create_loop()
        return self.loop
Original prompt

This section details on the original issue you should resolve

<issue_title>ASGI app with ASGIMiddleware on passenger_wsgi times out</issue_title>
<issue_description>I am trying to deploy a (test) ASGI app on a server with passenger_wsgi, because that is the only interface they offer.
I wanted to test out FastAPI, but the application hangs. To eliminate any malfunctioning of FastAPI, I replaced it by a super-simple ASGI app. It still hangs.

I used the ASGIMiddleware call to convert the ASGI app to a WSGI app, and put that code in a separate Python file. For debugging I added several print statements to see what is happening.

Here is the app.py, copied from uvicorn.dev.

from sys import stderr

def debug(format, *args):
    print(format, *args, file=stderr, flush=True)

# From: https://uvicorn.dev/#quickstart
debug(f"app loaded\n")

async def app(scope, receive, send):
    """
    Simple ASGI app that returns "Hello ASGI World!".
    """

    # accept only HTTP requests
    if scope['type'] != 'http':
        debug(f"app called with {type=}\n\n")
        return

    debug(f"app called, {scope=}\n")
    # Send headers (status 200 OK)
    await send({
        'type': 'http.response.start',
        'status': 200,
        'headers': [
            [b'content-type', b'text/plain'],
        ],
    })

    # Send body
    await send({
        'type': 'http.response.body',
        'body': b'Hello ASGI World!',
    })
    debug(f"app ended\n")

This is the glue code (wsgi.py). It uses the standard ASGIMiddleware idiom, as documented.
I just wrapped the result of ASGIMiddleware in a debugging function.

from app import app, debug
from a2wsgi import ASGIMiddleware
wsgi_app = ASGIMiddleware(app)

debug(f'ASGIMiddleware set up\n')
debug(f'{wsgi_app=}\n\n')

def wapp(environ, start_response):
    debug(f'calling wsgi_app with\n {environ=}\n {start_response=}\n\n')
    retval = wsgi_app(environ, start_response)
    debug(f'wsgi_app returned: {retval}\n\n')
    return retval

Passenger uses application = wsgi.wapp

In the stderr log I see that wsgi_app returns a generator object ASGIResponder.__call__ which is correct. But my app is never called, which indicates that the generator is stuck somewhere, before calling the app. I also tried to see if the generator is really called with the following code inside the wapp function in wsgi.py:
Instead of returning retval.

    def debug_generator():
        while True:
            debug('next_val requested\n')
            next_val = next(retval)
            debug(f'{next_val=}\n')
            yield next_val
    return debug_generator()

Then I see in the log `next_val requested`, but not the next debug statement.

Here is a copy of stderr.log with the URL's and IP addresses changed to protect the innocents:

app loaded

ASGIMiddleware set up

wsgi_app=<a2wsgi.asgi.ASGIMiddleware object at 0x7fa6e83167b0>

calling wsgi_app with
environ={'wsgi.url_scheme': 'https', 'PATH_INFO': '/', 'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br, zstd', 'HTTP_ACCEPT_LANGUAGE': 'en-GB,en;q=0.9', 'HTTP_HOST': 'asgiapp.example.com', 'HTTP_USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.3.1 Safari/605.1.15', 'HTTP_SEC_FETCH_DEST': 'document', 'HTTP_SEC_FETCH_SITE': 'none', 'HTTP_SEC_FETCH_MODE': 'navigate', 'HTTP_PRIORITY': 'u=0, i', 'REMOTE_ADDR': 'xxxx:xxxx::xxxx::0:xxxx::xxxx::xxxx::xxxx:, 'REMOTE_PORT': '50143', 'SERVER_ADDR': 'xxxx::xxxx::xxxx::0:xxxx::xxxx::xxxx::xxxx:', 'SERVER_NAME': 'asgiapp.example.com', 'SERVER_ADMIN': 'webmaster@example.com', 'SERVER_PORT': '443', 'REQUEST_SCHEME': 'https', 'REQUEST_URI': '/', 'HTTPS': 'on', 'X_SPDY': 'HTTP3', 'SSL_PROTOCOL': 'QUIC', 'SSL_CIPHER': 'TLS_AES_256_GCM_SHA384', 'SSL_CIPHER_USEKEYSIZE': '256', 'SSL_CIPHER_ALGKEYSIZE': '256', 'QUERY_STRING': '', 'SCRIPT_URI': 'https://asgiapp.example.com/', 'SCRIPT_URL': '/', 'SCRIPT_NAME': '', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'LiteSpeed', 'REQUEST_METHOD': 'GET', 'X-LSCACHE': 'on', 'wsgi.version': (1, 0), 'wsgi.input': <lsapi_wsgi.InputStream object at 0x7fa6e912ac10>, 'wsgi.errors': <_io.TextIOWrapper name='' mode='w' encoding='ascii'>, 'wsgi.multithread': False, 'wsgi.multiprocess': True, 'wsgi.run_once': False, 'wsgi.file_wrapper': <class 'lsapi_wsgi.FileWrapper'>}
start_response=<built-in method start_response of lsapi_wsgi.Request object at 0x7fa6e81de880>

wsgi_app returned: <generator object ASGIResponder.call at 0x7fa6e82bf040>

[UID:1948][3810995] Child process with pid: 3811020 was killed by signal: 15, core dumped: no


## Comments on the Issue (you are @copilot in this section)

<comments>
</comments>


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes abersheeran/a2wsgi#81

<!-- START COPILOT CODING AGENT TIPS -->
---

📱 Kick off Copilot coding agent tasks wherever you are with [GitHub Mobile](https://gh.io/cca-mobile-docs), available on iOS and Android.

Copilot AI and others added 2 commits March 22, 2026 04:44
Co-authored-by: abersheeran <25364828+abersheeran@users.noreply.github.com>
Agent-Logs-Url: https://github.com/abersheeran/a2wsgi/sessions/dd95209d-67f7-4bd1-ae72-85aed3f07808
Co-authored-by: abersheeran <25364828+abersheeran@users.noreply.github.com>
Agent-Logs-Url: https://github.com/abersheeran/a2wsgi/sessions/dd95209d-67f7-4bd1-ae72-85aed3f07808
Copilot AI changed the title [WIP] Fix ASGI app timeout issue with ASGIMiddleware Recreate ASGIMiddleware's internal event loop after forked WSGI startup Mar 22, 2026
Copilot AI requested a review from abersheeran March 22, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants