I DB connecting & auth handling
This commit is contained in:
parent
c77eec57c1
commit
7dd2df48d1
|
|
@ -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),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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,9 +87,31 @@ class Controller
|
|||
}
|
||||
|
||||
if (($controller->need_auth ?? true) && ! $connection->user) {
|
||||
$controller->error('Unauthorized');
|
||||
$controller->unboot();
|
||||
return;
|
||||
// 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)) {
|
||||
|
|
|
|||
|
|
@ -616,16 +616,34 @@ class Handler implements MessageComponentInterface
|
|||
]);
|
||||
}
|
||||
|
||||
if (app()->bound('sentry')) {
|
||||
app('sentry')->captureException($e);
|
||||
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.
|
||||
if (app()->bound('sentry')) {
|
||||
app('sentry')->flush();
|
||||
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();
|
||||
|
|
@ -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 {
|
||||
try {
|
||||
Controller::controll_message($mock, $channel, $message, $channelManager);
|
||||
} catch (\Throwable $e) {
|
||||
if (!$this->isDbConnectionError($e)) {
|
||||
throw $e;
|
||||
}
|
||||
$maxRetries = 2;
|
||||
$lastException = null;
|
||||
|
||||
// DB connection error — wait briefly for connections to free up, then retry
|
||||
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
|
||||
for ($attempt = 0; $attempt <= $maxRetries; $attempt++) {
|
||||
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(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue