diff --git a/src/Websocket/Handler.php b/src/Websocket/Handler.php index f6bb7aa..e9b45fb 100644 --- a/src/Websocket/Handler.php +++ b/src/Websocket/Handler.php @@ -590,13 +590,44 @@ class Handler implements MessageComponentInterface // This saves ~5-15ms for methods that don't use the database DB::disconnect(); - // Purge inherited Redis/cache connections from parent process. - // After fork(), child inherits parent's Redis socket fd — using it - // would corrupt parent's protocol state. Purging forces fresh - // connections on next cache() call (predis connects lazily). + // Purge inherited Redis/cache/session connections from the parent. + // After fork(), the child inherits the parent's Redis socket fds; using + // any of them interleaves requests on the shared socket and desyncs the + // predis protocol on BOTH the child AND the parent (unserialize "offset 0 + // of N bytes" / "Error while reading line"). Once session + cache live on + // Redis, the parent's authenticateConnection() opens Redis fds before it + // forks, so the child must drop EVERY inherited connection. + // + // 1) Actively disconnect each configured connection — this closes ONLY the + // child's fd copy (predis disconnect just fcloses, it sends no command, + // so the parent's socket is untouched), turning any accidental later + // reuse into a fresh reconnect instead of a desync. + try { + $redisManager = app('redis'); + foreach (config('database.redis', []) as $redisName => $redisConf) { + // Skip non-connection entries (client, options, clusters, …). + if (! is_array($redisConf) || ! isset($redisConf['host'])) { + continue; + } + try { + $redisManager->connection($redisName)->disconnect(); + } catch (\Throwable $e) { + // per-connection best-effort + } + } + } catch (\Throwable $e) { + // best-effort; the forgetInstance() calls below still force fresh managers + } + + // 2) Forget the singletons so the next cache()/session()/Redis() call + // lazily rebuilds with a fresh socket. session/session.store were the + // gap: once the session store is Redis-backed they hold their OWN + // inherited Redis connection, which the cache/redis purge never reset. app()->forgetInstance('cache'); app()->forgetInstance('cache.store'); app()->forgetInstance('redis'); + app()->forgetInstance('session'); + app()->forgetInstance('session.store'); // Configure DB reconnect-on-lost-connection for this child. // If MySQL returns "Too many connections" or "server has gone away",