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
| Feld | Typ | Pflicht | Hinweise |
|---|---|---|---|
id | uuid | ja | PK |
tenant_id | uuid | ja | FK → AA_TENANT. Worker-Filterung möglich, Job ist tenant-isoliert |
job_kind | mdm.job_kind | ja | email_send, bulk_import, reindex, outbox_dispatch, cleanup_jobs, virus_scan, index_sync (OP-17), gin_backfill (OP-17); erweiterbar per Migration |
status | mdm.job_status | ja | pending → running → done / failed / dlq |
payload | jsonb | ja | kind-spezifisch; muss JSON-Objekt sein |
priority | smallint | ja | default 100; niedriger Wert = wichtiger (1 vor 100) |
available_at | timestamptz | ja | default now(). Ein Worker pickt nur Jobs mit available_at <= now() (Delay/Backoff) |
attempts | integer | ja | default 0, wird bei jedem Claim inkrementiert |
max_attempts | integer | ja | default 8; bei attempts >= max_attempts → dlq statt pending |
last_error | text | nein | Letzte Fehlermeldung (truncated auf z. B. 4 KB im Service) |
claimed_by | text | nein | Worker-Identifier (<host>/<pid>/<uuid>), wird beim Claim gesetzt |
claimed_at | timestamptz | nein | Zeitpunkt des Claims; Stale-Detection prüft now() - claimed_at |
completed_at | timestamptz | nein | Erfolgs- oder Endgültig-Failure-Zeit |
idempotency_key | text | nein | optional, eindeutig je (tenant_id, job_kind, idempotency_key) für Duplikat-Schutz auf Enqueue-Seite |
created_at / created_by | – | – | Standardfelder |
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 >= 1job_idempotency_uk:UNIQUE (tenant_id, job_kind, idempotency_key)— partiellWHERE idempotency_key IS NOT NULL
Indizes
ix_job_dispatchpartial:(status, priority, available_at)WHERE status = 'pending'— der Hot-Path-Index für die Worker-Auswahl.ix_job_claimed_stalepartial:(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(fallsupdated_atergänzt wird).- Kein fachlicher Trigger; State-Übergänge laufen ausschließlich im Service-/Worker-Code.
Verhalten
- Enqueue: Service-Layer schreibt
INSERT … RETURNING idin derselben Transaktion wie die auslösende fachliche Mutation. Kein Job ohne Commit der Daten. Optionalidempotency_key(z. B. Hash überentity_id + event_type), um Doppel-Enqueue bei Retry zu verhindern. - Claim (siehe Async-Jobs): Worker hebt einen Job per
SELECT … FOR UPDATE SKIP LOCKEDund schreibtstatus='running',claimed_by,claimed_at,attempts=attempts+1in einer Mini-Transaktion. - Erfolg:
status='done',completed_at=now(). Optionale Audit-Zeile (action='job_done'oder kind-spezifisch wieemail_sent). - Fehler:
status='pending',available_at=now() + backoff(attempts),last_error. Beiattempts >= max_attempts→status='dlq',completed_at=now(), Auditaction='job_dlq'. - Stale-Claim-Recovery: Jobs mit
status='running'undclaimed_at < now() - <stale-ttl>werden von einem Aufräum-Worker aufpendingzurü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_LOGist das langfristige Gedächtnis für Email-spezifische Outcomes — ein gelöschterAA_JOBreferenziert peremail_log.job_idggf. ins Leere, das ist akzeptiert (ON DELETE SET NULLmö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.