fix(fork): fully isolate inherited Redis connections in the message child
The per-message fork child purged cache/redis manager singletons but never reset
the session bindings, and never actively disconnected the fds it inherited. Once an
app puts session + cache on Redis, the parent's authenticateConnection() opens Redis
sockets before forking; a child touching any un-reset connection interleaves on the
shared fd and desyncs the predis protocol on BOTH sides — flooding
"unserialize(): Error at offset 0 of N bytes" and
"Predis ConnectionException: Error while reading line from the server".
In the child, before purging: disconnect() every configured redis connection (closes
only the child's fd copy — predis disconnect fcloses without sending a command, so
the parent socket is untouched; any later reuse reconnects fresh instead of
desyncing), then also forgetInstance('session') + 'session.store' so a Redis-backed
session store rebuilds lazily too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b678a25a3c
commit
2a9986d209
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue