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=256M03 — 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:
- QueueBacklog: Eine fällige Nachricht wartet länger als 15 Minuten, oder die Zahlen haben sich seit 15 Minuten nicht bewegt.
- MessagesParked: In der Fehler-Warteschlange liegt etwas. Hier entscheidet ein Mensch.
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.
Über den Autor

Kai Ole Hartwig
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.