flowchart TD
subgraph Sources["📡 Event-Quellen"]
WH["`🔗 Webhook<br/>**GET + Token**`"]
DC["`📞 Direkter Aufruf<br/>**EventQueue::push**`"]
PE["📋 Pseudo-Events<br/>Log-Tabelle-Poller"]
RE["🔀 Route-Events<br/>POST auf mutierende Routes"]
end
subgraph Queue["⏳ Queue"]
CE[("`📦 cj_event<br/>**DB-Warteschlange**`")]
end
subgraph Tick["`⏱ Tick *(every 60 s)*`"]
MATCH["Events matchen<br/>coalesced: 1 per name"]
BUDGET["`⏲ **60 s** Wall-Clock-Budget`"]
end
subgraph Execution["⚙ Ausführung"]
JOB["`Job-Skript<br/>child process via **proc_open**`"]
ENV["🌐 CRONJOB_EVENT<br/>CRONJOB_EVENT_PAYLOAD"]
end
subgraph Storage["💾 Speicherung"]
RUN[("cj_run<br/>Laufverlauf")]
ER[("cj_event_run<br/>Verknüpfung")]
end
WH --> Queue
DC --> Queue
PE --> Queue
RE --> Queue
CE --> Tick
MATCH --> BUDGET
BUDGET --> JOB
JOB --> ENV
JOB --> RUN
JOB --> ER
classDef sourceStyle fill:#e3f2fd,stroke:#1565C0,stroke-width:2px
classDef queueStyle fill:#fff3e0,stroke:#E65100,stroke-width:2px
classDef tickStyle fill:#e8f5e9,stroke:#2E7D32,stroke-width:2px
classDef execStyle fill:#fce4ec,stroke:#C2185B,stroke-width:2px
classDef storageStyle fill:#f5f5f5,stroke:#616161,stroke-width:1px
class WH,DC,PE,RE sourceStyle
class CE queueStyle
class MATCH,BUDGET tickStyle
class JOB,ENV execStyle
class RUN,ER storageStyle
Neben Zeitplänen kann ein Job durch ein Ereignis getriggert werden. Webtrees hat kein Ereignissystem, daher sind Ereignisse einfache DB-Zeilen (eine kleine cj_event-Queue), die der Tick einmal pro Minute leert. Ein ereignisgesteuerter Job läuft für ein gequeuedes Ereignis, dessen Namen seinen Ereignisnamen entspricht — koalesziert: pro Tick wird höchstens das neueste Ereignis pro Namen geleert (ein Lauf pro Namen pro Tick), und ältere ausstehende Duplikate des Namens werden verworfen — eine Flut identischer Events queue nie eine Flut von Job-Läufen (Job-Skripte sind per Konvention idempotent/inkrementell).
Benennungsschema (analog zu Job-Namen). Ereignisnamen sind Slugs (a-z 0-9 _ -, max. 64 Zeichen insgesamt), optional namespaced <domain>:<slug>:
| Form | Bedeutung |
|---|---|
einfacher Slug (z.B. index-dirty) | eigene Events — Webhook oder direkter EventQueue::push() |
cronjob:<slug> | eingebaute Pseudo-Events (Log-Tabelle-Poller) |
_route:<slug> | Route-getriggerte Events (siehe unten) |
<module>:<slug> | von einem Modul angekündigte Events (siehe “Events ankündigen”) |
Ein Ereignis zu queueen hat vier Quellen:
- Webhook (externe Systeme). GET den auf der Admin-Seite gezeigten Endpunkt (Abschnitt “Event webhook”):
GET /module/_cronjob_/Event?event=<name>&data=<json>mit dem Token imX-Cronjob-Token-Header.dataist ein optionales JSON-Objekt, das mit dem Ereignis gespeichert wird. Der Endpunkt erfordert das Token, ist rate-limited (20/min pro Site) und akzeptiert nur Ereignisse, während das Modul aktiviert ist. Es ist ein GET, kein POST: webtrees lehnt POSTs ohne Session-CSRF-Token ab, den ein externer Aufrufender nicht senden kann. - Direkter Aufruf (eigene Module). Aus PHP-Code in einem anderen Modul:
\Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue::push('index-dirty', ['tree' => 'X']);— dies erfordert, dass das Cronjob-Modul installiert ist; schützen Sie es mitclass_exists('Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue'). - Pseudo-Events (eingebaute Poller). Webtrees hat keinen Event-Bus, daher werden Core-Aktionen (Logins, fehlgeschlagene Logins, Logouts, Datensatz-Edits, Fehler, Suchen) durch Polling der webtrees-Log-Tabelle beobachtet. Aktivieren Sie den angebotenen
cronjob:pseudo-events-Job und das Modul liest die neuen Log-Einträge höchstens alle 5 Minuten, wobei pro Eintrag ein Event gequeued wird. - Route-Events (eingebaut, request-getriggert). Ein kuratierter Satz mutierender Editor-Routes unter
/tree/{tree}/(Record-spezifische Routes mit{xref}im Pfad, plus eine kleine Tree-Level-Allowlist) queue sein Ereignis sofort, direkt in der Anfrage, die die Änderung vorgenommen hat.
Der Tick gleicht jedes ausstehende Ereignis mit den aktivierten Jobs ab, die einen Ereignis-Trigger für diesen Namen tragen (ein Job kann mehrere Ereignis-Trigger haben oder keiner), führt sie aus und markiert dann das Ereignis als verarbeitet (einmal verbraucht — auch bei Fehler, damit ein fehlerhafter Job dasselbe Ereignis nicht ewig neu ausführt). Der Drain ist begrenzt: er nimmt das neueste Ereignis pro Namen (Koaleszierung, siehe oben) und stoppt nach einem 60-s-Wall-Clock-Budget, damit eine gesättigte Queue den Rest des Ticks nicht verhungern lässt — die verbleibenden Events setzen einfach mit dem nächsten Tick fort. Jeder verbrauchende Lauf wird zusätzlich über die cj_event_run-Kreuztabelle mit dem Ereignis verknüpft (ein Ereignis kann mehrere Jobs triggern), und die Ausführungshistorie zeigt, welches Ereignis einen Lauf ausgelöst hat. Verarbeitete Events werden nach 7 Tagen gelöscht (ihre cj_event_run-Links werden per FK-Cascade entfernt); Events ohne passenden Job werden verworfen, damit die Queue nicht wächst.
Ereignis-Payload im Kindprozess. Event-Läufe übergeben das Ereignis dem Job-Skript über die Prozessumgebung (das argv ist für die statischen, strikt validierten Argumente des Jobs reserviert):
| Variable | Gesetzt bei | Wert |
|---|---|---|
CRONJOB_EVENT | jeder Event-Lauf | der Ereignisname (dient zugleich als Marker “dies ist ein Event-Lauf”) |
CRONJOB_EVENT_PAYLOAD | Event-Lauf mit nicht-leerem Payload | der Event-Payload als JSON (Route-Parameter, Log-Tabelle-Einträge, Webhook-data) |
$event = getenv('CRONJOB_EVENT'); // false bei Zeitplan-/manuellen Läufen
$payload = json_decode((string) getenv('CRONJOB_EVENT_PAYLOAD'), true) ?: [];
$xref = $payload['xref'] ?? ''; // z.B. von einem _route:* Event
- Variable nicht vorhanden = kein Payload. Der Payload wird beim Queueen auf 4000 Bytes gekürzt; ein JSON, das mitten durchgeschnitten ist, dekodiert nicht mehr, in dem Fall ist
CRONJOB_EVENT_PAYLOADgar nicht gesetzt (nie leer, nie gebrochenes JSON). - Behandeln Sie den Payload als nicht vertrauenswürdige Eingabe — vor allem für Webhook-
data, dessen Absender extern ist. Validieren Sie Werte (XREFs, Namen, …) vor der Verwendung. - Der Payload ist unabhängig vom Befehlstyp: Modul-CLI-Skripte und Core-Befehle (die die Variablen einfach ignorieren) funktionieren beide bei Event-Läufen.
- Der Payload ist auch in der Ausführungshistorie (Ereignis-Detail) sichtbar, unabhängig vom Kindprozess.