Skip to content

fix(hosts): detach connection-close listener when host disconnects - #82

Merged
elementbound merged 2 commits into
foxssake:mainfrom
krazyjakee:fix/register-host-listener-leak
Sep 20, 2026
Merged

elementbound merged 2 commits into
foxssake:mainfrom
krazyjakee:fix/register-host-listener-leak

Conversation

@krazyjakee

@krazyjakee krazyjakee commented Jul 21, 2026 •

Copy link
Copy Markdown

What

handleRegisterHost adds a listener to the process-global NorayEvents bus on every register-host command and never removes it. This makes the listener self-removing so it is detached once its socket has closed.

Fixes #81.

Why

In src/hosts/host.commands.ts, each register-host does:

NorayEvents.on("connection-close", (closed) => {
  if (closed !== socket) return;
  ...
});

NorayEvents (src/events.ts) is a module-level singleton whose EventBus stores handlers in a plain array that is only ever pushed to — there was no matching .off(...) anywhere. Since clients also issue register-host, every connection (host or client) permanently appends a closure that retains socket and host. Over time:

  • Memory grows without bound (leaked closures + the sockets/hosts they capture).
  • emit("connection-close", ...) becomes O(number of connections ever made), since each disconnect walks the entire accumulated array.
  • Eventually the event loop is starved enough that the TCP socket still accepts connections while the command handler stops replying — the "port bound but process hung" symptom that requires a restart. This is the same class of unresponsiveness the dbg/unresponsive-tcp / fix/unresponsive-tcp branches appear to be investigating; those branches add diagnostics but don't detach this listener.

Reproduction

Against ghcr.io/foxssake/noray:main (v1.5.4), 15,000 connect → register-host → disconnect cycles:

memory after 15k cycles register-host reply latency
main ~29 → ~65 MiB (linear, retained) drifting upward
this PR ~31 → ~43 → plateaus ~37 MiB (GC reclaims) flat

Driver:

import socket
for _ in range(15000):
    s = socket.create_connection(("127.0.0.1", 8890), timeout=2)
    s.sendall(b"register-host\n")
    s.recv(256)
    s.close()

noray_active_hosts also stops drifting above the real host count with the fix.

Change

Extract the inline listener into a named onConnectionClose and call NorayEvents.off("connection-close", onConnectionClose) once it has handled its own socket's close. No behavioural change for a live host — only the cleanup that was missing.

@krazyjakee
krazyjakee force-pushed the fix/register-host-listener-leak branch from e426280 to 6635365 Compare July 21, 2026 23:02
krazyjakee and others added 2 commits September 20, 2026 15:22
handleRegisterHost registered a listener on the process-global NorayEvents
bus for every register-host command and never removed it. The listener array
(and the socket/host each closure retained) grew without bound, and every
connection-close emit became O(number of connections ever made). Over long
uptimes this leaked memory and starved the reactor until it stopped answering
commands while the TCP socket still accepted connections, requiring a restart.

Make the listener self-removing so it is detached once its socket has closed.
Memory then plateaus and per-disconnect cost stays constant.

Fixes foxssake#81
@elementbound
elementbound force-pushed the fix/register-host-listener-leak branch from 6635365 to 0061b67 Compare September 20, 2026 13:23
@elementbound
elementbound merged commit e5a9ff7 into foxssake:main Sep 20, 2026
4 checks passed
milkyskies added a commit to milkyskies/noray that referenced this pull request Oct 19, 2026
Adopts the Bun refactor (foxssake#77), foxssake#79, foxssake#84 and foxssake#82. Drops the pnpm files and
the setup.node action. Keeps the trimsock 0.19.0 pin (wedge fix) as
^0.19.0 for trimsock-js and trimsock-bun, and ports the reactor isolation
regression test to BunSocketReactor.

Co-Authored-By: moss <moss@milkyskies>
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.

register-host leaks a global connection-close listener per connection (host stops answering after long uptime)

2 participants