I DB connecting & auth handling

This commit is contained in:
Fabian @ Blax Software 2026-03-23 14:13:30 +01:00
parent c77eec57c1
commit 7dd2df48d1
3 changed files with 94 additions and 37 deletions

View File

@ -43,7 +43,7 @@ return [
| For MySQL default of 151: 50 is a safe default. | For MySQL default of 151: 50 is a safe default.
| |
*/ */
'max_concurrent_children' => (int) env('WEBSOCKET_MAX_CHILDREN', 50), 'max_concurrent_children' => (int) env('WEBSOCKET_MAX_CHILDREN', 30),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -9,8 +9,10 @@ use BlaxSoftware\LaravelWebSockets\ChannelManagers\RedisChannelManager;
use BlaxSoftware\LaravelWebSockets\Channels\Channel; use BlaxSoftware\LaravelWebSockets\Channels\Channel;
use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel; use BlaxSoftware\LaravelWebSockets\Channels\PresenceChannel;
use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel; use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel;
use Ratchet\ConnectionInterface; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken;
use Ratchet\ConnectionInterface;
class Controller class Controller
{ {
@ -85,9 +87,31 @@ class Controller
} }
if (($controller->need_auth ?? true) && ! $connection->user) { if (($controller->need_auth ?? true) && ! $connection->user) {
$controller->error('Unauthorized'); // Self-heal: the parent process may have a stale DB connection that
$controller->unboot(); // can't find newly created tokens. The child process has a fresh DB
return; // connection (reconnected after fork), so try to authenticate here.
$authtoken = @$message['data']['authtoken'] ?? null;
if ($authtoken) {
try {
$tokenRecord = PersonalAccessToken::findToken($authtoken);
if ($tokenRecord?->tokenable) {
$connection->user = $tokenRecord->tokenable;
Auth::login($connection->user);
// Clear parent's stale auth cache so it re-authenticates
if ($connection instanceof MockConnectionSocketPair) {
$connection->clearConnectionData('authLoaded');
}
}
} catch (\Throwable $e) {
// Auth self-heal failed, fall through to Unauthorized
}
}
if (! $connection->user) {
$controller->error('Unauthorized');
$controller->unboot();
return;
}
} }
if (! method_exists($controllerClass, $method)) { if (! method_exists($controllerClass, $method)) {

View File

@ -616,16 +616,34 @@ class Handler implements MessageComponentInterface
]); ]);
} }
if (app()->bound('sentry')) { try {
app('sentry')->captureException($e); if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
} catch (\Throwable $sentryError) {
// Sentry capture failed (possibly also a DB issue), ignore
} }
} }
// Flush Sentry before the child exits so captured events are actually sent. // Flush Sentry before the child exits so captured events are actually sent.
// Without this, events from report()/captureException() may be lost because // Without this, events from report()/captureException() may be lost because
// the child calls exit(0) before the async transport can dispatch them. // the child calls exit(0) before the async transport can dispatch them.
if (app()->bound('sentry')) { try {
app('sentry')->flush(); if (app()->bound('sentry')) {
app('sentry')->flush();
}
} catch (\Throwable $e) {
// Sentry flush failed, continue with cleanup
}
// Explicitly close the MySQL connection before exit.
// Relying on exit(0) to close the FD is not instant — MySQL may keep
// the connection slot occupied until TCP cleanup completes.
// Under burst load this causes "Too many connections" errors.
try {
DB::disconnect();
} catch (\Throwable $e) {
// Disconnect failed, OS will clean up on exit
} }
$ipc->closeChild(); $ipc->closeChild();
@ -683,8 +701,8 @@ class Handler implements MessageComponentInterface
/** /**
* Execute the controller with DB connection resilience. * Execute the controller with DB connection resilience.
* If the first attempt fails with a DB connection error (e.g., "Too many connections", * If an attempt fails with a DB connection error (e.g., "Too many connections",
* "server has gone away"), waits briefly and retries once with a fresh connection. * "server has gone away"), retries with exponential backoff up to 2 times.
*/ */
protected function executeControllerWithDbResilience( protected function executeControllerWithDbResilience(
$mock, $mock,
@ -692,36 +710,51 @@ class Handler implements MessageComponentInterface
array $message, array $message,
ChannelManager $channelManager ChannelManager $channelManager
): void { ): void {
try { $maxRetries = 2;
Controller::controll_message($mock, $channel, $message, $channelManager); $lastException = null;
} catch (\Throwable $e) {
if (!$this->isDbConnectionError($e)) {
throw $e;
}
// DB connection error — wait briefly for connections to free up, then retry for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
Log::channel('websocket')->warning('DB connection error, retrying after 500ms', [
'error' => $e->getMessage(),
'event' => $message['event'] ?? 'unknown',
]);
usleep(500_000); // 500ms backoff
// Force a completely fresh DB connection
try { try {
DB::disconnect(); if ($attempt > 0) {
DB::reconnect(); // Force a completely fresh DB connection before retry
} catch (\Throwable $reconnectError) { try {
// If reconnect also fails, throw the original error DB::disconnect();
Log::channel('websocket')->error('DB reconnect failed after retry', [ DB::reconnect();
'error' => $reconnectError->getMessage(), } catch (\Throwable $reconnectError) {
]); Log::channel('websocket')->error('DB reconnect failed on retry attempt ' . $attempt, [
throw $e; 'error' => $reconnectError->getMessage(),
} ]);
throw $lastException;
}
}
// Retry the controller execution once Controller::controll_message($mock, $channel, $message, $channelManager);
Controller::controll_message($mock, $channel, $message, $channelManager); return; // Success
} catch (\Throwable $e) {
if (!$this->isDbConnectionError($e)) {
throw $e;
}
$lastException = $e;
if ($attempt < $maxRetries) {
// Exponential backoff: 500ms, 1500ms
$backoffMs = 500 * ($attempt + 1);
Log::channel('websocket')->warning('DB connection error, retry ' . ($attempt + 1) . '/' . $maxRetries . ' after ' . $backoffMs . 'ms', [
'error' => $e->getMessage(),
'event' => $message['event'] ?? 'unknown',
]);
usleep($backoffMs * 1000);
}
}
} }
// All retries exhausted
Log::channel('websocket')->error('DB connection error persisted after ' . $maxRetries . ' retries', [
'error' => $lastException?->getMessage(),
'event' => $message['event'] ?? 'unknown',
]);
throw $lastException;
} }
/** /**