Zum Inhalt springen

Worker & Cron-Jobs

Die meisten Anwendungen brauchen mehr als den Webserver. Irgendetwas muss die Queue abarbeiten, nachts den Newsletter verschicken oder einen WebSocket offen halten. Dafür gibt es in Envoryx Worker und Cron-Jobs.

Der Bereich Worker & Cron mit einem Laravel-Queue-Worker und dem Scheduler, beide laufend, dem Formular für einen weiteren Worker und dem ersten Cron-Job darunterDer Bereich Worker & Cron mit einem Laravel-Queue-Worker und dem Scheduler, beide laufend, dem Formular für einen weiteren Worker und dem ersten Cron-Job darunter

Ein Worker ist ein Prozess, der dauerhaft neben deiner Anwendung läuft - ein Queue-Worker, ein Scheduler, ein WebSocket-Server. Du legst ihn im Bereich Worker & Cron des Projekts an und wählst aus einer Liste von Vorlagen, was er ausführen soll:

Runtime Vorlagen
PHP Laravel Scheduler (schedule:work), Queue-Worker (queue:work, Queue-Namen), Queue-Listener (queue:listen), Horizon, Reverb; Symfony Messenger-Consumer (Transports) und Symfony Scheduler; Shopware Message Queue und Geplante Aufgaben; Craft-Queue; PHP-Script; Composer-Script
Node.js npm-Script (npm run <name>), Node.js-Script (node <datei>)
Python Python-Script, Python-Modul (python -m), Django manage.py-Befehl, Celery-Worker, Celery Beat
Go Go-Programm - baut ein Paket des Moduls und startet das Binary
Ruby Solid Queue (bin/jobs start), GoodJob (optional Queues), Sidekiq (optional Queues; Redis dazunehmen), Rake-Task, Ruby-Skript
Java Jar-Datei (java -jar <datei>), Build-Tool-Goal (mvn <goal> oder gradle <task>)
.NET Projekt (veröffentlicht das Projekt und startet es), DLL (dotnet <datei>)

Jeder Worker bekommt einen eigenen Container (envoryx-<projekt>-worker-<name>) aus dem Image seiner Runtime. Er läuft als Projektbesitzer mit der Umgebung des Projekts, startet nach einem Absturz von selbst neu und startet, stoppt und startet neu zusammen mit dem Projekt. Er läuft unter Dockers Init und bekommt beim Stoppen ein SIGTERM, ein Stopp oder Neustart wirkt also sofort, statt 30 Sekunden auf das Kill zu warten. Ein Job, an dem er in dem Moment arbeitet, wird dabei allerdings abgebrochen; für eine saubere Übergabe nimmst du den Befehl deines Frameworks (siehe unten). Du siehst nur Vorlagen für Runtimes, die das Projekt auch hat. Entfernst du eine Runtime, pausieren ihre Worker und kommen zurück, sobald die Runtime wieder da ist. Das Formular startet mit der Vorlage, die zum Projekt passt: Shopwares Message Queue in einem Shopware-Shop, Crafts Queue in Craft, Laravels Queue-Worker neben einer artisan, der Messenger-Consumer in einer Symfony-App mit Messenger.

