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 <noreply@anthropic.com>
This commit is contained in:
Soporte ZH Consultores
2026-07-17 10:58:18 -05:00
co-authored by Claude Opus 4.8
parent bf93e8f79b
commit 1bfb3fba1f
+28 -7
View File
@@ -9,7 +9,22 @@ final class Auth {
/** Cache por request del row de users del usuario autenticado */ /** Cache por request del row de users del usuario autenticado */
private static ?array $userRowCache = null; 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; if (session_status() === PHP_SESSION_ACTIVE) return;
$secure = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https' $secure = ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
@@ -23,7 +38,13 @@ final class Auth {
'httponly' => true, 'httponly' => true,
'samesite' => 'Lax', '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 { public static function isAuthenticated(): bool {
@@ -102,7 +123,7 @@ final class Auth {
} }
public static function buildLoginUrl(?string $returnTo = null): string { public static function buildLoginUrl(?string $returnTo = null): string {
self::startSession(); self::startSession(true);
$state = bin2hex(random_bytes(16)); $state = bin2hex(random_bytes(16));
$_SESSION['oauth_state'] = $state; $_SESSION['oauth_state'] = $state;
$_SESSION['oauth_return_to'] = $returnTo ?: ($_SERVER['REQUEST_URI'] ?? '/'); $_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 // No bloqueamos el login por un fallo de DB no crítico
} }
self::startSession(); self::startSession(true);
session_regenerate_id(true); session_regenerate_id(true);
$_SESSION['authenticated'] = true; $_SESSION['authenticated'] = true;
$_SESSION['user_email'] = $email; $_SESSION['user_email'] = $email;
@@ -189,7 +210,7 @@ final class Auth {
} }
public static function logout(): void { public static function logout(): void {
self::startSession(); self::startSession(true);
$email = $_SESSION['user_email'] ?? ''; $email = $_SESSION['user_email'] ?? '';
$name = $_SESSION['user_name'] ?? ''; $name = $_SESSION['user_name'] ?? '';
if ($email !== '') { if ($email !== '') {
@@ -204,14 +225,14 @@ final class Auth {
} }
public static function popReturnTo(): string { public static function popReturnTo(): string {
self::startSession(); self::startSession(true);
$to = $_SESSION['oauth_return_to'] ?? '/'; $to = $_SESSION['oauth_return_to'] ?? '/';
unset($_SESSION['oauth_return_to']); unset($_SESSION['oauth_return_to']);
return $to; return $to;
} }
public static function checkState(string $state): bool { public static function checkState(string $state): bool {
self::startSession(); self::startSession(true);
$expected = $_SESSION['oauth_state'] ?? ''; $expected = $_SESSION['oauth_state'] ?? '';
unset($_SESSION['oauth_state']); unset($_SESSION['oauth_state']);
return $state !== '' && hash_equals((string)$expected, $state); return $state !== '' && hash_equals((string)$expected, $state);