Handling transactions in a ZEO connection

Lorsque votre navigateur ouvre une requête sur un serveur ZopeApp/Bluebream, ce dernier démarre automatiquement une transaction qui est validée (« commit ») lorsque le traitement de la requête s'achève sans erreur, ou annulée (« abort ») lorsqu'une exception se produit.

Mais lorsque l'on utilise une ZODB, une exception spécifique peut se produire : une « ConflictError » !

Pour simplifier, à chaque objet stocké dans la ZODB est associé le numéro de la transaction qui l'a mis à jour pour la dernière fois. Lorsque l'on veut stocker un objet modifié dans la base de données, le système vérifie que ce numéro de transaction est toujours le bon ; dans le cas contraire, cela indique que l'objet a été modifié dans la base de données, par un autre processus, entre le moment où il a été chargé en mémoire et le moment de la nouvelle sauvegarde.

Ce mécanisme fait partie du mode de fonctionnement normal de la ZODB. Le serveur d'application prend donc en charge automatiquement ce type d'exception en rejouant automatiquement, jusqu'à trois fois, une requête au cours de laquelle cette exception est levée.

Gérer les transactions dans une connexion dédiée

J'utilise de plus en plus des threads voire des processus dédiés, notamment pour lancer des tâches de fond spécifiques nécessitant un long temps de traitement (exemple : conversion de fichiers vidéo au format web après leur publication, exécution de tâches de maintenance répétitives...). Cette mécanique repose le plus souvent sur l'utilisation d'une connexion ZEO dédiée, de façon à autoriser les mises à jour de façon asynchrone alors que le résultat de la requête initiale a déjà été renvoyé à l'utilisateur.

Dans le cadre de ces connexions ZEO, les conflits d'écriture ne sont plus pris en charge par le serveur d'application. Il est donc nécessaire de les prendre en charge soi-même !

Le schéma général est proposé dans le code ci-dessous, qui sera commenté ensuite :

  1. La classe ZEOConnectionInfo vient du package « ztfy.utils » ; elle permet de définir les paramètres d'une connexion ZEO. Elle peut être stockée et enregistrée de façon persistante dans la ZODB.
  1. On récupère la « vraie » connexion à la ZODB
  1. La connection à la base de donnée est ouverte et on récupère la « racine » de la base de données
  1. On suppose que l'on effectue ici un parcours dans la base de données pour rechercher un objet sur lequel on va appliquer un traitement. L'emplacement de cet objet (la variable « target_path ») dépendra bien évidemment des cas d'utilisation.
  1. À partir de la cible, on recherche le « gestionnaire de site » et on appelle la méthode « setSite » sur ce site pour que toutes les recherches d'utilitaires ultérieures soient basées sur lui.
  1. C'est ici que l'on démarre réellement le traitement des transactions. On commence pour cela par rechercher le gestionnaire de transaction associé à notre objet cible.
  1. Le gestionnaire de transaction fournit une méthode « attempts », qui accepte un paramètre par défaut qui est le nombre de tentatives que l'on veut effectuer. La valeur par défaut est 3. Cette méthode renvoie un « gestionnaire de contexte », que l'on utilise donc avec le mot-clé « with », qui va automatiquement prendre en charge la gestion des erreurs de conflits (mais uniquement celles-ci). À chaque itération, une nouvelle transaction est démarrée, le traitement est effectué et un « commit » est tenté ; en cas de « ConflictError », la transaction en cours est annulée et on recommence un nouveau cycle. Dans le cas contraire, on vérifie le statut de la transaction et l'on quitte la boucle si la transaction a pu être enregistrée.
  1. À la fin du traitement, on fait le ménage dans les éventuelles transaction en cours, et l'on n'oublie surtout pas de fermer la connexion à la base de données !

Quelques précautions...

Comme indiqué dans le paragraphe précédent, une transaction est démarrée par le gestionnaire de contexte dans sa méthode « __enter__ », donc à partir de la ligne contenant le « with ». Ce démarrage de transaction a pour effet immédiat d'annuler toute transaction antérieure ! Ce qui signifie simplement que si vous modifiez des objets avant d'arriver à cette ligne, ces modifications ne seront pas conservées !

Toutes les modifications de données doivent donc impérativement être effectuées à l'intérieur de ce bloc « with ».

C'est là la conséquence naturelle du fait qu'avec la ZODB, lorsqu'une transaction est annulée, ce sont tous les objets en mémoire modifiés dans le cadre de cette transaction qui sont rechargés à partir de la base de données. À la différence des bases de données relationnelles, pour lesquelles les données en mémoire et les données stockées en base peuvent être totalement désynchronisées.