Ein paar Details ersparen dir Rätselraten. Ruby-Worker führen vorher bundle install aus, wenn das Bundle unvollständig ist, und bekommen dieselbe Umgebung wie der Server (RAILS_ENV und so weiter). Geprüft wird das allerdings beim Start: Ein laufender Worker behält die Gems, mit denen er gestartet ist. Nach bundle add oder einer anderen Änderung am Gemfile startest du den Worker mit Neustart neu, damit er das neue Bundle lädt. Ein Solid Queue-Worker, dem seine Tabellen fehlen, wartet mit einem Hinweis im Log, statt in einer Schleife neu zu starten; führ rails db:prepare aus, dann legt er los (das Rails-Template gibt Solid Queue eine eigene Datenbank <database>_queue, siehe die Ruby-Seite). Die wiederkehrenden Aufgaben aus config/recurring.yml laufen im selben Worker mit, dafür brauchst du also weder einen zweiten Worker noch einen Cron-Job. Der Symfony-Messenger-Consumer führt für seine Transports zuerst messenger:setup-transports aus. Das legt die Tabelle messenger_messages an, die das doctrine://default?auto_setup=0 des Recipes der Anwendung überlässt, und solange die Datenbank noch nicht erreichbar ist, wartet der Worker mit einem Hinweis, statt in einer Schleife neu zu starten. Laravels queue:work beendet sich absichtlich nach einer Stunde, damit der automatische Neustart deine Codeänderungen mitnimmt; sollen Änderungen sofort greifen, nimm queue:listen. Auf die Stunde musst du aber auch nicht warten: php artisan queue:restart im PHP-Terminal sagt dem Worker, er soll seinen aktuellen Job fertig machen und sich beenden, und Envoryx startet ihn mit dem neuen Code wieder. Der Symfony-Messenger-Consumer hört ebenfalls nach einer Stunde auf, bei ihm erledigt php bin/console messenger:stop-workers dasselbe.

Die Vorlage npm-Script ruft immer npm run auf, auch in einem pnpm- oder Yarn-Projekt. Das ist in Ordnung, npm führt das Script aus der package.json genauso aus. Die Vorlage Composer-Script startet composer run-script --timeout=0, damit die übliche Grenze von 300 Sekunden pro Script deinen Worker nicht nach fünf Minuten beendet.

Node.js-Worker und die Cron-Jobs im Node.js-Container übernehmen ihr NODE_ENV vom Dev-Server: production, solange er einen Produktions-Build ausliefert, sonst development, auch in einem Projekt ohne Dev-Server. In der Entwicklung gewinnt ein NODE_ENV, das du im Bereich Umgebung setzt, genau wie beim Dev-Server; im Produktionsmodus ist es in jedem Fall production. Ein Projekt, das seinen Produktions-Build laufen lässt, arbeitet seine Queue also nicht im Entwicklungsmodus ab. Wie der Dev-Server wartet ein Node.js-Worker auf node_modules, statt abzustürzen, überlässt die Installation aber dem Dev-Server oder dir. Python-Worker warten, solange die .venv für eine andere Python-Version gebaut ist, siehe die Python-Seite.

Die Ausgabe jedes Workers steht im Bereich Logs, und ein Projekt kann bis zu 10 davon haben.

Neustart neben einem Worker startet nur diesen einen Container neu, der Rest des Projekts läuft weiter. Das brauchst du immer dann, wenn ein Worker neuen Code laden soll (ein Queue-Worker lädt deine Anwendung nur einmal, beim Start), und sobald du behoben hast, woran er abgestürzt ist. Ein Worker, der kurz nach jedem Start wieder endet, steht auf restarting statt running, das Projekt auf teilweise laufend; warum, steht in seinem Log. Docker wartet vor jedem neuen Versuch etwas länger, der Neustart-Knopf überspringt diese Wartezeit. Neu starten dürfen auch Entwickler, Worker anlegen und ändern bleibt den Admins des Projekts vorbehalten.

Ein neues Laravel-Projekt aus der Vorlage führt artisan migrate aus, sobald es läuft. So sind die Tabellen für Jobs, Cache und Sitzungen da, bevor der Queue-Worker startet. Bei einer geklonten oder importierten App startest du die Migration selbst über Aktionen.

Weiter unten im selben Bereich Worker & Cron stehen die Cron-Jobs. Sie führen beliebige Shell-Befehle nach Zeitplan im PHP-, Python-, Go-, Ruby-, Java-, .NET- oder Node.js-Container des Projekts aus. Das passiert als Projektbesitzer im Projektverzeichnis, über sh -c - Pipes und && gehen also - und mit der Umgebung des Projekts.

