I need to do something basic, I have two entities: User and Action. Each User has X tokens assigned by the Admin and then he can perform Y actions based on the amount of tokens. So lets say one User only has enough tokens to perform one Action, I identified that if I perform multiple simultaneously requests at the same exact time (like 5 or more requests at the same time). Instead of just one Action, the User executes two or more Actions (and only in the explained scenario, in the rest everything works fine)
The related code to my explanation:
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 = "Not enough tokens";
}
if(empty($error)){
$user->setTokens($tokens-1);
$entityManager->flush();
$action = new Action();
$action->setUser($user);
$entityManager->persist($transaction);
$entityManager->flush();
}
}
}
And I am using mariadb 10.5.12 with InnoDB as the engine
Obviously I am making a big mistake in my code or missing something in the Symfony or Doctrine configuration. Someone could tell me the mistake? Thanks
CodePudding user response:
You probably have a race condition between the user's SELECT
(the user refresh, called by your authentication mechanism) and your UPDATE
(the first $entityManager->flush();
).
Doctrine has a built-in method to manage concurrent requests, the EntityManager::transactional()
method (doc). You may just need to wrap your code into it:
$entityManager = $this->getDoctrine()->getManager();
$error = $entityManager->transactional(function($entityManager) use ($user) {
// Required to perform a blocking SELECT for UPDATE
$user = $entityManager->refresh($user);
$tokens = $user->getTokens();
if($tokens<1){
return "Not enough tokens";
}
$user->setTokens($tokens-1);
$entityManager->persist($user);
});
if (empty($error)) {
$action = new Action();
$action->setUser($user);
$entityManager->persist($action);
$entityManager->flush();
}
Note: make sure that your transaction is fast enough, as it has blocking effects on the mariadb side, affecting the involved rows.
CodePudding user response:
For the execution of one update command, you should use flush once, and not in each iterate.
also, you can use transactions to do all of them as one unit.
but if you want to prevent concurrency updating you should use versioning! by this, when doctrine wants to update something check versioning and if it changes throw an error and prevent to persist data to the database ( you can handle this error by a listener ) and by this way just first request will be a success.
for example, I flag updatedAt filed as a version filed and it will be checked in each persistence.
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Version
* @ORM\Column(type="datetime",options={"default": "CURRENT_TIMESTAMP"})
*/
private $UpdatedAt;