The fork-per-message model spawns a child per inbound message; each child
exit(0)s when done. The only reaper was a single, non-looping
pcntl_waitpid(-1, WNOHANG) inside the IPC onClose callback — which fires on
socket EOF, an event that routinely precedes the child's actual process exit,
so it frequently reaped nothing and left <defunct> zombies parented to the
live serve process (where no PID 1 init can reach them). Under sustained
traffic these accumulated until pcntl_fork() itself failed.
- Handler::onClose now drains ALL exited children in a loop (was a single
wait). activeChildCount bookkeeping stays per-socket, untouched.
- StartServer adds a SIGCHLD handler + a 10s periodic backstop (catches
children that crashed before IPC setup, so no onClose fires) + a
drain-on-shutdown so a deploy restart does not hand orphans to PID 1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Handler::cacheAuthenticatedUser() and ::cleanupChannelConnections() were
writing/forgetting ws_socket_auth_<rawSocketId> while
WebsocketService::getAuth() and ::setUserAuthed() have always slugged
("123.456" → "123-456"). Result: the cache write was reachable from the
package's own writer path but not from the service-layer reader, so the
admin tooling (websockets:watch -v) saw cache misses and rendered #<id>
instead of the configured IdentityFormatter output.
Also: WatchStats now batch-loads missing users via the configured auth
provider model in one query per render, so the User column still renders
the full formatter shape even when the per-socket cache blob predates
the writer or got evicted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verbose mode reuses Symfony's built-in -v flag (the short option is
reserved, so a custom one can't be added). Each channel summary is
followed by sub-rows showing the socket id, the authed user (falling
back to "Guest" or the user id if the user blob expired), and how long
the connection has been open.
Connection start times are written by Handler::establishConnection() into
ws_connection_<slug(socketId)> on open and forgotten on close. Also
fixed a latent bug in finalizeConnectionClose() where the forget key was
missing the slug() call that the reader has always used — previously
invisible because nothing wrote the key, now load-bearing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Introduced `helpers-and-testing.md` to document global helpers and WebsocketService class usage.
- Created `HandlerLifecycleTest.php` to test the full WebSocket handler lifecycle, including connection management, channel subscriptions, and message routing.
- Added `WebsocketServiceTest.php` to validate state tracking methods in WebsocketService, covering user authentication, channel tracking, and broadcast functionality.