Eviworx
Docs

Cascading System

Das Cascading-System propagiert Status-Änderungen automatisch entlang der ITSM-Kette Problem → Incident → Ticket. Wenn ein Problem oder Incident den Status wechselt, werden alle nachgelagerten verlinkten Entitäten aktualisiert: Activity-Logs, Notifications, SLA-Pause/Resume und WebSocket-Updates — vollständig asynchron, ohne den auslösenden Request zu blockieren.

🔗
Funktionen
✓ Transactional Outbox (eine DB-Transaktion)
✓ Asynchrone Redis-Queue (5 parallel, max. 20/s)
✓ 2-Hop-Propagation (Problem → Incident → Ticket)
✓ Kaskaden-Einträge mit Autor „System“
✓ Ticket-SLA folgt dem Incident (Resume/Pause)
✓ Idempotent (keine Duplikate bei Re-Runs)
✓ 3 Versuche mit Backoff, dann Dead-Letter
✓ Crash-Recovery beim Start

Architektur: Transactional Outbox + BullMQ

Die Status-Mutation und das Cascading-Event sind atomar gekoppelt, die Verarbeitung selbst ist davon entkoppelt. Das verhindert sowohl verlorene Kaskaden (Event committet, aber Worker stürzt ab) als auch blockierte Requests (User wartet nicht auf die Kaskade).

TRIGGER (synchronous, in one transaction)

  ProblemMutationService / IncidentStatusService
    │  BEGIN TX
    │   ├─ Change status (RESOLVED / CLOSED / …)
    │   └─ INSERT CascadingEvent (status = PENDING)   ← Outbox
    │  COMMIT
    │
    └─ cascadingQueue.enqueue(eventId)   ← after commit

PROCESSING (async, BullMQ worker in backend)

  Queue "cascading-events"  (Redis)
    │
    ▼
  processCascadingEvent(eventId)
    ├─ Claim: PENDING → PROCESSING (atomic)
    ├─ INCIDENT → linked tickets
    │   PROBLEM  → tickets + incidents + tickets-of-incidents (2-hop)
    ├─ per target: activity log (System) + SLA + notification + WebSocket
    └─ status → PROCESSED, audit CASCADING_PROCESSED

Läuft im: Backend-Container. Die Verarbeitung startet mit dem Backend, nutzt Redis als Queue und reiht beim Start offene Events neu ein.

CascadingEvent (Outbox-Tabelle)

Feld Werte Beschreibung
entityTypeINCIDENT, PROBLEMQuell-Entität der Kaskade
entityIdIDQuell-Entität
eventRESOLVED, CLOSED, MITIGATED, REOPENED, WORKAROUND_UPDATEDWas passiert ist
payloadJSONresolution, rootCause, mitigation, workaround, userId (für WebSocket-Echo-Suppression)
statusPENDINGPROCESSINGPROCESSED | FAILEDLebenszyklus
attemptsIntZähler (Dead-Letter nach 3)
error / processedAtText / DateTimeFehlertext bzw. Zeitpunkt der erfolgreichen Verarbeitung

Lebenszyklus

PENDING ──claim──▶ PROCESSING ──ok──▶ PROCESSED
   ▲                   │
   │                   └──error──▶ attempts < 3 ──▶ PENDING  (Retry mit Backoff)
   │                                attempts ≥ 3 ──▶ FAILED   (Dead-Letter + Audit)
   │
   └─ Recovery beim Start: PENDING/PROCESSING-Events älter als 10 s werden neu eingereiht

Propagations-Richtung

Quelle Ziele Verknüpfung
Incident verlinkte Tickets (1-Hop) ticketOnIncident
Problem direkt verlinkte Tickets (1-Hop) ticketOnProblem
verlinkte Incidents (1-Hop) incidentOnProblem
Tickets dieser Incidents (2-Hop) Incident → ticketOnIncident

Doppel-Vermeidung: Ein Ticket, das sowohl direkt mit dem Problem als auch über einen Incident verlinkt ist, erhält nur den direkten Eintrag — der 2-Hop-Pfad überspringt bereits direkt verlinkte Tickets.

Event-Typen & Wirkung

