es.davy.ai

Preguntas y respuestas de programación confiables

¿Tienes una pregunta?

Si tienes alguna pregunta, puedes hacerla a continuación o ingresar lo que estás buscando.

Doctrina no está actualizando adecuadamente los datos de la entidad con varias solicitudes concurrentes.

Necesito hacer algo básico, tengo dos entidades: User y Action. Cada Usuario tiene asignados X tokens por el Administrador y luego puede realizar Y acciones en función de la cantidad de tokens. Así que digamos que un Usuario solo tiene suficientes tokens para realizar una Acción, me di cuenta de que si realizo múltiples solicitudes simultáneamente al mismo tiempo (como 5 o más solicitudes al mismo tiempo). En lugar de solo una Acción, el Usuario ejecuta dos o más Acciones (y solo en el escenario explicado, en el resto todo funciona correctamente).

El código relacionado con mi explicación:

public function useractions(Requests $request){
        $user = $this->getUser();
        $post = Request::createFromGlobals();
        if($post->request->has('new_action') && $this->isCsrfTokenValid("mycsrf", $post->request->get('csrf_token'))) {
            $entityManager = $this->getDoctrine()->getManager();
            $tokens = $user->getTokens();

        if($tokens<1){
            $error = "No hay suficientes tokens";
        }
        if(empty($error)){
            $user->setTokens($tokens-1);
            $entityManager->flush();
            $action = new Action();
            $action->setUser($user);
            $entityManager->persist($transaction);
            $entityManager->flush();
         }
     }
}

Y estoy usando mariadb 10.5.12 con InnoDB como motor.

Obviamente estoy cometiendo un gran error en mi código o estoy omitiendo algo en la configuración de Symfony o Doctrine. ¿Alguien podría decirme cuál es el error? Gracias.

Tags:  , , , ,

Answer

  1. Avatar for davy.ai

    Como modelo de lenguaje de inteligencia artificial, no puedo garantizar la precisión del código ya que no puedo ejecutarlo, pero puedo identificar el problema y proporcionar sugerencias sobre cómo solucionarlo.

    El problema al que se enfrenta es una condición de carrera. Cuando se realizan múltiples solicitudes simultáneamente, acceden al mismo valor de tokens de la entidad de usuario y comprueban si es mayor que 1 al mismo tiempo. En función de esta condición, proceden a decrementarlo y crear una entidad de Acción. Sin embargo, dado que están procediendo al mismo tiempo, todos ven que el valor de tokens es mayor que 1 y lo decrementan, lo que conduce a la creación de múltiples entidades de Acción por un solo token. Este no es el comportamiento esperado.

    Para solucionar este problema, es necesario utilizar transacciones de base de datos para garantizar que las actualizaciones de la entidad de usuario sean atómicas y estén aisladas de otras transacciones. Esto significa que cuando una transacción está actualizando el valor de tokens, otras transacciones no pueden acceder o modificar el mismo valor hasta que se complete la primera transacción. Esto garantiza que solo se cree una entidad de Acción por token.

    Para utilizar transacciones en Doctrine, puede modificar su código de la siguiente manera:

    use Doctrine\DBAL\LockMode;
    
    public function useractions(Requests $request){
        $user = $this->getUser();
        $post = Request::createFromGlobals();
        if($post->request->has('new_action') && $this->isCsrfTokenValid("mycsrf", $post->request->get('csrf_token'))) {
            $entityManager = $this->getDoctrine()->getManager();
            $entityManager->getConnection()->beginTransaction();
            try {
                $user = $entityManager->getRepository(User::class)->find($user->getId(), LockMode::PESSIMISTIC_WRITE);
                // Esto bloquea la entidad de usuario para el acceso de escritura, evitando que otras transacciones la accedan o la modifiquen hasta que se complete esta transacción.
    
                $tokens = $user->getTokens();
    
                if($tokens < 1){
                    $error = "No hay suficientes tokens";
                }
                if(empty($error)){
                    $user->setTokens($tokens-1);
                    $entityManager->flush();
    
                    $action = new Action();
                    $action->setUser($user);
                    $entityManager->persist($action);
                    $entityManager->flush();
                 }
    
                 $entityManager->getConnection()->commit();
            } catch (\Exception $e) {
                $entityManager->getConnection()->rollBack();
                throw $e;
            }
        }
    }
    

    En este código modificado, la transacción se inicia utilizando el método beginTransaction() del objeto de conexión del administrador de entidades. Luego, la entidad de usuario se carga usando el método find() del repositorio con LockMode::PESSIMISTIC_WRITE como segundo argumento. Esto bloquea la entidad de usuario para el acceso de escritura, evitando que otras transacciones la accedan o la modifiquen hasta que se complete esta transacción. Luego, el valor de tokens se verifica y actualiza en consecuencia, y luego se crea la entidad de Acción y se compromete en la base de datos.

    Si ocurre una excepción durante la transacción, la transacción se revierte utilizando el método rollBack() del objeto de conexión y se vuelve a lanzar la excepción.

    Con esta modificación, su código ahora debería funcionar según lo esperado y se debería crear solo una entidad de Acción por token incluso cuando se realizan múltiples solicitudes simultáneamente.

Comments are closed.