Skip to content

Bugfix: Fix quart threadedrunner stop graceful shutdown#3824

Open
joschrag wants to merge 3 commits into
plotly:devfrom
joschrag:fix/quart-threadedrunner-stop-graceful-shutdown
Open

Bugfix: Fix quart threadedrunner stop graceful shutdown#3824
joschrag wants to merge 3 commits into
plotly:devfrom
joschrag:fix/quart-threadedrunner-stop-graceful-shutdown

Conversation

@joschrag

Copy link
Copy Markdown

dash.testing's ThreadedRunner.stop() only has a graceful-shutdown branch for FastAPI — it keys off hasattr(self._app, "_uvicorn_server"). A Quart app has no such attribute, so it falls through to the else branch: thread.kill() injects an asynchronous SystemExit, then thread.join() is called with no timeout.

the Quart backend already serves with serve(shutdown_trigger=...) awaiting backend._ws_shutdown_event; only the main-thread signal handler ever sets it, and in tests the server runs in a worker thread. stop() now sets that event itself — thread-safely, via loop.call_soon_threadsafe(event.set) on the server's own loop — then joins bounded by stop_timeout.

Closes #3823

Contributor Checklist

  • I have broken down my PR scope into the following TODO tasks
    • Add a graceful Quart shutdown branch to ThreadedRunner.stop()
    • Add a regression test covering the teardown hang
  • I have run the tests locally and they passed. (refer to testing section in contributing)
  • I have added tests, or extended existing tests, to cover any new features or bugs fixed in this PR

optionals

  • I have added entry in the CHANGELOG.md
  • If this PR needs a follow-up in dash docs, community thread, I have mentioned the relevant URLS as follows
    • this GitHub #PR number updates the dash docs
    • here is the show and tell thread in Plotly Dash community

joschrag added 2 commits June 17, 2026 17:10
ThreadedRunner.stop() only had a graceful shutdown branch for FastAPI
(keyed on `_uvicorn_server`). A Quart app fell through to the kill-based
branch, which injects an async SystemExit via thread.kill() and then calls
join() with no timeout. The server thread is parked in a blocking syscall
(IOCP on Windows, epoll on POSIX), so the SystemExit is not delivered
promptly and join() can hang indefinitely -- on Windows and Linux alike.

Add a Quart branch that signals the backend's existing cooperative shutdown
switch (backend._ws_shutdown_event) thread-safely on the server's own loop
via call_soon_threadsafe, then joins bounded by stop_timeout. Flask/other
backends keep the kill path unchanged.
Add a dash.testing regression test that starts a Quart app on ThreadedRunner
and asserts stop() returns bounded by stop_timeout (run under a watchdog so a
regression fails fast instead of wedging the suite). Verified it fails against
the unpatched stop() and passes with the fix.
@sonarqubecloud

Copy link
Copy Markdown

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.

[BUG] dash.testing ThreadedRunner.stop() hangs at teardown for Quart backend apps

1 participant