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.
|
| 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\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)) {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue