Repository navigation
fix: shut the dashboard down quietly on Ctrl+C - #10
Merged
Merged
Conversation
Ctrl+C ended in a KeyboardInterrupt traceback, and with a browser attached it took ten seconds to get there. Two causes: uvicorn shuts the application down gracefully on SIGINT and then re-raises the signal so that callers see conventional interrupt semantics. For a foreground server that only amounts to a traceback printed after a clean shutdown, so serve_dashboard now swallows it and logs "Dashboard stopped." instead. Real failures from `server.run()` still propagate. The delay was the event stream. The `/ws/events` handler only woke when the graph service yielded, so a quiet stream held the connection -- and with it the server shutdown -- until the next heartbeat, ten seconds away. Long enough that Ctrl+C looks hung and gets pressed again, and the second press tears the loop down mid-flight, which is where the messiest output came from. The handler now watches for the disconnect concurrently and stops as soon as one arrives. Measured with a live websocket: 10.05s to shut down, before; 0.16s, after. A second Ctrl+C no longer has time to land. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #8 (base is
feat/publisher-focus-on-unit-click), so the diff here is just the four files below.The reported symptom
Ctrl+C on
ezmsg dashboardends in a traceback:There turned out to be two separate causes, and the second is the one that actually made it unpleasant.
1. uvicorn re-raises the signal by design
Server.capture_signals()handles SIGINT, shuts the application down gracefully, restores the original handlers, and then callssignal.raise_signal()on what it captured — deliberately, so callers see conventional interrupt semantics. The restored handler isasyncio.Runner's, which cancels the main task and converts it intoKeyboardInterrupt.So the traceback is printed after a clean shutdown. For a foreground server that is pure noise, so
serve_dashboardnow swallowsKeyboardInterrupt/CancelledErrorfromserver.run()and logsDashboard stopped.. Anything else still propagates — there's a test for that, since it would be easy to swallow a real startup failure here.2. The event stream held shutdown open for ten seconds
With a browser attached, the first Ctrl+C took 10 seconds. The
/ws/eventshandler only woke when the graph service yielded an envelope, so on a quiet graph it sat inqueue.get()until the next heartbeat —heartbeat_secondsis 10 — and uvicorn waited for that connection before finishing shutdown.Ten seconds is long enough that Ctrl+C looks hung, so it gets pressed again — and the second press sets uvicorn's
force_exit, tearing the loop down while the lifespan task is mid-receive(). That produced the ugliest output of all, a cancelled-task dump on top of theKeyboardInterrupt.The handler now runs the envelope pump and a disconnect watcher concurrently and stops as soon as either finishes. The same change means a closed browser tab releases its graph subscriptions immediately instead of up to a heartbeat later.
Measured with a live websocket connected:
Verification
Reproduced first, against a real graph server, with a real websocket client, sending SIGINT to the process group the way a terminal does. All three cases — no websocket, websocket attached, and websocket plus a second interrupt — produced the reported traceback before and are clean after, with
Application shutdown completestill logged beforeDashboard stopped.in each.Tests: 46 backend passed (35 frontend and 28 e2e unchanged and still green).
test_serve_dashboard_exits_quietly_on_interrupt— parametrized overKeyboardInterruptandCancelledError, using a stand-in server that reproduces uvicorn's raise-after-shutdown behavior.test_serve_dashboard_still_propagates_real_errors— aRuntimeErrorfromrun()must not be swallowed.test_events_websocket_returns_when_the_client_disconnects— drives the ASGI app directly rather than throughTestClient, because TestClient cancels the application task on teardown, which hides whether the handler noticed anything itself. Confirmed it fails (TimeoutError) without the app change and passes with it — I wrote aTestClientversion first and threw it away when it passed against unfixed code.Note
The graceful path now exits 0 rather than 130. That's a deliberate choice for an interactive server stopped by its operator; if you'd rather keep the conventional interrupt status for scripting,
raise SystemExit(130)in place of thepassis the whole change.🤖 Generated with Claude Code