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:
co-authored by
Claude Opus 4.8
parent
bf93e8f79b
commit
1bfb3fba1f
+28
-7
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user