Zum Inhalt springen

AA_JOB

Status: Entwurf · Spec-Kandidat: ja

Zweck

Generische Hintergrund-Job-Queue. Erste Anwendung: Email-Versand (job_kind='email_send'). Weitere Anwendungen wachsen ohne neues Schema in dieselbe Tabelle hinein (bulk_import für OP-41, reindex, outbox_dispatch, Cleanup-Jobs).

Die Tabelle ersetzt nicht AA_OUTBOX_EVENT. Outbox = Event-Backbone für externe Konsumenten (Webhook, Broker). AA_JOB = interne, asynchrone Arbeit. Beide Pattern können sich treffen: Ein Outbox-Konsument (Email-Orchestrator) liest Outbox-Events und schreibt daraus AA_JOB-Zeilen für den konkreten Versand.

Felder

FeldTypPflichtHinweise
iduuidjaPK
tenant_iduuidjaFK → AA_TENANT. Worker-Filterung möglich, Job ist tenant-isoliert
job_kindmdm.job_kindjaemail_send, bulk_import, reindex, outbox_dispatch, cleanup_jobs, virus_scan, index_sync (OP-17), gin_backfill (OP-17); erweiterbar per Migration
statusmdm.job_statusjapendingrunningdone / failed / dlq
payloadjsonbjakind-spezifisch; muss JSON-Objekt sein
prioritysmallintjadefault 100; niedriger Wert = wichtiger (1 vor 100)
available_attimestamptzjadefault now(). Ein Worker pickt nur Jobs mit available_at <= now() (Delay/Backoff)
attemptsintegerjadefault 0, wird bei jedem Claim inkrementiert
max_attemptsintegerjadefault 8; bei attempts >= max_attemptsdlq statt pending
last_errortextneinLetzte Fehlermeldung (truncated auf z. B. 4 KB im Service)
claimed_bytextneinWorker-Identifier (<host>/<pid>/<uuid>), wird beim Claim gesetzt
claimed_attimestamptzneinZeitpunkt des Claims; Stale-Detection prüft now() - claimed_at
completed_attimestamptzneinErfolgs- oder Endgültig-Failure-Zeit
idempotency_keytextneinoptional, eindeutig je (tenant_id, job_kind, idempotency_key) für Duplikat-Schutz auf Enqueue-Seite
created_at / created_byStandardfelder

Wertebereiche

CREATE TYPE mdm.job_kind AS ENUM (
'email_send',
'bulk_import',
'reindex',
'outbox_dispatch',
'cleanup_jobs',
'virus_scan',
'index_sync', -- OP-17: Build/Drop Expression-Indizes auf searchable/sortable
'gin_backfill' -- OP-17: Catch-all-GIN für is_external_source_target=true
);
CREATE TYPE mdm.job_status AS ENUM (
'pending',
'running',
'done',
'failed',
'dlq'
);

failed ist ein transienter Zustand für Auswertungen — der Worker setzt nach einem Fehlversuch standardmäßig wieder pending mit erhöhtem available_at (Backoff). failed wird nur dann persistiert, wenn der Service den Job aus diagnostischen Gründen aktiv stoppen will, ohne ihn endgültig in den DLQ zu verschieben. dlq ist der Endzustand nach max_attempts.

Constraints

  • job_payload_is_object_chk: jsonb_typeof(payload) = 'object'
  • job_attempts_nonneg_chk: attempts >= 0 AND max_attempts >= 1
  • job_idempotency_uk: UNIQUE (tenant_id, job_kind, idempotency_key) — partiell WHERE idempotency_key IS NOT NULL

Indizes

  • ix_job_dispatch partial: (status, priority, available_at) WHERE status = 'pending' — der Hot-Path-Index für die Worker-Auswahl.
  • ix_job_claimed_stale partial: (claimed_at) WHERE status = 'running' — für Stale-Worker-Recovery (siehe Async-Jobs).
  • ix_job_tenant_kind: (tenant_id, job_kind, status) für Admin-/Reporting-Sichten.

Trigger

  • trg_job_set_updated_at (falls updated_at ergänzt wird).
  • Kein fachlicher Trigger; State-Übergänge laufen ausschließlich im Service-/Worker-Code.

Verhalten

  • Enqueue: Service-Layer schreibt INSERT … RETURNING id in derselben Transaktion wie die auslösende fachliche Mutation. Kein Job ohne Commit der Daten. Optional idempotency_key (z. B. Hash über entity_id + event_type), um Doppel-Enqueue bei Retry zu verhindern.
  • Claim (siehe Async-Jobs): Worker hebt einen Job per SELECT … FOR UPDATE SKIP LOCKED und schreibt status='running', claimed_by, claimed_at, attempts=attempts+1 in einer Mini-Transaktion.
  • Erfolg: status='done', completed_at=now(). Optionale Audit-Zeile (action='job_done' oder kind-spezifisch wie email_sent).
  • Fehler: status='pending', available_at=now() + backoff(attempts), last_error. Bei attempts >= max_attemptsstatus='dlq', completed_at=now(), Audit action='job_dlq'.
  • Stale-Claim-Recovery: Jobs mit status='running' und claimed_at < now() - <stale-ttl> werden von einem Aufräum-Worker auf pending zurückgesetzt (Worker-Crash-Schutz).

Cleanup / Retention

  • Done-Jobs werden nach Retention-Fenster (Default 14 Tage) hart gelöscht — separater Cleanup-Job job_kind='cleanup_jobs'.
  • DLQ-Einträge bleiben unbegrenzt liegen, bis Admin sie über einen Replay- oder Discard-Endpoint behandelt.
  • AA_EMAIL_LOG ist das langfristige Gedächtnis für Email-spezifische Outcomes — ein gelöschter AA_JOB referenziert per email_log.job_id ggf. ins Leere, das ist akzeptiert (ON DELETE SET NULL möglich).

Tenant-Bezug

tenant_id ist Pflicht. Worker dürfen optional auf einen Tenant-Subset eingeschränkt werden (Einsatz: dedizierte Worker für High-Volume-Tenants). Default: jeder Worker bedient alle Tenants. Cross-Tenant-Jobs sind nicht vorgesehen.

Verwandte Dokumente