Das ist eine für den Ausdruck optimierte Ansicht des gesamten Kapitels inkl. Unterseiten. Druckvorgang starten.

Zur Standardansicht zurückkehren.

Ereignisse

Ereignisgesteuerte Jobs, Webhooks, Pseudo-Events und Route-Events
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>:

FormBedeutung
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:

  1. 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 im X-Cronjob-Token-Header. data ist 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.
  2. 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 mit class_exists('Schwendinger\Webtrees\Module\Cronjob\Services\EventQueue').
  3. 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.
  4. 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):

VariableGesetzt beiWert
CRONJOB_EVENTjeder Event-Laufder Ereignisname (dient zugleich als Marker “dies ist ein Event-Lauf”)
CRONJOB_EVENT_PAYLOADEvent-Lauf mit nicht-leerem Payloadder 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_PAYLOAD gar 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.

1 - Pseudo-Events

Log-Tabelle-Polling

Webtrees hat keinen Event-Bus, daher werden Core-Aktionen (Logins, fehlgeschlagene Logins, Logouts, Datensatz-Edits, Fehler, Suchen) durch Polling der webtrees-Log-Tabelle (app/Log.php, Tabelle log) detektiert und pro neuem passenden Log-Eintrag ein Ereignis ausgelöst. Es ist standardmäßig deaktiviert: nichts läuft, bis der angebotene cronjob:pseudo-events-Job aktiviert wird.

Eingebaute Events (ein cj_event pro neuer Log-Zeile, Payload = die Zeile):

EreignisLog-Filter (log_type, Nachricht-Präfix)Feuer bei
cronjob:log-auth-failedauth, Login failedein Login-Versuch fehlgeschlagen (falsches Passwort, unbekannter Benutzer, nicht verifiziert, nicht genehmigt, keine Session-Cookies)
cronjob:log-auth-loginauth, Login:ein Benutzer eingeloggt
cronjob:log-auth-logoutauth, Logout:ein Benutzer ausgeloggt
cronjob:log-errorerror, –eine Ausnahme erfasst und geloggt (die Nachricht ist der Trace)
cronjob:log-edit-updateedit, Update:ein Datensatz aktualisiert (GedcomRecord::updateRecord())
cronjob:log-edit-deleteedit, Delete:ein Datensatz gelöscht (GedcomRecord::deleteRecord())
cronjob:log-searchsearch, –eine Suche ausgeführt (eine Log-Zeile pro durchsuchtem Baum)

Payload: log_id, log_time, log_message (auf 250 Zeichen gekürzt, UTF-8-safe) plus gedcom_id / user_id, wenn die Log-Zeile sie trägt. Die Nachrichten-Präfixe sind die hartkodierten englischen Strings des aktuellen Cores; ein Core-Update, das sie ändert, stoppt das passende Ereignis still (kein Fallback per Design) — die Event-Katalog-Beschreibungen und die test-pseudo-events-Suite fixieren die aktuelle Zuordnung.

So läuft es

  • Ein Trigger, ein Cooldown. Die Detektion läuft nur vom angebotenen cronjob:pseudo-events-Job (*/5 * * * * per Default), im Kindprozess des Ticks — isoliert von jeder Web-Anfrage und durch den Job-Timeout begrenzt, sodass ein schwerer Detektor nie eine Page-Load blockieren oder verlangsamen kann. Ein 5-Minuten-Cooldown (ein data/ mtime-Marker) plus ein Lock halten wiederholte Läufe günstig und idempotent; der Cooldown begrenzt auch einen häufigeren Job-Cron.
  • Vom Job aktiviert, pro Ereignis gated. Das Aktivieren des cronjob:pseudo-events-Jobs schaltet die Detektion ein (es gibt keinen separaten Schalter — der Tick führt nur aktivierte Jobs aus). Ein Detektor, dessen Ereignis kein aktiver ereignisgesteuerter Job abhört, wird komplett übersprungen — keine Abfrage, kein Push.
  • Checkpoint + Backpressure. Jeder Detektor erinnert sich an die höchste log_id, die er verarbeitet hat (Statusdatei). Höchstens 200 Zeilen werden pro Lauf pro Detektor ausgelöst; der Checkpoint rückt nur bis zur letzten ausgelösten Zeile vor, sodass eine Flut über mehrere Läufe abgearbeitet wird — kein Datenverlust, nur Latenz.
  • Baseline beim ersten Lauf. Die erste Detektion erfasst nur die aktuelle max log_id und löst nichts aus, sodass das Aktivieren des Jobs nicht rückwirkend auf bestehende Daten feuert. (Erneutes Abhören eines zuvor nicht abgehörten Events holt seinen Backlog nach, begrenzt wie oben.)
  • Reset, nicht neu feuern, bei Tabellenschrumpfung. Wenn die Tabelle unter einen Checkpoint schrumpfte (TRUNCATE / DB-Restore — einfache Zeilend deletions können das nicht, MySQL Auto-Increment läuft nach Deletes weiter), baselined der Detektor still neu.
  • Offline-sicher & isoliert. Übersprungen, während data/offline.txt existiert; ein kaputter Detektor bricht nie die anderen; die Detektion ist serialisiert (flock) und der Status wird atomar nach data/cronjob/pseudo-events.json persistiert.

Die Log-Tabelle ist die Quelle der Wahrheit: unter einer Flut führt die Koaleszierung des Ticks einen Job einmal pro Ereignisnamen pro Tick mit dem neuesten Payload aus — ältere Zeilen werden nicht neu zugestellt, aber sie bleiben in der Log-Tabelle, und log_id im Payload ermöglicht einem Job, der Vollständigkeit braucht, den Bereich selbst neu zu lesen.

Wenn ein Ereignisname verschwindet, markiert die Admin-Tabelle solche Trigger mit dem unknown event-Badge.

Detektierte Events gehen in dieselbe cj_event-Queue wie Webhooks und werden vom normalen Event-Drain des Ticks konsumiert. Um darauf zu reagieren, erstellen Sie einen ereignisgesteuerten Job, dessen Ereignisname passt (z.B. bei cronjob:log-auth-failed Alarm, bei cronjob:log-edit-update neu indexieren).

2 - Route-Events

request-getriggert

Ein ergänzender, sofortiger Detektionsmechanismus: anstatt Zustand nach Zeitplan zu pollen, beobachtet das Modul die HTTP-Anfrage selbst. Wenn eine Anfrage an eine abgebildete mutierende Route erfolgreich bearbeitet wurde (2xx/3xx), queue sie das passende Ereignis in dieselbe cj_event-Queue — ein abhörender Event-Job reagiert dann beim nächsten Tick (~60 s), ohne Polling-Latenz.

Welche Routes abgebildet sind — zwei Regeln, beide abgeleitet aus der live webtrees-Routentabelle. Gemeinsame Voraussetzungen: Pfad beginnt mit /tree/{tree}/, der Handler ist im webtrees-Request-Handler-Namespace (RequestHandlers in 2.2.6, Controllers in 2.3), und die Anfrage ist eine POST-Mutation. In 2.2.6 wird dies bei der Abbildung erzwungen (GET-Seiten-Routes werden nicht abgebildet); in 2.3 trägt die Route keine HTTP-Methode, daher erfolgt die POST-Prüfung zur Auslösezeit und das GET-Formular-Load derselben Route wird ignoriert.

  1. Record-Routes (regelbasiert — Core-Updates werden automatisch übernommen): der Pfad enthält {xref} (den Ziel-Datensatz) und der Handler-Klassenname beginnt mit Edit, Delete, Add, Create, Link, Paste, Reorder oder Pending. Der Ereignisname ist das dritte Pfad-Segment, namespaced _route:<segment>; wenn mehrere abgebildete Routes ein Segment teilen (delete, edit-raw, accept, reject), werden die folgenden Pfad-Parameter angehängt, Bindestrich-getrennt.
  2. Tree-Level-Routes (kleine kuratierte Allowlist): die signifikanten baumweiten Mutationen, die kein {xref} in der URL tragen (Import, Merge, Renumber, …). Jeder Allowlist-Eintrag hat einen expliziten Ereignisnamen. Neue baumweite Mutationen in einem Core-Update brauchen ein Modul-Update (die Record-Route-Regel bleibt voll automatisch).
EreignisRoute (unter /tree/{tree}/)Payload
_route:edit-note-objectPOST edit-note-object/{xref}tree, xref
_route:update-recordPOST update-record/{xref}tree, xref
_route:update-factPOST update-fact/{xref}{/fact_id}tree, xref (, fact_id)
_route:delete-xref / _route:delete-xref-fact_idPOST delete/{xref}{/fact_id}tree, xref (, fact_id)
_route:edit-raw-xref / _route:edit-raw-xref-fact_idPOST edit-raw/{xref}{/fact_id}tree, xref (, fact_id)
_route:add-child-to-family, _route:add-spouse-to-family, _route:add-media-filePOST add-…/{xref}tree, xref
_route:add-child-to-individual, _route:add-parent-to-individual, _route:add-spouse-to-individualPOST add-…/{xref}tree, xref
_route:paste-factPOST paste-fact/{xref}tree, xref
_route:link-media-to-recordPOST link-media-to-record/{xref}tree, xref — das Media-Objekt; der Ziel-Datensatz ist ein Formularfeld, nicht in der URL
_route:link-child-to-family, _route:link-spouse-to-individualPOST link-…/{xref}tree, xref
_route:reorder-children, _route:reorder-spouses, _route:reorder-media, _route:reorder-media-files, _route:reorder-namesPOST reorder-…/{xref}tree, xref
_route:accept-xref / _route:accept-xref-changePOST accept/{xref}{/change}tree, xref (, change)
_route:reject-xref / _route:reject-xref-changePOST reject/{xref}{/change}tree, xref (, change)
_route:importPOST importtree
_route:loadPOST loadtree — die chunkweise Import-Fortsetzung feuert einmal pro Chunk
_route:merge-step1 / _route:merge-step2POST merge-step1 / merge-step2tree
_route:search-replacePOST search-replacetree
_route:renumberPOST renumbertree
_route:data-fix-update / _route:data-fix-update-allPOST data-fix/{data_fix}/update[-all]tree, data_fix
_route:accept / _route:rejectPOST accept / reject (Stück)tree
_route:change-family-membersPOST change-family-memberstree — der Familien-XREF ist ein Formularfeld

Insgesamt 38 Events (27 Record-Routes + 11 Tree-Level-Routes). Der Payload enthält immer tree (ID) + tree_name plus alle erkannten Route-Parameter. Record-Routes (mit {xref}) tragen zusätzlich change_pending (bool): ob die Änderung nach dieser Anfrage noch in der change-Tabelle aussteht (die Auto-Akzept-Präferenz des Benutzers ist aus) oder bereits angewendet (Auto-Akzept an). Eine ausstehende Änderung wird angewendet, wenn ein Admin sie akzeptiert — was ihre eigenen _route:accept*-Events feuert. Tree-Level-Routes schreiben direkt (keine change-Zeilen) und tragen kein solches Flag.

Breaking Change (diese Version). Record-Routes ohne {xref} in der URL — alle _route:create-*-Events plus _route:add-unlinked-individual — werden nicht mehr abgebildet: sie tragen keinen Ziel-Datensatz. Jobs, die auf diese Events hören, feuern nie wieder; die Admin-Tabelle markiert solche Trigger mit einem unknown route event-Badge (eine generische Prüfung aller _route:*-Trigger aller Jobs gegen die live Map), sodass das Umrichten sichtbar statt still ist.

_route:accept* / _route:reject* feuern, wenn eine ausstehende Änderung akzeptiert oder abgelehnt wird — die Akzeptierung einer ausstehenden Löschung ist der Moment, in dem der Datensatz tatsächlich entfernt wird.

So funktioniert es

  • Doppelt gated, standardmäßig aus. Ein Event feuert nur, während der Admin-Schalter “Route-Events” (Events-Abschnitt) an ist und mindestens ein aktiver ereignisgesteuerter Job auf diesen Ereignisnamen hört. Eine nicht abgebildete Anfrage kostet nichts (eine In-Memory-Suche); nur eine erfolgreiche Anfrage auf einer abgebildeten Route macht zwei winzige indexierte DB-Lesevorgänge.
  • Transaktional. Der cj_event-Insert läuft in der gleichen DB-Transaktion wie die Mutation (das Modul-Middleware sitzt innerhalb der webtrees-Request-Transaktion), damit das gequeue Event mit der Änderung zusammen committet oder zurückgerollt wird — kein Drift, kein Doppel-Feuer bei fehlgeschlagenem Speichern.
  • Blockiert nie. Die Detektion läuft nach dem Handler, ist exceptions-umhüllt und kann die Antwort nie verändern.
  • Robust gegenüber Core-Upgrades. Die Map wird aus der Routentabelle neu aufgebaut (geschlüsselt nach Handler-Klassen-Strings, kein ::class), sodass ein umbenannter/entfernter Handler einfach aufhört zu matchen statt zu fatale. Wenn die Routentabelle nicht verfügbar ist (z.B. CLI), hält eine statische Fallback-Map mit der Note-Route den Piloten am Laufen. Die Record-Route-Regel ist voll automatisch; nur die kleine Tree-Level-Allowlist ist kuratiert (siehe oben), und Jobs, die auf ein nicht mehr existierendes Ereignis zeigen — ein entferntes Route-Event oder ein entferntes eingebautes Pseudo-Event — werden in der Admin-Tabelle markiert (unknown event-Badge).

Um darauf zu reagieren, erstellen Sie einen ereignisgesteuerten Job, dessen Ereignisname passt (z.B. _route:update-record) und aktivieren Sie den “Route-Events”-Schalter. Beachten Sie die Überlappung mit den cronjob:log-edit-update / cronjob:log-edit-delete-Pollern: jede Record-Edit/Delete wird auch in die Log-Tabelle geschrieben, sodass das passende Log-Event beim nächsten Poll ebenfalls folgt — wählen Sie den Mechanismus, dessen Latenz/Präzision zum Job passt (Route-Events sind quasi-sofort und pro Objekt präzise, und tragen change_pending; Log-Poller hängen ~5 min hinterher, fangen aber auch Änderungen außerhalb der Web-UI).