Skip to content

fix: prevent FD exhaustion from WebSocket reconnect loop#112

Merged
CoderGamester merged 1 commit intoCoderGamester:mainfrom
klingela:fix/fd-exhaustion-websocket-reconnect
Feb 21, 2026
Merged

fix: prevent FD exhaustion from WebSocket reconnect loop#112
CoderGamester merged 1 commit intoCoderGamester:mainfrom
klingela:fix/fd-exhaustion-websocket-reconnect

Conversation

@klingela
Copy link
Copy Markdown

Summary

Fixes #110 — Mono IOSelector FD exhaustion crash caused by WebSocket reconnect loop.

The Node.js MCP client used ws.close() for graceful shutdown, but close() leaves the socket alive during the TCP close handshake. When reconnection fires immediately, old and new sockets overlap, accumulating file descriptors on the Unity side. Since websocket-sharp uses Mono's IOSelector (select()), FD values exceeding ~1024 crash Unity with System.NotSupportedException: Could not register to wait for file descriptor N.

Changes (3 layers of defense)

  • Node.js — Always terminate(): Replace ws.close() with ws.terminate() in closeWebSocket(). Sends TCP RST, releases FD immediately, prevents overlap. Nulls ws reference and strips handlers before terminating.
  • Node.js — Cap reconnect attempts: maxReconnectAttempts from -1 (unlimited) → 50. With exponential backoff this covers ~14 hours of downtime before giving up.
  • Unity C# — Close stale sessions on connect: McpUnitySocketHandler.OnOpen() now enumerates existing sessions and closes any that aren't the new connection, cleaning up zombies from Node.js restarts.

Test Evidence

Deployed fix locally (Unity 6.3 LTS, macOS, Ultraleap project) and ran 20+ rapid MCP tool calls over 3 minutes while monitoring Unity's FD count:

Metric Baseline After stress test Delta
Total FDs 280 280 0
TCP sockets 69-70 69-70 0
Port 8090 connections 3 3 0

FD monitor log (sampled every 25s):

[09:40:43] FDs=280 TCP=70 8090_conns=3
[09:41:08] FDs=280 TCP=70 8090_conns=3
[09:41:34] FDs=280 TCP=70 8090_conns=3
[09:42:00] FDs=280 TCP=70 8090_conns=3
[09:42:25] FDs=280 TCP=69 8090_conns=3
[09:42:52] FDs=280 TCP=70 8090_conns=3
[09:43:17] FDs=280 TCP=70 8090_conns=3

Unity console confirmed stale connection cleanup working:

[MCP Unity] Closed 1 stale connection(s) to accept new client

All 96 existing tests pass (npm test).

Related

…er#110)

The Node.js MCP client used ws.close() for graceful WebSocket shutdown,
but close() leaves the socket alive during the TCP close handshake. When
reconnection fires immediately after, the old and new sockets overlap,
accumulating file descriptors on the Unity side. Since websocket-sharp
uses Mono's IOSelector (select()), FD values exceeding ~1024 crash Unity
with "System.NotSupportedException: Could not register to wait for file
descriptor N".

Three-layer fix:

1. **Node.js — Always terminate():** Replace ws.close() with
   ws.terminate() in closeWebSocket(). This sends TCP RST and releases
   the FD immediately, preventing overlap with the next connection.
   Null the ws reference and strip all handlers before terminating to
   prevent stale event callbacks.

2. **Node.js — Cap reconnect attempts:** Change maxReconnectAttempts
   from -1 (unlimited) to 50. Even with terminate(), a permanently
   unavailable server would retry forever; 50 attempts with exponential
   backoff covers ~14 hours of downtime.

3. **Unity C# — Close stale sessions on connect:** In
   McpUnitySocketHandler.OnOpen(), enumerate existing sessions and close
   any that aren't the new connection. This cleans up zombie sessions
   that survived a Node.js process restart.

Tested locally: 20+ rapid MCP tool calls over 3 minutes, FD count stayed
at exactly 280 (zero growth). Unity logs confirmed stale connection
cleanup: "Closed 1 stale connection(s) to accept new client".
Copy link
Copy Markdown
Owner

@CoderGamester CoderGamester left a comment

Choose a reason for hiding this comment

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

LGTM
All tests passed

@CoderGamester CoderGamester merged commit 007fc93 into CoderGamester:main Feb 21, 2026
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.

Mono IOSelector FD exhaustion crash from WebSocket reconnect loop

2 participants