Skip to content

fix: stop the coordinator piling up beacon sockets on port 19877 - #477

Merged
Mearman merged 5 commits into
mainfrom
fix/udp-socket-leak-476
Nov 3, 2026
Merged

Mearman merged 5 commits into
mainfrom
fix/udp-socket-leak-476

Conversation

@Mearman

@Mearman Mearman commented Nov 3, 2026

Copy link
Copy Markdown
Member

Fixes #476

The process holding many sockets on *:19877 is the machine's coordinator bridge, which runs the default cc-peer front. Every store the front builds for a fronted Claude Code session went through createBridgeMeshFromIdentity, whose firstContactPort defaults to the shared beacon port, so the coordinator bound one more reuseAddr socket on that port per fronted session (a live coordinator here held 14 on 19877 next to 15 loopback data listeners, one per fronted store, while every other bridge process held 1). With reuseAddr each of those sockets receives every probe and beacon on the machine and answers every probe with a multicast plus a broadcast beacon, so traffic and CPU grow with the number of fronted sessions. The front then leaked stores on top of that: ticks run on a fixed interval while an attach can wait out a room join's approval window, so the same session was attached again each tick and only the last record kept, and a failed attach or detach left its store running.

The changes:

  • The front builds its per-session stores with firstContactPort: false (new option on the bridge-mesh factories): they live in the coordinator's process and reach the mesh through its coordinator port, so they need no presence.
  • CcPeerFront skips a session whose attach is still in flight, and detaches a record whose attach finishes after stop().
  • A failed attach shuts its store down before rethrowing, and detach shuts the store down even when marking the agent offline fails.
  • MdnsDiscoveryBackend shares one bind promise across concurrent callers, keeps (never replaces) a bound socket that reports an error, closes and rejects on a failed bind instead of hanging, and stop() waits for a bind in flight.
  • MeshStore.shutdown() now stops the discovery backends, so an advertisement's beacon socket no longer outlives its store.

Each fix has a test that failed before it: src/test/discovery-mdns.unit.test.ts, the "an attach outlasting the poll interval" block in cc-peer-front-controller.unit.test.ts, the attachFrontedSession and detach-failure cases in cc-peer-front-relay.unit.test.ts, cc-peer-front-runtime-mesh.unit.test.ts, the two firstContactPort socket-count cases in bridge-mesh.integration.test.ts, and the discovery-backend case in mesh-store-orchestration.unit.test.ts.

Concurrent startAdvertising and discover calls each passed the isListening check
before the first bind finished, so each bound its own socket
and the last one overwrote this.socket, leaving the others open on the beacon port.
An error event flipped isListening off without closing the socket,
so the next call bound a second socket beside it,
and a failed bind never called back, leaving its caller waiting forever.

Every caller now awaits one shared bind promise,
an error on a bound socket no longer replaces it,
a failed bind closes its socket and rejects,
and stop() waits for a bind in flight so the socket it produces is the one closed.
The port and socket factory are injectable so tests count real sockets.

Refs #476
The front polls on a fixed interval whether or not the previous tick finished,
and an attach can wait out a room join's approval window, many intervals long.
Every tick in that window attached the same session again,
each attach building another mesh store with its own sockets,
and only the last record to finish was kept, so the rest ran for the life of the process.
An attach finishing after stop() was likewise kept fronted with nothing to detach it.

Sessions with an attach in flight are now skipped,
and a record whose attach finishes after stop() is detached at once.

Refs #476
A fronted session's store was built before registration and its project room,
so a failure in either left the store running
while the front retried on the next tick with a new one.
Detach likewise skipped shutdown when marking the agent offline failed.
Each failure left a running store, with its own sockets and timers, behind.

A failed attach now shuts its store down before rethrowing,
and detach shuts the store down whether or not the offline patch went out.

Refs #476
…sence

Every store the cc-peer front built for a fronted session went through
createBridgeMeshFromIdentity, which defaults firstContactPort to the shared beacon port,
so the coordinator process bound one more UDP socket on that port
for every Claude Code session it fronted.
With reuseAddr every one of those sockets received every probe and beacon on the machine
and answered each probe with a multicast and a broadcast beacon of its own,
multiplying the traffic by the number of fronted sessions.

The fronted stores live in the coordinator's process
and reach the mesh through its coordinator port, so they need no presence:
createBridgeMesh now takes firstContactPort false to build a store without one,
and the front passes it.

Fixes #476
MeshStore.shutdown() stopped the first-contact presence but never its DiscoveryManager,
so a store that had run mesh_advertise kept its mDNS beacon socket and timer
on the beacon port for the life of the process after it shut down.

DiscoveryManager gains stop(), which stops every backend and forgets every advertisement,
and shutdown() calls it.
The default backend set moves into createDefaultDiscovery
so the store names no concrete backend.

Refs #476
@Mearman
Mearman marked this pull request as ready for review November 3, 2026 15:31
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ⚠️ Failed 2026-11-03T15:31:16.666374Z
We couldn’t complete this review. Comment @codex security review to try again.
f82bcef Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

This security review couldn’t start because the paying account has exhausted its Codex allowance and has insufficient available credits. The payer can check their balance and add credits in the Codex usage dashboard, then try again. If you do not manage the paying account, contact this repository's admins.

@Mearman
Mearman merged commit ff7d171 into main Nov 3, 2026
8 checks passed
@Mearman
Mearman deleted the fix/udp-socket-leak-476 branch November 3, 2026 15:31
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 8.15.23 🎉

The release is available on:

Your semantic-release bot 📦🚀

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.

Process accumulates UDP sockets on the beacon port and saturates CPU

1 participant