Skip to content

Self-remove event subscriber on WebSocketClosedError - #1680

Merged
Zsailer merged 5 commits into
jupyter-server:mainfrom
Zsailer:fix-events-self-remove-on-closed-socket
Jul 17, 2026
Merged

Self-remove event subscriber on WebSocketClosedError#1680
Zsailer merged 5 commits into
jupyter-server:mainfrom
Zsailer:fix-events-self-remove-on-closed-socket

Conversation

@Zsailer

@Zsailer Zsailer commented Jul 17, 2026

Copy link
Copy Markdown
Member

SubscribeWebsocket.event_listener writes to the subscriber's WebSocket without guarding against WebSocketClosedError, and only cleans up through on_close, which isn't reliably fired when the peer disappears without a clean close handshake (network drop, browser tab killed, stream reset).

When that happens, the stale bound event_listener stays in event_logger._modified_listeners. Since it's a bound method, it keeps a strong reference to its SubscribeWebsocket instance, and therefore to the tornado WebSocket handler, the request, and any state captured during the handshake. Nothing ever removes it, so it lives until the process exits. This is a memory leak that grows linearly with the number of unclean disconnects over the server's lifetime.

The fix guards write_message with a WebSocketClosedError handler and removes the listener from the logger when the write fails. EventLogger.remove_listener uses set.discard, so double-remove is a no-op.

Zsailer and others added 2 commits July 17, 2026 09:37
SubscribeWebsocket.event_listener wrote to the socket without guarding
against WebSocketClosedError and only cleaned up through on_close,
which doesnt reliably fire when the peer disappears without a clean
close handshake. The stale bound event_listener kept its
SubscribeWebsocket instance (and everything the instance retained)
pinned in the event loggers listener set for the life of the process,
producing a memory leak that grew linearly with the number of unclean
disconnects.

Guard write_message and remove the listener from the logger on
WebSocketClosedError. Remove_listener uses set.discard, so
double-remove is a no-op.
@Zsailer Zsailer added the bug label Jul 17, 2026
@Zsailer
Zsailer marked this pull request as ready for review July 17, 2026 16:50

@krassowski krassowski left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me, just the docstring looks not like something I would want to read in docs. Something shorter documenting behaviour rather than imperative code comment would be better there IMO.

Comment thread jupyter_server/services/events/handlers.py Outdated
@Zsailer

Zsailer commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

In full transparency, I patched this code a while ago internally (human written), then realized I never opened a PR to fix upstream.

I used AI to port my changes over (hench, why it went hard on the docstring 😆 ). Acknowledging here that I used AI.

Co-authored-by: Michał Krassowski <5832902+krassowski@users.noreply.github.com>
Comment thread jupyter_server/services/events/handlers.py Outdated
Comment thread jupyter_server/services/events/handlers.py Outdated
@Zsailer
Zsailer enabled auto-merge (squash) July 17, 2026 18:18
@Zsailer
Zsailer merged commit ceaaf4f into jupyter-server:main Jul 17, 2026
38 checks passed
@krassowski krassowski changed the title [Bug] Self-remove event subscriber on WebSocketClosedError Self-remove event subscriber on WebSocketClosedError Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants