Stratégies de gestion des problèmes de concurrence causés par des objects de domaine obsolètes (Grails / GORM / Hibernate)

J’aime qualifier ce problème de «chercheur répétable», car il est en quelque sorte opposé à «lecture non répétable». Comme hibernate réutilise les objects attachés à sa session, les résultats d’un outil de recherche peuvent inclure certaines anciennes versions d’objects obsolètes.

Le problème est techniquement lié à la conception Hibernate, mais comme la session Hibernate est implicite dans les objects du domaine Grails et Grails, elle a une longue durée de vie (la requête HTTP m’est longue), j’ai décidé de poser cette question dans le contexte de Grails / GORM.

Je voudrais demander aux experts s’il existe des stratégies communément établies pour traiter ce problème.

Considère ceci:

class BankAccount { Ssortingng name Float amount static constraints = { name unique: true } } 

et le code ‘componentA’:

  BankAccount.findByName('a1') 

‘code composantB:

  def result = BankAccount.findAll() 

Supposons que componentA s’exécute en premier, suivi d’une autre logique, suivi de componentB, le résultat du composant B est rendu par une vue. Les composants A et B ne veulent pas en savoir beaucoup les uns sur les autres.

De cette manière, le résultat du composant B contient l’ancienne version de BankAccount ‘a1’.

Beaucoup de choses très embarrassantes peuvent se produire. Si des comptes bancaires ont été modifiés simultanément, la liste présentée peut, par exemple, contenir 2 éléments portant le nom “a1” (le caractère unique semble aller à l’utilisateur!) Ou les transferts d’argent entre comptes peuvent apparaître comme des transactions partiellement appliquées (si l’argent a été transféré de a2 à a1 alors il montrera déduit de a2 mais pas encore pour a1). Ces problèmes sont embarrassants et peuvent réduire la confiance des utilisateurs dans l’application.

( ADDED 24/09/2014: Voici un exemple révélateur, cette assertion peut échouer:

  BankAccount.findAllByName('a1').every{ it.name == 'a1' } 

Des exemples de la façon dont cela se produit peuvent être trouvés dans l’un des billets JIRA liés ou mon blog. )

( ADDED 9/24/2014: NOTE: un conseil apparemment judicieux d’utiliser des clés uniques imposées par la firebase database lors de l’implémentation de la méthode equals () n’est pas protégé contre la simultanéité. Vous pouvez obtenir 2 objects avec la même valeur que la ‘clé de commerce’, qui sont différents. )

Les solutions possibles semblent être d’append beaucoup d’appels discard () ou beaucoup d’appels withNewSession () et de traiter avec LazyIntializationExeption et DuplicateKeyException, etc.
Mais si je fais cela, pourquoi est-ce que j’utilise Hibernate / GORM? L’appel de l’actualisation sur chaque object renvoyé par chaque requête semble tout simplement ridicule.

Ma pensée actuelle est que l’utilisation de sessions courtes / avecNewSession dans certains domaines critiques est la meilleure approche, mais elle ne résout pas le problème dans tous les cas, mais seulement dans certains domaines d’application critiques.

Est-ce quelque chose que les applications Grails doivent vivre? Pouvez-vous m’indiquer toute documentation / discussion sur ce problème?

Édité le 24/09/2014 : billet Grail JIRA pertinent: https://jira.grails.org/browse/GRAILS-11645 , Hibernate JIRA: https://hibernate.atlassian.net/browse/HHH-9367 (a malheureusement été rejeté), mon blog contient des exemples plus détaillés: http://rpeszek.blogspot.com/2014/08/i-dont-like-hibernategrails-part-2.html

ADDED 17/10/2014: J’ai reçu plusieurs réponses indiquant qu’il s’agissait de n’importe quelle application de firebase database / problème ORM. Ce n’est pas correct.

