Datenbank-Transaktionen mit Drupal 6

Heute hatte ich mal wieder die Anforderung, in einem auf Drupal 6 basierenden Projekt einige Prozessschritte in einer Datenbank-Transaktion zu kapseln, was Drupal leider nicht nativ anbietet.

Es galt über den Cron externe Daten einzulesen und daraus Drupal-Nodes zu erzeugen. Dabei ist sicherzustellen, dass entweder alle Knoten erfolgreich erzeugt werden oder gar keiner. Also Transaktionen ...

Eine schnelle Suche führte mich zum Modul Transaction: Magische Commits und keinerlei Information darüber, ob irgendwo in einem Drupal-Modul ein Datenbankfehler aufgetreten ist, der in meinem Anwendungsfall einen Commit verhindern soll. Kurz, dieses Modul schied aus.

Also habe kurzerhand ein eigenes kleines Modul geschrieben, das die Methoden begin, commit und rollback bereitstellt und alle Datenbankfehler, die irgendwo in Drupal auftreten, detektiert. Als Vereinfachung habe ich zudem auf die Unterstützung von verschachtelten Transaktionen verzichtet und MySQL vorausgesetzt.

MySQL und Transaktionen bedeutet, dass InnoDB als MySQL-Datenbank-Engine eingesetzt werden muss. Dank dem eingesetzten Cocomore-Drupal-Core, der seit Version 6.17.2 dafür sorgt, dass auch alle Tabellen von anderen Modulen in InnoDB angelegt werden, wenn diese Engine in MySQL aktiviert ist, war diese Voraussetzung bereits erfüllt.

Trotzdem funktionierte das Ganze nicht auf Anhieb. Ein künstlich erzeugter Datenbankfehler im Prozess sorgte nicht für einen Rollback.

Also habe ich das MySQL-Query-Log aktiviert, um dem Problem auf die Spur zu kommen. Der Datenbankfehler war im Log zu sehen. Auch das Rollback-Komando wurde am Ende abgesetzt. Aber vor dem ROLLBACK kam ein TRUNCATE cache_page.

Jetzt war das Problem klar. In Drupal löst ein node_save() ein unmittelbares Leeren des Page- und des Block-Caches aus. Seit Version 6.1x geschieht dies über ein schnelles TRUNCATE. Dummerweise ist TRUNCATE ein Statement, welches die Tabellenstruktur verändert und somit ein implizites COMMIT auslöst (siehe dazu Statements That Cause an Implicit Commit).

Da es sich in meinem Fall um neue Nodes handelte, war mir der Page-Cache egal, den Drupal hier "vorsichtshalber" leert. Also wie kann ich das TRUNCATE verhindern, ohne in den Drupal-Core eingreifen zu müssen. Die Lösung ist simpel. In Drupal gibt es die Optionen, eine minimale Lebensdauer für Cacheeinträge zu erzwingen. Ist diese Option gesetzt, wird ein Timer gestartet, anstelle sofort ein TRUNCATE auszuführen. Nachdem diese Option auf eine Minute gesetzt war, funktionierte der ROLLBACK.

Als Schönheitsfehler blieb aber noch das Zeitfenster von einer Minute, denn die könnte ja mitten im Prozess vorbei sein. Erst recht, wenn ein User 50 Sekunden vor meinem Prozess einen Node anlegt oder editiert.

An dieser Stelle hilft wieder der Cocomore-Drupal-Core, der eine Option anbietet, das nur der Cron die Caches bereinigt. Dies ist eine sinnvolle Performance-Optimierung, da es ansonsten einen willkürlichen User der Drupal-Seite trifft, der mit seinem Klick die Bereinigung der Caches auslöst und evtl. lange auf seine Seite warten muss. Zwischen den Cronläufen werden dann bei Bedarf gezielt einzelne Cacheeinträge verworfen.

Durch die Kombination dieser beiden Optionen war jetzt also sichergestellt, dass das TRUNCATE meine Transaktion nicht mehr unterbricht.

Für mein aktuelles Problem ist diese Lösung robust und ich überlege, das kleine Modul zur Transaktionssteuerung auf drupal.org zu veröffentlichen. Allerdings steckt der Teufel mal wieder im Detail und die Anzahl der Supportanfragen könnte sehr hoch sein. Kommentare sind daher herzlich Willkommen!