Kai Ole Hartwig
8 Min. Lesezeit
Niedrig

Mails und Bilder verlassen den TYPO3-Request

Für alle, die TYPO3 14 betreiben und bei denen ein Formular erst antwortet, wenn der Mailserver fertig ist. Bisher hat jeder Request seine langsame Arbeit selbst erledigt: Er hat die Mail zugestellt und die Bildgrößen von ImageMagick rechnen lassen. Heute landet beides in einer Warteschlange, die ein eigener Pod abarbeitet. Diese Feldnotiz erzählt, warum TYPO3 einer fehlgeschlagenen Nachricht von Haus aus keine zweite Chance gibt, weshalb sich Bildgrößen nicht vorab festlegen lassen und woran man einen Consumer erkennt, der hängt. Sie knüpft an die Feldnotiz zum Backend im eigenen Pod an.

01 — Was im Request nicht warten sollte

Ein Besucher schickt ein Formular ab. Bevor er die Antwortseite sieht, rendert TYPO3 die Bestätigungsmail und reicht sie an den Mailserver weiter. Ist der Mailserver langsam, wartet der Besucher mit. Antwortet er gar nicht, geht die Mail verloren, und niemand erfährt davon. Bei uns endete auf diese Weise einmal eine Passwort-Zurücksetzung mit einem Fehler 500.

Mit Bildern verhält es sich ähnlich. TYPO3 rechnet eine Bildgröße erst, wenn eine Seite sie anfordert, und ein Seitenkopf mit Srcset in drei Formaten fordert gleich sechzehn davon an, AVIF eingeschlossen. Diese Arbeit fällt auf die erste Ansicht nach dem Veröffentlichen: auf die Vorschau der Redaktion, auf den ersten Besucher oder auf einen Crawler.

In beiden Fällen braucht der Request das Ergebnis gar nicht. Der Besucher will seine Antwort, nicht die Zustellung der Mail abwarten. Und die Seite braucht fertige Bilder, nicht ihre Herstellung.

02 — Symfony Messenger in TYPO3 14, aber ohne Netz

TYPO3 14 bringt Symfony Messenger mit. Diese Komponente legt Arbeit als Nachricht in eine Warteschlange, statt sie sofort zu erledigen. Ein eigener Prozess, der Consumer, holt die Nachrichten dort ab und arbeitet sie ab. In TYPO3 ist das der Befehl messenger:consume. Auf den ersten Blick ist damit alles da.

Auf den zweiten fehlt das Netz. Der Consumer des Kerns registriert weder eine Wiederholung noch eine Fehler-Warteschlange. Scheitert der Code, der eine Nachricht verarbeitet, meldet der Consumer sie trotzdem als erledigt. Dann ist sie verschwunden. Für eine Mail bedeutet das: Ein kurzer Aussetzer beim Mailserver genügt, und sie kommt nie an. Bemerken wird das niemand, denn nirgends erscheint ein Fehler.

Diese Lücke schließt die Extension koh/typo3-async. Symfony bringt die Bausteine für Wiederholung und Fehler-Warteschlange schon mit. Die Extension hängt sie in die Ereignisverarbeitung von TYPO3 ein. Jede Nachricht bekommt drei Versuche im Abstand von 30, 60 und 120 Sekunden. Scheitern alle, landet sie in der Warteschlange failed, statt zu verschwinden.

Gerendert wird die Mail weiterhin im Request, denn nur dort kennt TYPO3 Site, Sprache und Links. Der Consumer übernimmt allein die Zustellung.

 

vendor/bin/typo3 messenger:consume koh_async_mail koh_async_images \
  --time-limit=3600 --memory-limit=256M

03 — Ein Consumer pro Mandant, und der Schalter gehört der Plattform

Die Warteschlange liegt in der Datenbank des Mandanten, in der Tabelle sys_messenger_messages. So läuft sie im Backup mit, und es braucht keinen zusätzlichen Dienst. Valkey, der Cache-Server der Plattform, schied aus. Ein Cache darf unter Speicherdruck Schlüssel verwerfen, eine Warteschlange darf das nicht.

Den Consumer baut das Helm-Chart aus derselben Pod-Vorlage wie die App. Er hat damit dieselbe Identität, dieselben Netzwerkregeln und denselben Code. Er läuft aber als eigenes Deployment und startet sich stündlich selbst neu. Ein CronJob im Fünf-Minuten-Takt wäre die einfachere Wahl gewesen. Dann hätte eine Mail zur Passwort-Zurücksetzung aber bis zu fünf Minuten auf sich warten lassen.

Wichtiger als der Pod ist, wer den Schalter umlegt. Eine Warteschlange, die niemand liest, verzögert eine Nachricht nicht, sie behält sie. Deshalb entscheidet nicht die Extension, ob Nachrichten in die Warteschlange gehen, sondern die Umgebung. Das geschieht nur, wenn MESSENGER_ASYNC=1 gesetzt ist, und diese Variable setzt das Chart ausschließlich zusammen mit dem Consumer. Wer sie ohne Consumer von Hand setzt, bei dem scheitert schon das Rendern des Charts, und es gibt keinen Rollout.

04 — Bildgrößen werden gelernt, nicht konfiguriert

Naheliegend war eine Liste: Diese Größen bitte nach jedem Upload rechnen. Daran scheiterte der erste Ansatz, und zwar an einem Detail. TYPO3 erkennt ein fertiges Bild an einer Prüfsumme über die Anweisungen, mit denen es entstanden ist. In diese Prüfsumme fließen auch die Reihenfolge der Schlüssel und leere Werte ein.

 

{"width": 640}
{"width": 640, "height": null}

 

Für TYPO3 sind das zwei verschiedene Bilder. Eine von Hand gepflegte Liste erzeugt deshalb schnell Bilder, die keine Seite je findet, und sie veraltet unbemerkt, sobald sich ein Template ändert.

Stattdessen liest die Extension die Anweisungen aus der Tabelle der bereits gerechneten Bilder, Schlüssel für Schlüssel und in der gespeicherten Reihenfolge. Was ein Template für mindestens zehn Prozent der Bilder angefordert hat, rechnet der Consumer künftig gleich nach dem Upload. Zuschnitte lernt sie über den Namen der Zuschnitt-Variante, den sie aus den Verknüpfungen der Datei mit Inhaltselementen ableitet. Was der Consumer selbst gerechnet hat, zählt dabei nicht mit. Sonst würde jede gelernte Größe sich mit jedem neuen Upload selbst bestätigen.

Schon der erste Lauf hat sich gelohnt. Meiner Liste, die ich vorher gegen die Templates geprüft hatte, fehlte eine Zuschnitt-Variante, die der Lernmodus auf Anhieb fand. Ein Testbild bekam 25 Größen in rund 90 Sekunden, und die Seite fand sie danach in 17 bis 100 Millisekunden.

05 — Der Consumer meldet sich selbst

Fällt der Consumer aus, merkt der Besucher davon nichts mehr. Genau das macht ihn gefährlich, denn er fällt still aus. Die Prüfung, mit der Kubernetes einen Pod überwacht, sieht nur einen laufenden Prozess. Ob dieser Prozess auch arbeitet, sieht sie nicht.

Deshalb schreibt der Consumer seine Zahlen selbst, aus seiner eigenen Schleife heraus und alle 15 Sekunden. Je Warteschlange sind das die Zahl der Nachrichten, das Alter der ältesten fälligen Nachricht und ein Zeitstempel. Ein kleiner Begleitcontainer im selben Pod liefert die Datei an Prometheus aus und braucht dabei keinen Datenbankzugang. Bleibt der Zeitstempel stehen, hängt die Schleife. Das erkennt kein anderer Mechanismus.

Daraus entstehen zwei Alarme:

Diesem Menschen stehen drei Befehle zur Verfügung. messenger:failed:show zeigt die Nachricht und ihren Fehler, koh-async:failed:retry legt sie zurück in ihre Warteschlange, und messenger:failed:remove verwirft sie.

06 — Zwei Fallen auf dem Weg

Die erste Falle steckte in einer Einstellungsvorlage. Version 1.1.0 brachte eine ext_conf_template.txt mit. Beim Deploy ruft TYPO3 extension:setup auf, und dieser Befehl schreibt die Standardwerte jeder Vorlage in die settings.php. Bei uns liegt der Code jedoch schreibgeschützt im Pod. Also scheiterte der Setup-Schritt vor dem Ausrollen, und Argo CD hielt den Rollout an. Weil die Mandanten nacheinander ausrollen, blieb auch der nächste stehen. Ausgefallen ist dabei keine Seite, beide liefen auf der alten Version weiter. Version 1.1.1 kam ohne Vorlage, und ein Test sorgt dafür, dass sie nicht zurückkehrt.

Die zweite Falle ist ein Befehl, den es nicht gibt. TYPO3 registriert nur einen einzigen: messenger:consume. Der naheliegende Ausweg wäre messenger:failed:retry aus Symfony. Dieser Befehl verarbeitet die Nachricht allerdings mit Symfonys eigener Verteilung, an TYPO3 vorbei. Schlägt sie dabei erneut fehl, greift keine Wiederholung mehr, und sie ist verloren. koh-async:failed:retry legt die Nachricht deshalb nur in ihre Warteschlange zurück und überlässt den Rest dem Consumer, mit allen drei Versuchen.

Häufige Fragen

Was passiert, wenn der Consumer ausfällt?+

Nichts geht verloren. Die Nachrichten bleiben in der Warteschlange und warten. Läuft der Consumer wieder, arbeitet er sie ab. Damit das nicht unbemerkt Stunden dauert, gibt es zwei Alarme: einen für den fehlenden Pod und QueueBacklog für eine Warteschlange, die nicht kleiner wird.

Lohnt sich das auf einer Seite mit wenigen Bildern?+

Für die Mails immer. Bei den Bildern spart eine bilderarme Seite eine langsame erste Ansicht pro neuem Beitrag. Viel bringt das Vorrendern bei Shops, Galerien und Magazinen, wo ständig neue Bilder kommen. Der Consumer braucht im Leerlauf rund 200 MiB Speicher und wenige Millicores.

Brauche ich dafür Kubernetes?+

Nein. Die Extension braucht TYPO3 14 und einen Prozess, der messenger:consume dauerhaft ausführt, zum Beispiel einen systemd-Dienst. Wichtig ist nur dieselbe Regel wie im Chart: MESSENGER_ASYNC=1 erst setzen, wenn der Consumer läuft.

Fazit

Langsame Arbeit gehört nicht in den Request. Seit Mails und Bildgrößen in einer Warteschlange landen, wartet kein Besucher mehr auf den Mailserver, und eine kurze Störung kostet keine Mail mehr. Neue Bilder sind fertig, bevor jemand die Seite öffnet.

Die eigentliche Arbeit steckte nicht in der Warteschlange selbst, sondern in dem, was TYPO3 nicht mitbringt: Wiederholung, eine Fehler-Warteschlange und einen Consumer, der sich selbst meldet. Dazu kam eine Einsicht über Bilder. Ihre Größen lassen sich nicht planen, nur aus dem lernen, was die Seite tatsächlich anfordert.

Wartet dein TYPO3 noch auf den Mailserver?

Ich sehe mir an, welche Arbeit deine TYPO3-Instanz im Request erledigt, und lagere Mails und Bildgrößen so aus, dass keine Mail mehr verloren geht und neue Bilder fertig sind, bevor jemand die Seite öffnet.

Termin buchen →

Über den Autor

Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Freiberuflicher DevSecOps-Berater · OnlyOle Consulting

Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.