Event Quelle Propagiert an Besonderheit
RESOLVEDIncident / ProblemTickets (+ Incidents + 2-Hop bei Problem)SLA-Resume; Kunde wird benachrichtigt
CLOSEDIncident / ProblemTickets (+ Incidents + 2-Hop bei Problem)SLA-Resume (Incident)
REOPENEDIncident / ProblemTickets (+ Incidents + 2-Hop bei Problem)Incident-REOPENED → SLA erneut pausiert; „Resolution invalidiert"
MITIGATEDIncidentTicketsnur Activity + Notification (kein SLA-Resume)
WORKAROUND_UPDATEDProblemNUR Incidentskein Ticket-Eintrag, kein 2-Hop

Seiteneffekte pro Ziel

1. Activity-Log (System-Actor)

  • Ticket: TicketMessage type=ACTIVITY, activityData.type=CASCADING_UPDATE
  • Incident: IncidentActivity CASCADING_UPDATE
  • Autor ist „System“. Der auslösende Benutzer erscheint nicht als Autor; sein Client erhält kein doppeltes Live-Update.
  • Geschlossene Tickets (CLOSED/SPAM) und geschlossene Incidents (CLOSED) werden übersprungen.

2. SLA-Pause / Resume (Incident → Ticket)

  • RESOLVED / CLOSED → Ticket-SLA läuft weiter
  • REOPENED → Ticket-SLA wird erneut pausiert
  • Dies ist die Laufzeit-Seite des SLA-Flags pauseOnIncidentLink (SLA).

3. Notifications

  • Benachrichtigt werden Agent (IN_APP) und Kunde (E-Mail + IN_APP).
  • Kunden werden nur bei RESOLVED und REOPENED benachrichtigt.
  • Notification-Typen: LINKED_INCIDENT_{RESOLVED,CLOSED,MITIGATED,REOPENED}, LINKED_PROBLEM_{RESOLVED,CLOSED,REOPENED,WORKAROUND}.
  • Jede Notification hat einen dedupeKey (Ziel + Event + cascadingEventId) → keine Doppel-Benachrichtigung.
  • WORKAROUND_UPDATED: nur Incident-Agents, keine Ticket-Benachrichtigung.

4. WebSocket-Broadcast

  • Nach dem Speichern erhält jede betroffene Entität ein Live-Update (Kanal „activities“).
  • Echo-Suppression: der auslösende Client (userId aus payload) erhält kein redundantes Update.

Zuverlässigkeit

Mechanismus Wirkung
Transactional OutboxEvent und Status-Änderung werden gemeinsam gespeichert oder gar nicht
Atomarer ClaimPENDING→PROCESSING in einem atomaren Schritt — kein Event wird parallel doppelt verarbeitet
Activity-DedupPro Ziel wird auf cascadingEventId geprüft — Re-Runs erzeugen keine Duplikate
Job-DedupBullMQ jobId = cascading-{eventId}
Retry3 Versuche, Exponential-Backoff 1 s / 2 s / 4 s
Dead-LetterNach 3 Fehlversuchen → status=FAILED + Audit CASCADING_FAILED (ERROR)
Crash-RecoveryBeim Start: offene Events > 10 s werden neu eingereiht (PROCESSING → PENDING zurückgesetzt)
Stale-MonitoringCronJob stale_cascading_reminder meldet hängende Resolution-Ketten (Quelle resolved, Kind offen)

BullMQ-Queue-Konfiguration

Queue:          "cascading-events"  (Redis)
attempts:       3
backoff:        exponential, 1000 ms   (1s, 2s, 4s)
concurrency:    5
limiter:        max 20 / 1000 ms
removeOnComplete: { count: 500, age: 24h }
removeOnFail:     { count: 200 }

Audit

Jede Kaskade wird im Enterprise-Audit-System festgehalten (Domain ENTITY, Kategorie CASCADING, Actor SYSTEM):

  • CASCADING_PROCESSED (INFO) — erfolgreich verarbeitet, mit affectedCount
  • CASCADING_FAILED (ERROR) — nach endgültigem Fehlschlag (Dead-Letter)
Verwandte Dokumentation