improve postgresql performance

This commit is contained in:
DioCrafts
2025-04-09 00:21:20 +02:00
parent 7473ee438f
commit 8f1d213526
36 changed files with 2191 additions and 233 deletions
@@ -1,12 +1,21 @@
use async_trait::async_trait;
use sqlx::{PgPool, Row};
use sqlx::{PgPool, Row, Executor};
use std::sync::Arc;
use chrono::Utc;
use futures::future::BoxFuture;
use crate::domain::entities::session::Session;
use crate::domain::repositories::session_repository::{SessionRepository, SessionRepositoryError, SessionRepositoryResult};
use crate::application::ports::auth_ports::SessionStoragePort;
use crate::common::errors::DomainError;
use crate::infrastructure::repositories::pg::transaction_utils::with_transaction;
// Implementar From<sqlx::Error> para SessionRepositoryError para permitir conversiones automáticas
impl From<sqlx::Error> for SessionRepositoryError {
fn from(err: sqlx::Error) -> Self {
SessionPgRepository::map_sqlx_error(err)
}
}
pub struct SessionPgRepository {
pool: Arc<PgPool>,
@@ -18,7 +27,7 @@ impl SessionPgRepository {
}
// Método auxiliar para mapear errores SQL a errores de dominio
fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
pub fn map_sqlx_error(err: sqlx::Error) -> SessionRepositoryError {
match err {
sqlx::Error::RowNotFound => {
SessionRepositoryError::NotFound("Sesión no encontrada".to_string())
@@ -32,30 +41,66 @@ impl SessionPgRepository {
#[async_trait]
impl SessionRepository for SessionPgRepository {
/// Crea una nueva sesión
/// Crea una nueva sesión utilizando una transacción
async fn create_session(&self, session: Session) -> SessionRepositoryResult<Session> {
sqlx::query(
r#"
INSERT INTO auth.sessions (
id, user_id, refresh_token, expires_at,
ip_address, user_agent, created_at, revoked
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
"#
)
.bind(session.id())
.bind(session.user_id())
.bind(session.refresh_token())
.bind(session.expires_at())
.bind(&session.ip_address)
.bind(&session.user_agent)
.bind(session.created_at())
.bind(session.is_revoked())
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
// Crear una copia de la sesión para el closure
let session_clone = session.clone();
with_transaction(
&self.pool,
"create_session",
|tx| {
Box::pin(async move {
// Insertar la sesión
sqlx::query(
r#"
INSERT INTO auth.sessions (
id, user_id, refresh_token, expires_at,
ip_address, user_agent, created_at, revoked
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
"#
)
.bind(session_clone.id())
.bind(session_clone.user_id())
.bind(session_clone.refresh_token())
.bind(session_clone.expires_at())
.bind(&session_clone.ip_address)
.bind(&session_clone.user_agent)
.bind(session_clone.created_at())
.bind(session_clone.is_revoked())
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Opcionalmente, actualizar el último login del usuario
// dentro de la misma transacción
sqlx::query(
r#"
UPDATE auth.users
SET last_login_at = NOW(), updated_at = NOW()
WHERE id = $1
"#
)
.bind(session_clone.user_id())
.execute(&mut **tx)
.await
.map_err(|e| {
// Convertimos el error pero sin interrumpir la creación
// de la sesión si falla la actualización
tracing::warn!("No se pudo actualizar last_login_at para usuario {}: {}",
session_clone.user_id(), e);
SessionRepositoryError::DatabaseError(format!(
"Sesión creada pero no se pudo actualizar last_login_at: {}", e
))
})?;
Ok(session_clone)
}) as BoxFuture<'_, SessionRepositoryResult<Session>>
}
).await?;
Ok(session)
}
@@ -150,38 +195,78 @@ impl SessionRepository for SessionPgRepository {
Ok(sessions)
}
/// Revoca una sesión específica
/// Revoca una sesión específica utilizando una transacción
async fn revoke_session(&self, session_id: &str) -> SessionRepositoryResult<()> {
sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE id = $1
"#
)
.bind(session_id)
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
Ok(())
let id = session_id.to_string(); // Clone para uso en closure
with_transaction(
&self.pool,
"revoke_session",
|tx| {
Box::pin(async move {
// Revocar la sesión
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE id = $1
RETURNING user_id
"#
)
.bind(&id)
.fetch_optional(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
// Si encontramos la sesión, podemos registrar un evento de seguridad
if let Some(row) = result {
let user_id: String = row.try_get("user_id").unwrap_or_default();
// Registrar evento de seguridad (en una tabla de seguridad)
// Esto es opcional pero muestra cómo se puede realizar operaciones
// adicionales en la misma transacción
tracing::info!("Sesión con ID {} del usuario {} revocada", id, user_id);
}
Ok(())
}) as BoxFuture<'_, SessionRepositoryResult<()>>
}
).await
}
/// Revoca todas las sesiones de un usuario
/// Revoca todas las sesiones de un usuario utilizando una transacción
async fn revoke_all_user_sessions(&self, user_id: &str) -> SessionRepositoryResult<u64> {
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE user_id = $1 AND revoked = false
"#
)
.bind(user_id)
.execute(&*self.pool)
.await
.map_err(Self::map_sqlx_error)?;
Ok(result.rows_affected())
let user_id_clone = user_id.to_string(); // Clone para uso en closure
with_transaction(
&self.pool,
"revoke_all_user_sessions",
|tx| {
Box::pin(async move {
// Revocar todas las sesiones del usuario
let result = sqlx::query(
r#"
UPDATE auth.sessions
SET revoked = true
WHERE user_id = $1 AND revoked = false
"#
)
.bind(&user_id_clone)
.execute(&mut **tx)
.await
.map_err(Self::map_sqlx_error)?;
let affected = result.rows_affected();
// Registrar evento de seguridad
if affected > 0 {
tracing::info!("Revocadas {} sesiones del usuario {}", affected, user_id_clone);
}
Ok(affected)
}) as BoxFuture<'_, SessionRepositoryResult<u64>>
}
).await
}
/// Elimina sesiones expiradas