web-dev-qa-db-fra.com

Rails/ActiveRecord: évite que deux threads mettent à jour le modèle en même temps avec des verrous

Supposons que je dois être sûr que ModelName ne puisse pas être mis à jour en même temps par deux threads Rails différents; Cela peut arriver, par exemple, lorsqu'un message Webhooks envoyé à l'application tente de la modifier en même temps qu'un autre code est en cours d'exécution.

Documentation Per Rails , Je pense que la solution serait d'utiliser model_name_instance.with_lock, qui commence également une nouvelle transaction. 

Cela fonctionne correctement et empêche les mises à jour simultanées du modèle, mais n'empêche pas les autres threads de lire cette ligne de la table pendant l'exécution du bloc with_lock.

Je peux prouver que with_lock n'empêche pas d'autres READS de cette manière:

  • Ouvrez les consoles à 2 rails;

  • Sur la console 1, tapez quelque chose comme ModelName.last.with_lock { sleep 30 }

  • Sur la console 2, tapez ModelName.last. Vous serez capable de lire le modèle sans problème.

  • Sur la console 2, tapez ModelName.update_columns(updated_at: Time.now). Vous verrez qu'il attendra que le verrou de 30 secondes expire avant de se terminer.

Cela prouve que le verrou n'empêche PAS la lecture et, autant que je sache, il n'y a aucun moyen de verrouiller la ligne de la base de données. 

Ceci est problématique car si 2 threads exécutent la même méthode à la même heure et que je dois décider d’exécuter le bloc with_lock concernant certaines vérifications précédentes sur les données du modèle, le thread 2 pourrait lire des données obsolètes qui seraient bientôt mises à jour par thread. 1 après avoir terminé le bloc with_lock qui est déjà en cours d'exécution, car le thread 2 PEUT LIRE le modèle alors que le bloc with_lock est en cours dans le thread 1, il ne peut pas le mettre à jour uniquement à cause du verrou. 

EDIT: J'ai trouvé une réponse à cette question, vous pouvez donc arrêter de lire ici et aller directement au-dessous :)

Une des idées que j'avais était de commencer le bloc with_lock en publiant une mise à jour inoffensive du modèle (telle que model_instance_name.update_columns(updated_at: Time.now) par exemple), puis en la suivant avec un model_name_instance.reload pour être sûr qu'il obtienne les données les plus à jour. Ainsi, si deux threads exécutent le même code en même temps, un seul pourra émettre la première mise à jour, tandis que l’autre devra attendre que le verrou soit libéré. Une fois qu'il est publié, il sera suivi de ce model_instance_name.reload pour s'assurer que toutes les mises à jour sont effectuées par l'autre thread.

Le problème est que cette solution semble trop compliquée à mon goût et je ne suis pas sûre de devoir réinventer la roue ici (je ne sais pas s'il me manque des étuis Edge). Comment garantir que, lorsque deux threads exécutent exactement la même méthode exactement au même moment, un thread attend que l'autre ait fini de lire le modèle?

8
sandre89

Merci Robert pour les informations de verrouillage optimistes, je pouvais vraiment me voir emprunter cette voie, mais le verrouillage optimiste fonctionne en générant une exception au moment de l'écriture dans la base de données (SQL UPDATE), et j'ai beaucoup de logique commerciale complexe que je ne 'même pas envie de courir avec les données obsolètes en premier lieu.

Voici comment je l'ai résolu, et c'était plus simple que ce que j'avais imaginé. 

Tout d’abord, j’ai appris que le verrouillage pessimiste NE FAUT PAS empêche tout autre thread de lire cette ligne de la base de données. 

Mais j’ai aussi appris que with_lock initialise également le verrou immédiatement, que vous essayiez ou non d’écrire.

Donc, si vous démarrez les consoles 2 Rails (simulant deux threads différents), vous pouvez tester cela:

  • Si vous tapez ModelName.last.with_lock { sleep 30 } sur la console 1 et ModelName.last sur la console 2, celle-ci peut immédiatement lire cet enregistrement.

  • Si vous tapez ModelName.last.with_lock { sleep 30 } sur la console 1 et ModelName.last.with_lock { p 'I'm waiting' } sur la console 2, celle-ci attendra que la serrure soit bloquée par la console 1, même si elle ne génère aucune écriture.

C’est donc une façon de «verrouiller la lecture»: si vous voulez que votre code soit exécuté simultanément (même pour les lectures!), Démarrez cette méthode en ouvrant un bloc with_lock et lancez votre commande. le modèle lit à l'intérieur qu'il attendra que les autres verrous soient libérés en premier. Si vous publiez vos lectures en dehors de celui-ci, vos lectures seront effectuées même si un autre morceau de code dans un autre thread a un verrou sur cette ligne du tableau. 

Quelques autres bonnes choses que j'ai apprises:

  • Ce sont des gemmes conçues spécifiquement pour bloquer le même morceau de code s'exécutant en même temps dans plusieurs threads (ce qui, selon moi, correspond à la majorité des applications Rails en environnement de production), quel que soit le verrouillage de la base de données. Jetez un coup d'œil à redis-mutex, redis-sémaphore et redis-lock.

  • De nombreux articles sur le Web (je pourrais en trouver au moins 3) stipulent que Rails with_lock empêchera une LECTURE sur la ligne de la base de données, alors que nous pouvons facilement voir avec les tests ci-dessus que ce n'est pas le cas. Faites attention et confirmez toujours les informations en les testant vous-même! J'ai essayé de les commenter en les avertissant. 

7
sandre89

Vous étiez proches, vous voulez un verrouillage optimiste au lieu d'un verrouillage pessimiste: http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html

Cela n'empêchera pas de lire un objet et de soumettre un formulaire. Mais il peut détecter que le formulaire a été soumis lorsque l'utilisateur voyait une version obsolète de l'objet.

1
Robert Pankowecki