Skip to content

Keep an accepted connection's peer instead of re-reading it at cleanup - #14

Open
FezVrasta wants to merge 1 commit into
Herbertmt978:masterfrom
FezVrasta:fix/grottserver-connection-cleanup
Open

Keep an accepted connection's peer instead of re-reading it at cleanup#14
FezVrasta wants to merge 1 commit into
Herbertmt978:masterfrom
FezVrasta:fix/grottserver-connection-cleanup

Conversation

@FezVrasta

Copy link
Copy Markdown

Independent of #13 — both touch handle_new_connection, but in different hunks, so they merge cleanly in either order.

The bug

close_connection() and handle_writable_socket() both ask the socket who its peer is, at points that only occur once the peer is already gone. getpeername() raises there, and neither handler survives it.

close_connection() removes the socket from inputs/outputs, then calls getpeername() and gets [Errno 107] Transport endpoint is not connected. Everything after that line is skipped:

  • del send_queuereg[qname] never runs
  • the loggerreg entry for the datalogger is never cleaned up
  • s.close() never runs — the fd is only released when refcounting happens to collect the socket

handle_writable_socket() wraps the same call in a bare except that prints and falls through with pass, leaving client_address unbound. The very next line builds qname from it and raises NameError, caught by the generic handler.

Observed on a live add-on, one full disconnect cycle:

 - Grottserver - socket closed :
 - Grottserver - exception in server thread - handle_writable_socket :  cannot access local variable 'client_address' where it is not associated with a value
 - Grottserver - Close connection :  <socket.socket fd=5, ...>
 - Grottserver - exception in server thread - close connection : [Errno 107] Transport endpoint is not connected

The datalogger there reconnects about once a minute, so this produced roughly 140k log lines a day and buried every real error.

The fix

Record the peer in self.peers when the connection is accepted — while it is still reachable — and read it from there afterwards.

close_connection() now closes the socket in a finally block, so a failure anywhere above can no longer strand the fd, and it tolerates being called twice (the select loop can offer an already-reaped socket).

No protocol or MQTT behaviour changes.

Tests

tests/test_grottserver_connection_cleanup.py, 9 cases over real loopback sockets: the peer is recorded on accept, cleanup after a disconnect drops the send queue and closes the fd, deregistration is complete, a double close is harmless, the writable handler reports no exception for a socket it no longer knows, and it still delivers queued responses.

Two of them assert structurally (AST) that neither handler calls getpeername() at all. Whether that call actually raises is the kernel's timing to decide — on macOS loopback it can still succeed right after the client closes — so racing a disconnect would make the regression guard flaky. Asserting the call is gone is deterministic on every platform.

ruff check --select E9,F63,F7,F82 . passes. The 36 pre-existing failures in test_release_validation.py / test_release_metadata.py / test_addon_runtime_config.py reproduce unchanged on master — they gate on a clean worktree and release environment.

close_connection() and handle_writable_socket() both asked the socket
itself who the peer was, at points that only happen once the peer is
already gone. getpeername() raises there, and neither handler survived
it.

close_connection() removed the socket from inputs/outputs, then called
getpeername() and got [Errno 107]. Everything after it was skipped: the
send_queuereg entry was never deleted, the loggerreg entry for the
datalogger was never cleaned up, and the socket was never closed - the
fd only went away when refcounting happened to collect it.

handle_writable_socket() wrapped the same call in a bare except that
merely printed and fell through, leaving client_address unbound so the
next line raised NameError into the generic handler. On a datalogger
that reconnects every minute that is two "exception in server thread"
lines per disconnect, which was ~140k log lines a day on the reporter's
add-on and buried every real error.

Record the peer in self.peers when the connection is accepted, while it
is still reachable, and read it from there. close_connection() now
closes the socket in a finally block so a failure anywhere above it can
no longer strand the fd, and tolerates being called twice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tusdu4cS5cacEms4ruvoTt
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.

1 participant