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,10 +87,32 @@ class Controller
} }
if (($controller->need_auth ?? true) && ! $connection->user) { if (($controller->need_auth ?? true) && ! $connection->user) {
// Self-heal: the parent process may have a stale DB connection that
// can't find newly created tokens. The child process has a fresh DB
// 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->error('Unauthorized');
$controller->unboot(); $controller->unboot();
return; return;
} }
}
if (! method_exists($controllerClass, $method)) { if (! method_exists($controllerClass, $method)) {
$controller->error('Event could not be handled'); $controller->error('Event could not be handled');

View File

@ -616,17 +616,35 @@ class Handler implements MessageComponentInterface
]); ]);
} }
try {
if (app()->bound('sentry')) { if (app()->bound('sentry')) {
app('sentry')->captureException($e); 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.
try {
if (app()->bound('sentry')) { if (app()->bound('sentry')) {
app('sentry')->flush(); 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();
exit(0); exit(0);
@ -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 {
$maxRetries = 2;
$lastException = null;
for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
try { try {
if ($attempt > 0) {
// Force a completely fresh DB connection before retry
try {
DB::disconnect();
DB::reconnect();
} catch (\Throwable $reconnectError) {
Log::channel('websocket')->error('DB reconnect failed on retry attempt ' . $attempt, [
'error' => $reconnectError->getMessage(),
]);
throw $lastException;
}
}
Controller::controll_message($mock, $channel, $message, $channelManager); Controller::controll_message($mock, $channel, $message, $channelManager);
return; // Success
} catch (\Throwable $e) { } catch (\Throwable $e) {
if (!$this->isDbConnectionError($e)) { if (!$this->isDbConnectionError($e)) {
throw $e; throw $e;
} }
// DB connection error — wait briefly for connections to free up, then retry $lastException = $e;
Log::channel('websocket')->warning('DB connection error, retrying after 500ms', [
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(), 'error' => $e->getMessage(),
'event' => $message['event'] ?? 'unknown', 'event' => $message['event'] ?? 'unknown',
]); ]);
usleep($backoffMs * 1000);
}
}
}
usleep(500_000); // 500ms backoff // All retries exhausted
Log::channel('websocket')->error('DB connection error persisted after ' . $maxRetries . ' retries', [
// Force a completely fresh DB connection 'error' => $lastException?->getMessage(),
try { 'event' => $message['event'] ?? 'unknown',
DB::disconnect();
DB::reconnect();
} catch (\Throwable $reconnectError) {
// If reconnect also fails, throw the original error
Log::channel('websocket')->error('DB reconnect failed after retry', [
'error' => $reconnectError->getMessage(),
]); ]);
throw $e; throw $lastException;
}
// Retry the controller execution once
Controller::controll_message($mock, $channel, $message, $channelManager);
}
} }
/** /**