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.
|
*/
'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\PresenceChannel;
use BlaxSoftware\LaravelWebSockets\Channels\PrivateChannel;
use Ratchet\ConnectionInterface;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Laravel\Sanctum\PersonalAccessToken;
use Ratchet\ConnectionInterface;
class Controller
{
@ -85,10 +87,32 @@ class Controller
}
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->unboot();
return;
}
}
if (! method_exists($controllerClass, $method)) {
$controller->error('Event could not be handled');

View File

@ -616,17 +616,35 @@ class Handler implements MessageComponentInterface
]);
}
try {
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.
// Without this, events from report()/captureException() may be lost because
// the child calls exit(0) before the async transport can dispatch them.
try {
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();
exit(0);
@ -683,8 +701,8 @@ class Handler implements MessageComponentInterface
/**
* Execute the controller with DB connection resilience.
* If the first 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.
* If an attempt fails with a DB connection error (e.g., "Too many connections",
* "server has gone away"), retries with exponential backoff up to 2 times.
*/
protected function executeControllerWithDbResilience(
$mock,
@ -692,36 +710,51 @@ class Handler implements MessageComponentInterface
array $message,
ChannelManager $channelManager
): void {
$maxRetries = 2;
$lastException = null;
for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
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);
return; // Success
} catch (\Throwable $e) {
if (!$this->isDbConnectionError($e)) {
throw $e;
}
// DB connection error — wait briefly for connections to free up, then retry
Log::channel('websocket')->warning('DB connection error, retrying after 500ms', [
$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);
}
}
}
usleep(500_000); // 500ms backoff
// Force a completely fresh DB connection
try {
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(),
// All retries exhausted
Log::channel('websocket')->error('DB connection error persisted after ' . $maxRetries . ' retries', [
'error' => $lastException?->getMessage(),
'event' => $message['event'] ?? 'unknown',
]);
throw $e;
}
// Retry the controller execution once
Controller::controll_message($mock, $channel, $message, $channelManager);
}
throw $lastException;
}
/**