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 */
|
/** 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user