Du wählst einen Zeitplan (alle paar Minuten, stündlich, täglich, wöchentlich, monatlich) oder gibst einen Cron-Ausdruck ein; das Formular zeigt dir die nächsten Läufe, eine gute Kontrolle, ob du dich nicht vertippt hast. Die Zeiten gelten in der Zeitzone von Envoryx - stimmen sie nicht, setz TZ am Envoryx-Container. Ein neuer oder geänderter Job läuft zum ersten Mal zu der nächsten Zeit, die das Formular beim Speichern gezeigt hat; speicherst du einen Job für jede Minute um 10:15:40, läuft er um 10:16.

Über dem Formular stehen ein paar Ausgangspunkte für die Runtimes des Projekts, darunter Laravel-Scheduler, TYPO3-Scheduler und Drupal-Cron. Ein Klick trägt den Befehl und einen passenden Zeitplan ein.

Jobs laufen nur, solange das Projekt läuft, und ein Job, der noch läuft, wird nicht ein zweites Mal gestartet - langsame Läufe stapeln sich also nicht. Ein Timeout (standardmäßig 10 Minuten, bis zu 24 Stunden) beendet Befehle, die hängen. Jetzt ausführen startet einen Job sofort - praktisch, solange du ihn einrichtest. Die letzten 20 Läufe behalten ihre Ausgabe, und schlägt ein Lauf fehl, bekommst du eine Benachrichtigung.

Ein Shopware-Shop will zwei Worker, und für beide gibt es eine Vorlage. Message Queue startet messenger:consume async low_priority und richtet vorher beide Transports ein. Geplante Aufgaben startet scheduled-task:run, das Shopwares Aufgaben in die Queue stellt, sobald sie fällig sind; ohne den ersten Worker bringt er also nichts. Beide starten jede Stunde neu und haben ein Speicherlimit von 512 MB, damit ein langsames Leck nicht immer weiter wächst. Die Vorlage Symfony Scheduler lässt du links liegen: Sie verarbeitet scheduler_default, und das benutzt Shopware nicht. Wie du den Admin-Worker abschaltest, der sonst dieselbe Arbeit erledigt, solange der Admin offen ist, steht auf der PHP-Seite.

workers:
- {name: queue, preset: "shopware:queue"}
- {name: scheduled-tasks, preset: "shopware:scheduled-tasks"}

TYPO3 und Drupal brauchen nur einen Cron-Job, und das Cron-Formular hat für beide einen Ausgangspunkt. Der Scheduler von TYPO3 läuft mit vendor/bin/typo3 scheduler:run jede Minute. Er ist eine eigene Extension, typo3/cms-scheduler: Das TYPO3-Template installiert sie, ein mitgebrachtes TYPO3 hat sie aber vielleicht nicht, und dann scheitert der Job mit There are no commands defined in the “scheduler” namespace. Führ einmal composer require typo3/cms-scheduler und vendor/bin/typo3 extension:setup im PHP-Terminal aus. Drupal startet seinen Cron mit vendor/bin/drush cron alle 15 Minuten, das reicht völlig; der Lauf ist stumm, eine leere Ausgabe ist also normal. Craft arbeitet seine Queue ab, solange jemand das Control Panel offen hat. Die Vorlage Craft-Queue (php craft queue/listen) erledigt das auch ohne, damit Bild-Transformationen nach einem Upload fertig werden, auch wenn gerade niemand hinschaut.

Worker und Cron-Jobs gehören zum Projekt-Manifest und wandern damit mit dem Repository:

workers:
- {name: queue, preset: "laravel:queue", arg: default}
cron:
- {name: prune, schedule: "0 3 * * *", command: php artisan model:prune, timeout: 5m}

Die CLI macht dasselbe aus einer Shell oder einer Pipeline heraus:

Terminal-Fenster
envoryx worker add shop queue --preset laravel:queue --arg default
envoryx cron add shop prune --schedule "0 3 * * *" --max-time 5m -- php artisan model:prune
envoryx cron run shop prune --wait # führt ihn sofort aus und endet mit seinem Exit-Code