From 1bfb3fba1f905834c50bef4a471dc62b7ab717b1 Mon Sep 17 00:00:00 2001 From: Soporte ZH Consultores Date: Fri, 17 Jul 2026 10:58:18 -0500 Subject: [PATCH] fix(auth): read_and_close en startSession para evitar deadlock de lock de sesion session_start() retenia un flock exclusivo sobre el archivo de sesion durante todo el request. Requests paralelos del mismo usuario (polling de /escrituras/) se serializaban y agotaban el pool de php-fpm, tumbando la app (timeout total). Ahora startSession() abre en modo read_and_close (lee $_SESSION y libera el lock de inmediato). Los flujos que escriben en la sesion (login OAuth, logout) pasan $forWrite=true para tomar el lock de escritura normal. Co-Authored-By: Claude Opus 4.8 --- src/Auth.php | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Auth.php b/src/Auth.php index c45ca3a..c031e59 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -9,7 +9,22 @@ final class Auth { /** Cache por request del row de users del usuario autenticado */ private static ?array $userRowCache = null; - public static function startSession(): void { + /** Marca si $_SESSION ya fue cargado en este request (incluye modo read_and_close). */ + private static bool $sessionLoaded = false; + + /** + * Abre la sesión. + * + * Por defecto usa read_and_close: carga $_SESSION en memoria y libera de + * inmediato el lock del archivo de sesión. Sin esto, session_start() retiene + * un flock() exclusivo durante todo el request, y los requests paralelos del + * mismo usuario (p.ej. el polling de /escrituras/) se serializan y agotan el + * pool de php-fpm hasta tumbar la app. + * + * Pasar $forWrite=true SOLO en los flujos que modifican la sesión (login + * OAuth, logout): toma el lock de escritura normal, liberado al fin del request. + */ + public static function startSession(bool $forWrite = false): void { if (session_status() === PHP_SESSION_ACTIVE) return; $secure = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https' @@ -23,7 +38,13 @@ final class Auth { 'httponly' => true, 'samesite' => 'Lax', ]); - session_start(); + + if ($forWrite) { + session_start(); // lock exclusivo hasta fin del request + } elseif (!self::$sessionLoaded) { + session_start(['read_and_close' => true]); // lee $_SESSION y libera el lock ya + } + self::$sessionLoaded = true; } public static function isAuthenticated(): bool { @@ -102,7 +123,7 @@ final class Auth { } public static function buildLoginUrl(?string $returnTo = null): string { - self::startSession(); + self::startSession(true); $state = bin2hex(random_bytes(16)); $_SESSION['oauth_state'] = $state; $_SESSION['oauth_return_to'] = $returnTo ?: ($_SERVER['REQUEST_URI'] ?? '/'); @@ -176,7 +197,7 @@ final class Auth { // No bloqueamos el login por un fallo de DB no crítico } - self::startSession(); + self::startSession(true); session_regenerate_id(true); $_SESSION['authenticated'] = true; $_SESSION['user_email'] = $email; @@ -189,7 +210,7 @@ final class Auth { } public static function logout(): void { - self::startSession(); + self::startSession(true); $email = $_SESSION['user_email'] ?? ''; $name = $_SESSION['user_name'] ?? ''; if ($email !== '') { @@ -204,14 +225,14 @@ final class Auth { } public static function popReturnTo(): string { - self::startSession(); + self::startSession(true); $to = $_SESSION['oauth_return_to'] ?? '/'; unset($_SESSION['oauth_return_to']); return $to; } public static function checkState(string $state): bool { - self::startSession(); + self::startSession(true); $expected = $_SESSION['oauth_state'] ?? ''; unset($_SESSION['oauth_state']); return $state !== '' && hash_equals((string)$expected, $state);