Spring TransactionSynchronization et SimpleThreadScope pour « remplacer » une connexion XA

Besoin : sur une appli Java, compenser la suppression du XA sur un couple JDBCJMS.

Les classes TransactionSynchronization et SimpleThreadScope du framework Spring nous ont grandement rendu service pour ça.

Sommaire

Contexte

Nous avions des connexions JDBC et JMS configurées en XA : le commit à deux phases nous assurait une transaction globale sur les mises à jour JDBC et JMS. Si une mise à jour sur l’une des deux échouait, ça faisait un rollback global sur les deux. C’est très efficace pour conserver de la cohérence entre les deux, en cas d’erreur.

Sauf qu’il n’y avait pas de gros besoin métier là-derrière, et le risque de rollback sur JMS est extrêmement faible dans notre cas. D’autre part, l’implémentation XA utilisée (Bitronix) nous causait des problèmes de stabilité des performances.

Mais comment supprimer le XA avec suffisamment de garde-fous pour ne pas perdre de données?

Il n’y a pas de miracle : en supprimant XA, on n’a plus de transaction globale. Il y a donc forcément des cas où une transaction peut être commitée (ou rollbackée) sans l’autre. L’objectif était de minimiser ces cas, en se basant sur le fait que la connexion JMS est très fiable (pas de règle de gestion, serveur et réseau stables), alors que le JDBC est beaucoup plus exposé à des rollbacks (erreurs SQL, contraintes d’intégrité etc).

L’idée est donc de faire d’abord toutes les requêtes JDBC, puis les envois JMS (si le JDBC n’a pas rencontré de problème). On fait le pari que l’envoi JMS n’échouera pas. Si cela arrive (cas qui devrait être rarissime), on ne peut plus revenir sur le JDBC, mais on peut au moins logger ou envoyer une alerte.

Comment faire les envois JMS le plus tard possible?

Première idée : déplacer les envois JMS juste après la fin de transaction JDBC

De cette manière, l’envoi JMS se fait uniquement si la transaction JDBC a été commitée.

Hélas, ce n’était pas facilement réalisable dans notre contexte technique, car il aurait fallu revoir la gestion des transactions ou le découpage en couches. Les transactions sont en effet gérées en AOP par Spring, directement sur les facades exposées à l’extérieur. Il aurait donc fallu rajouter des classes devant chacune de ces facades pour faire les envois JMS (en-dehors du contexte transactionnel jdbc). Et impacter tous les appelants (puisqu’ils devront appeler la nouvelle classe).

Seconde idée : déplacer les envois JMS juste AVANT la fin de transaction JDBC

Ca évite le problème cité ci-dessus, mais est plus risqué puisque le commit JDBC se fait juste après l’envoi JMS.

Or nous utilisons le framework Hibernate pour les accès en base. Si Hibernate n’a pas flushé ses objets (c’est-à-dire exécuté les requêtes SQL en base), certaines requêtes SQL peuvent donc toujours être exécutées après l’envoi JMS. Il aurait donc fallu forcer Hibernate à flusher avant l’envoi JMS.

Et surtout on a toujours besoin de modifier chacune des facades susceptibles d’utiliser du XA plus tard dans l’arbre d’appel, au risque d’en oublier lors d’un refactoring ultérieur.

Meilleure idée : utiliser la classe TransactionSynchronization Spring

La classe TransactionSynchronization est très adaptée à ce cas de figure. Elle se charge d’appeler notre code (envoi des messages JMS) au bon moment : juste après le commit (ou rollback) jdbc, via la méthode afterCompletion. Donc pas besoin d’ajouter nous-mêmes ce code sur chaque facade concernée : on écrit le code une seule fois, et Spring l’injecte pour nous. Et surtout, on n’a aucun risque d’oublier une facade.

Petite limitation : en cas d’erreur lors de l’envoi JMS, si on « rethrowe » l’exception, elle ne remontera pas la pile d’appel (jusqu’à l’utilisateur dans notre cas de figure). Spring se contentera de la logger (comme expliqué dans leur javadoc).

Comment garder les messages JMS « sous le coude » avant de les envoyer?

Les envois JMS sont parfois implémentés au beau milieu d’un gros algorithme, au bout d’un long arbre d’appel. Il faut trouver un moyen de « garder sous le coude » ces messages JMS.

On peut imaginer de passer une variable en paramètre sur tout l’arbre d’appel (et l’enrichir au fur et à mesure), mais ça ne tient pas la route longtemps s’il y a beaucoup de code. Et ce n’est pas très lisible, ni facilement testable.

Nous avons pensé à mettre un singleton en scope request ou session, sauf que les facades ne sont pas toutes appelées depuis un client HTTP. Certaines sont notamment déclenchées par la réception d’un message JMS.

La solution serait d’utiliser un ThreadLocal mais il nous a paru plus propre de s’appuyer sur Spring pour cela. Il y a en effet le scope SimpleThreadScope de Spring (qu’il faut activer programmatiquement), qui encapsule un ThreadLocal à notre place, et rend le code plus lisible.

Pourquoi ce scope n’est-il pas activé par défaut par Spring? Parce que, contrairement à des scopes comme request et session, Spring n’est pas capable de supprimer les données à la fin du traitement. Il est donc impératif de le faire soi-même, d’autant que les serveurs d’applications utilisent des pool de connexion HTTP, qui permettent de réutiliser un même thread pour plusieurs requêtes. Dans notre cas, il a suffit, dans la méthode afterCompletion, d’encadrer notre code par un try, avec un finally qui vide tous les messages JMS.

Bilan

L’utilisation de ces classes de Spring a largement limité la quantité de code modifié, ainsi que les risques (même code réutilisé partout, pas de possibilité d’oublier une facade).

Ca tourne maintenant en production et s’est révélé parfaitement efficace et stable.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *