-- PROJ: mandanten-job-queue (Phase 1+2) -- Doku-only (siehe README.md in diesem Verzeichnis) — die tatsächliche -- Ausführung passiert idempotent über -- internal/storage/processing_jobs.go initProcessingJobsSchema(), -- aufgerufen aus (*Store).initSchema() (zuletzt in der Kette, da die Tabelle -- documents(id) FK-referenziert). -- -- Mandanten-faire Verarbeitungs-Queue für die NACHGELAGERTE Dokument- -- verarbeitung (OCR-Extraktion, Taxonomie-Autozuordnung, on_upload-Workflows). -- Vorher lief das synchron im Upload-Request und erzeugte bei Batch-Scans / -- SFTP-Massenuploads Lastspitzen. -- -- * Kein Redis: die Queue ist eine ganz normale Postgres-Tabelle. Dokument- -- INSERT und Job-INSERT laufen in EINER Transaktion -- (CreateDocumentWithJob) — nie ein Dokument ohne Job, nie ein Job ohne -- Dokument. -- * WORM bleibt synchron im Request: Hash, Ablage unter -- store////. und chmod 0440 passieren VOR -- dem INSERT. Der Job liest die archivierte Datei nur und schreibt -- ausschließlich abgeleitete Metadaten. -- * Dispatch: Round-Robin über die Mandanten (je Runde ein Job pro Mandant), -- Locking über FOR UPDATE SKIP LOCKED. -- * Retry: exponentielles Backoff über next_attempt_at (2^retry_count -- Sekunden). Ab retry_count > max_retries (Default 5) bleibt der Job -- dauerhaft 'failed' — kein Automatik-Retry mehr. -- * derive_title: merkt sich, ob der Titel beim Staging nur ein Platzhalter -- war und aus dem OCR-Text ersetzt werden darf. Ein vom Benutzer bzw. aus -- dem SFTP-Dateinamen vorgegebener Titel wird nie überschrieben. CREATE TABLE IF NOT EXISTS processing_jobs ( id BIGSERIAL PRIMARY KEY, tenant_id BIGINT NOT NULL, document_id BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'queued', -- queued | processing | done | failed retry_count INT NOT NULL DEFAULT 0, derive_title BOOLEAN NOT NULL DEFAULT false, error_message TEXT, next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(), started_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); ALTER TABLE processing_jobs ADD COLUMN IF NOT EXISTS derive_title BOOLEAN NOT NULL DEFAULT false; ALTER TABLE processing_jobs ADD COLUMN IF NOT EXISTS next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(); ALTER TABLE processing_jobs ADD COLUMN IF NOT EXISTS started_at TIMESTAMPTZ; ALTER TABLE processing_jobs ADD COLUMN IF NOT EXISTS error_message TEXT; CREATE INDEX IF NOT EXISTS idx_processing_jobs_dispatch ON processing_jobs(tenant_id, status, next_attempt_at, created_at); CREATE INDEX IF NOT EXISTS idx_processing_jobs_document ON processing_jobs(document_id); CREATE INDEX IF NOT EXISTS idx_processing_jobs_status ON processing_jobs(status); -- Anzeige-/Ablaufstatus am Dokument. Default 'done': der komplette Altbestand -- wurde noch synchron verarbeitet und ist per Definition fertig — bewusst KEIN -- Backfill-Skript, der Spalten-Default deckt alle Bestandszeilen ab. ALTER TABLE documents ADD COLUMN IF NOT EXISTS processing_status TEXT NOT NULL DEFAULT 'done';