Il est vrai que ce problème peut être évité en utilisant de longues transactions (longueur de session Hibernate / longueur de requête HTTP) + paramètre supérieur au niveau d’isolement de firebase database normal de REPEATABLE READ. Cette solution n’est tout simplement pas acceptable (pourquoi avons-nous des services transnationaux si, pour que l’application fonctionne correctement, nous avons besoin de transactions longues avec une requête HTTP!?)

Les applications DB et autres ORM ne présenteront pas ce problème. Ils n’auront pas besoin de longues transactions pour fonctionner et le problème est évité avec juste READ COMMITTED.

Il y a maintenant 2 mois que j’ai posté cette question ici et elle n’a pas reçu de réponse significative. C’est simplement parce que cette question n’a pas de réponse. Hibernate peut résoudre ce problème et non une application Grails ne peut le réparer. ADDED 10/17/2014-FIN

Voici ma propre tentative pour répondre à cette question.

( ADDED 9/24/2014 Il n’ya tout simplement pas de bonne solution à ce problème. Malheureusement, le ticket HHH-9367 JIRA a été rejeté par Hibernate, car il ne s’agissait pas d’un bogue. La seule solution suggérée dans ce ticket était d’utiliser l’actualisation (je suppose cela nécessiterait de changer toutes les requêtes en quelque chose qui ressemble à:

 BankAccount.findAllBy...(...).each{ it.refresh() } 

Personnellement, je ne suis pas d’accord pour dire qu’il s’agit d’une solution significative.)

Comme je l’ai expliqué ci-dessus, si la requête Hibernate / GORM renvoie un ensemble de DomainObjects et que certains de ces objects sont déjà en session de veille prolongée (remplis par les requêtes précédentes), la requête renvoie ces anciens objects et ces objects ne seront pas automatiquement actualisés. Cela peut entraîner des problèmes de concurrence difficiles à détecter. Je l’appelle le problème Répétable Finder.

Cela n’a rien à voir avec le cache de second niveau. Ce problème est dû au mode de fonctionnement de la mise en veille prolongée même sans la configuration du cache de second niveau. (EDITED 24/09/2014: Et ce n’est pas un ORM, un problème d’application de firebase database, le problème est spécifique à l’utilisation d’Hibernate).

Implications sur votre application:

( Je peux seulement expliquer les impacts que je connais, je ne prétends pas que ce sont les seuls impacts ).

Les objects de domaine ont généralement un ensemble de contraintes / règles logiques associées qui doivent généralement contenir plusieurs enregistrements et qui sont appliqués par l’application ou la firebase database elle-même. J’emprunterai un terme de FP et de test et appellerai ces «propriétés».

Exemple de propriétés: Dans l’exemple ci-dessus, BankAccount indique que le nom uniqueness (appliqué par DB) est une propriété (vous pouvez par exemple l’utiliser pour définir la méthode equals ()). Si de l’argent est transféré entre comptes, le montant total dans ces comptes doit être une constante – c’est une propriété.
Si je modifie ma classe BankAccount et que j’y ajoute une association ‘twig’:

 BankBranch branch 

Ensuite, c’est aussi une propriété:

 assert BankAccount.findAllByBranch(b).every{it.branch == b}. 

(Édité (e), cette propriété doit être techniquement appliquée par la firebase database et l’implémentation de la méthode find et du développeur peut supposer qu’elle est “sûre” et incassable. En fait, la plupart des critères “where” et “join” utilisés par votre application quelque part sous hibernate définissent des propriétés. de nature similaire.).

Un problème de repète répétable peut entraîner la rupture de la plupart des propriétés en cas d’utilisation concurrente (ce qui est effrayant!). Par exemple, je répète ici un morceau de code que j’ai écrit dans le ticket JIRA concerné et lié à la question:

 ... a1 has branch b1 BankAccount.findByName('a1') ... concurrently a1 is moved to branch b2 //fails because stale a1.branch == b1 assert BankAccount.findAllByBranch(b2).every{it.branch == b2} 

Votre application utilise probablement des propriétés explicites et implicites et peut avoir une logique pour les appliquer. Par exemple, l’application peut s’appuyer sur des noms uniques et va exclure ou renvoyer des résultats incorrects s’ils ne sont pas uniques (peut-être que nom seul est utilisé pour définir des équivalents ()). C’est un usage explicite. Application peut fournir des vues de liste et ce sera très embarrassant si la liste affiche la propriété violée (la liste des comptes sous la twig b2 montre certains comptes avec la twig b1 – ceci est une utilisation implicite). N’importe lequel de ces cas sera affecté par “findable reproductible”.

Si le code Grails (et non la contrainte de firebase database) est utilisé pour appliquer une propriété, en plus du «détecteur répétable», des problèmes de concurrence plus évidents doivent être résolus. (Je ne discute pas ici.)

Trouver des problèmes:

( Cela concerne uniquement les propriétés endommagées. Je ne sais pas si un outil de recherche pouvant être répété provoque d’autres problèmes. )

Donc, je pense que la première étape consiste à identifier toutes les propriétés de l’application (EDITED: il y en aura beaucoup, potentiellement trop pour pouvoir être examinées – donc, en se concentrant sur les objects de domaine susceptibles de changer simultanément, peut-être la clé ici.), La deuxième étape consiste à identifier où et comment l’application (implicite ou explicite) utilise ces propriétés et comment elles sont appliquées. Code pour chacun de ces besoins doit être examiné pour vérifier que finder reproductible n’est pas le problème.

C’est peut-être une bonne idée de simplement activer le traçage SQL (ainsi que de déterminer le sharepoint départ et de fin de chaque demande HTTP) et d’examiner les traces du journal à partir de zones de préoccupation identifiées pour tout nom de table figurant dans la partie “De” de SQL. Si un tel tableau apparaît plus d’une fois par requête, cela peut être une bonne indication d’un problème. Une couverture de bon test fonctionnel pourrait aider à générer de tels fichiers journaux.

Ce n’est évidemment pas un processus sortingvial et il n’y a pas de solution à toute épreuve ici.

Problèmes de réparation:

L’utilisation de discard () sur des objects de requêtes précédentes ou l’exécution de la requête qui repose sur certaines propriétés / propriétés d’application dans une session d’hibernation distincte devraient résoudre le problème. Utiliser une nouvelle approche de session devrait être plus efficace. Je ne recommande pas d’utiliser refresh () ici. (Remarque: hibernate ne fournit aucune API publique pour interroger les objects attachés à sa session.)
L’utilisation d’une nouvelle session exposera l’application à de nouveaux problèmes tels que LazyInitalizationException ou DupicateKeyException. Ce sont sortingviaux en comparaison.

NOTE LATÉRALE: Personnellement, j’envisage la décision de conception de la structure qui provoque la rupture du code lorsqu’une requête supplémentaire est ajoutée: c’est un défaut de conception terrible.

Il est intéressant de comparer Hibernate à Active Record (dont je connais beaucoup moins). Hibernate a adopté l’approche puriste de l’ORM en essayant de transformer le SGBDR en OO, Active Record a adopté l’approche «sans partage» consistant à restr plus proche de la firebase database et à traiter les problèmes de concurrence plus complexes.
Bien sûr, dans Active Record, node.children.first (). Parent! = Parent, mais est-ce une si mauvaise chose?
J’avoue ne pas comprendre les raisons qui ont motivé la décision d’hibernation de ne pas actualiser les objects dans son cache lorsque la nouvelle requête est exécutée. Ont-ils été préoccupés par les effets secondaires? Peut-on faire pression sur Hibernate et Grails pour changer cela? Parce que cela semble être la meilleure solution à long terme. (Édité le 24/09/2014: mes efforts pour que Hibernate résolve le problème ont échoué.)

AJOUTÉ (2014/08/12): Il pourrait également être utile de repenser la conception de votre application Grails et d’utiliser GORM / Hibernate uniquement en tant que couche de persistance très fine. La conception d’une telle couche avec un contrôle étroit des requêtes émises lors de chaque requête devrait minimiser ce problème. Ce n’est évidemment pas ce que préconise le référentiel Grails (EDITED 24/09/2014 et cela ne fera que réduire le problème).

Après mûre reflection, il me semble que cela constitue peut-être un trou logique majeur dans la stack technologique de Grails / Hibernate. Il n’y a vraiment pas de bonne solution si vous vous souciez de la concurrence, vous devriez être inquiet.

Les lectures répétables sont un moyen d’ empêcher les mises à jour perdues dans une transaction de firebase database. La plupart des applications utilisent un modèle d’access aux données en lecture-modification-écriture brisant les limites des transactions de la firebase database et poussant les transactions vers la couche application .

Hibernate utilise une politique d’écriture différée transactionnelle , de sorte que les transitions d’état des entités sont retardées autant que possible, afin de réduire le locking de la firebase database associé aux instructions DML.

Dans une transaction au niveau de l’application, le cache de premier niveau agit comme un mécanisme de lecture répétable au niveau de l’application. Cependant, bien que le locking de la firebase database garantisse la cohérence de lecture reproductible lors de l’utilisation de transactions physiques, vous avez besoin d’un mécanisme de locking au niveau de l’application. C’est pourquoi vous devriez toujours utiliser le locking optimiste en premier lieu.

Le locking optimiste permet aux autres utilisateurs de modifier vos données précédemment chargées tout en vous empêchant de mettre à jour des données obsolètes.

Ce n’est pas l’égal qui est cassé. Les contraintes de firebase database doivent toujours appliquer des clés métier uniques.

Pour les opérations concernant les mises à jour de compte, vous devez utiliser une transaction de firebase database unique garantissant la sécurité via des acquisitions de verrous (SELECT FOR UPDATE) ou utiliser un locking optimiste. Ainsi, lorsque d’autres personnes mettent à jour vos données, vous obtenez une exception d’entité périmée.

Je pourrais reproduire votre cas d’utilisation . L’entité est réutilisée à partir du cache de premier niveau. Pour les requêtes SQL, vous avez la possibilité de charger des modifications simultanées. Tant que vous chargez des entités pour les mettre à jour ultérieurement, cela devrait aller, car le mécanisme de locking optimiste vous empêchera de sauvegarder des données obsolètes.

Si vous utilisez HQL / JPQL uniquement pour la visualisation, vous pouvez utiliser des projections à la place.

Un bon article de Marc Palmer sur ces problèmes. Je l’ai trouvé très intéressant. À la fin de l’article, il donne quelques “solutions” qui pourraient répondre aux besoins de certains d’entre vous.

Le faux optimisme de GORM et Hibernate

Pour autant que je puisse comprendre la question, le problème se résume à un isolement insuffisant des transactions de firebase database.

Je suggérerais également que ce problème peut exister dans n’importe quelle application, avec n’importe quel cadre pour l’access à une firebase database.

Dans une transaction de firebase database, vous devez tenir pour acquis que vous êtes le seul accesseur à la firebase database et que vous avez une vue cohérente de la firebase database dans cette transaction.

Lors de la validation, vous pouvez constater que l’état a changé d’une manière incompatible avec les modifications que vous avez apscopes, et votre transaction sera annulée.

Si vous n’effectuez que des access en lecture seule, vous devez toujours tenir compte de la cohérence de la scope de votre transaction et de la firebase database pour vous protéger des modifications simultanées.

Le cache de second niveau d’Hibernate couvre les transactions, il doit donc être effacé en cas de modifications simultanées et, dans tous les cas, une firebase database peut être modifiée par d’autres applications. Le cache de second niveau doit donc être utilisé avec précaution.

Mais vous avez déjà dit que le cache de deuxième niveau n’est pas votre problème. Eh bien, je suis d’accord. Votre problème ressemble à un degré insuffisant d’isolement des transactions dans votre firebase database. Cela peut-il être adressé?