| « Symposium DNG 2004 | La couche de présentation est-elle la moins stable ? » |
L'API System.Transactions
L'API System.Transactions sera au coeur du Framework 2.0 et constituera le premier socle transactionnel en mode managed digne de ce nom chez Microsoft. Il faut dire que nombreux étaient ceux qui réclamaient quelque chose de plus sérieux que les enterprise services. C'est chose faite avec Indigo, l'équipe de Don Box a considérablement travaillé ces dernier mois pour fournir une implémentation minimale de cette API. Je me suis intéressé à cette implémentation et j'ai surtout voulu comparer System.Transaction à JTA/JTS (Java Transaction API), utilisée dans la plupart des serveurs d'application J2EE.
Si ce sujet est si important (en tout cas à mes yeux) c'est que demain on verra peut-être apparaître les premières applications du type Hibernate/JTA avec le couple ObjectSpaces/System.Transactions. Dans une architecture serveur, les transactions sont quasiment indispensables. A noter que je ne parle pas uniquement des transactions distribuées (qui implique plusieurs connexions ou "ressources" différentes), mais aussi des transactions locales, de plus en plus prises en charge dans la couche applicative et non plus dans la couche données (pour résumer, ce n'est plus au provider ADO.NET de jouer le rôle de coordinateur).
Côté fonctionnalité top moumoute, le team Indigo a donc pris à bras le corps l'API System.Transactions et nous a concocté le TransactionScope. Son rôle consiste à entourer un bloc donné afin de gérer automatiquement le commit() et le rollback(). Si aucune exception n'est survenue, toutes les requêtes SQL envoyées à la base sont commitées, sinon un rollback est effectué. Bref, ceux qui connaissent les EJB et JTA retrouveront un principe similaires.
Voici un exemple de code.
Quelques explications :
Lorsque vous ouvrez une connexion avec SqlConnection.Open(), un enlist() automatique est effectué sur la transaction courante créée préalablement par TxScope. C'est le cas de toutes les connexions SQL ouvertes dans le bloc using (lors du dispose, un commit est effectué si et seulement si TransactionScope.Consistent est à Vrai). Ce changement est fondamental pour la suite car cela signifie en pratique que les transactions sont désormais gérées à votre place (et même de manière déclarative) sans avoir à utiliser les entreprise services (plus d'enregistrement dans le GAC et de casse tête de déploiement). De plus, lorsqu'une transaction est créée avec une seule ressource (une seule base de donnée), la transactions locale du provider est utilisée plutôt que la transaction distribuée, plus contraignante et surtout moins performante (elle nécessite une phase de prepare avant de commiter, d'où l'appellation two-phase commit).
Vous vous demandez peut-être comment une connexion SQL peut-elle "par magie" s'enregistrer dans une transaction courante ? Et bien Microsoft utilise un principe aussi très vogue dans le monde des serveurs d'application J2EE, le TLS (Thread Local Storage). En effet, pour récupérer la transaction courante, l'utilisateur fait : "Transaction.Current". Or "Current" est une propriété statique de ITransaction. Si "Current" n'était pas du type [StaticTreadAttribute], en mode multi-thread, plusieurs personnes risqueraient de prendre la main sur la même Tx.
Une autre fonctionnalité intéressante de l'API System.Transaction est la possibilité de créer soit même une ressource qui va s'enlister automatiquement dans la transaction courante. Vous avez également la possibilité de vous faire notifier via le handler IEnlistmentNotification des évènements de commit, prepare, etc ... Cela peut-être très intéressant pour synchroniser un état particuler. Les providers de SGDB implémentent la plupart cette classe.
A noter toutefois que je n'ai pas réussit à redéfinir celui utilisé par défaut par SqlConnection. Ce qui est dommage lorsqu'on ne souhaite pas créer de ressource spécifique mais simplement écouter ce qu'il se passe avec les providers existants comme c'est le cas avec la classe SessionSynchronization de J2EE. J'ai remonté la question le wiki de Don Box, en attendant une réponse ...
J'ai aussi eu pas mal de difficulé à changer le mode d'isolation transactionnelle par défaut (Serializable) qui dépend essentiellement du provider. Microsoft souligne dans sa doc que changer ce paramètre peut n'avoir aucun effet. Ce fût mon cas. Dommage, car en production, ce mode est à proscrire totalement.
Bref, je reviendrais dans d'autres articles sur System.Transactions, mais il faut avouer que même si on est encore loin d'atteindre le niveau de JTS/JTA en terme de cohérence des API (Microsoft doit jouer avec la compatibilité EntepriseServices/OleTx/..., ce qui impose parfois des phases de marshalling de paramètres), On est sur la bonne voie ...
MAJ : Don a répondu à mes questions mais le sujet reste toujours ouvert.
MAJ 2 : Ca discute toujours ;-). Je suis assez étonné en fait de voir que MS n'a pas vraiment regardé ce qui se faisait côté J2EE avec JTA/JTS (qui pour moi reste un modèle de référence en terme de gestion des transactions). En même temps, on ne pourra guère les accuser de plagiat :-)
4 commentaires
Je viens juste de parcourir l'article et je me pose une question sur l'accès à "Transaction.Current" et sur le pattern "Thread Local Storage". Si j'ai bien compris, tu dis que ce pattern est mis en oeuvre grâce à un champ statique "unique par thread" (attribut [ThreadStatic]). Or ce type de champs pose problème lors d'une utilisation via ASP.NET 1.0 et IIS (réutilisation des threads par IIS) : le champ est bien unique par thread mais pas unique par requête (je ne suis pas sûr de ce que j'avance car c'est lié à une simple expérimentation perso => je n'ai pas trouvé de doc à ce sujet).
Un même champ peut donc se retrouver partagé par plusieurs utilisateurs, ce qui pose problème pour la transaction courante.
Pour contourner ce problème on peut utiliser une zone de stockage unique par requête et non unique par thread (System.Web.HttpContext.Current.Items) en ASP.NET 1.0.
Ma question est "Dans .Net 2.0, Microsoft a-t-il modifié le comportement de l'attribut [ThreadLocal] de façon a ce que l'interaction avec IIS soit plus fiable ?"
Très bonne question. Si une transaction doit avoir une durée de vie supérieure à celle d'une WebRequest, alors effectivement il vaut mieux utiliser HttpContext.Current.Items. Ceci dit, il faut garder à l'esprit que la transaction est initiée dans la couche de service (BLL) et sa durée de vie est souvent égale à celle du service (le ThreadID de la WebRequest ne bouge pas entre deux appels de méthode BLL).
Ce cas de figure peut se produire lorsqu'on utilise des Transaction "longues", déconseillée dans la mesure du possible.
Sami
Je suis tout à fait d'accord sur le fait d'éviter les transactions longues. HttpContext.Current.Items ne peut pas permettre le "stockage temporaire" d'une transaction dont la durée de vie est supérieure à la WebRequest car sa durée de vie est justement la même qu'une WebRequest.
Ma remarque sur [ThreadLocal] portait sur la notion de réutilisation d'un Thread existant par IIS : je craignais de voir un Thread interrompu par IIS (une sorte de multi-activités au sein du thread comme on a du multi-thread au sein d'un processus) et donc de voir le pattern "Thread Local Storage" remis en cause dans le cas d'une utilisation via ASP.Net. Apparemment ce n'est pas le cas puisque tu indiques que le ThreadID de la WebRequest ne bouge pas au cours de son exécution.
Merci encore pour ces éclaircissements.