Why this is needed
The worker reuses one database connection indefinitely. A failed PostgreSQL transaction is not rolled back before failure status is written, so one database error can poison the worker loop permanently.
Long scans use a fixed 60-minute stale threshold with no heartbeat, lease owner or fencing token. A second worker can reclaim a still-running scan and both can persist duplicate results. CVE enrichment runs in a Gunicorn daemon thread and can disappear on restart.
Evidence: api/models/finding.py:160-231,367-450, scanner/worker.py:40-101, and api/routes/scans.py:160-198.
Acceptance criteria
- All transactions rollback on failure and discard/reacquire broken connections.
- Claims use renewable leases with owner, expiry and fencing token.
- Heartbeat and completion updates require the current fencing token.
- Evaluation/finding persistence is idempotent using stable unique keys/upserts.
- Scan admission has per-subscription quotas, one-active-scan deduplication and idempotency keys.
- Enrichment is a durable claimed job with retries, stale recovery and complete pagination.
- PostgreSQL-backed fault-injection tests cover abort, restart, duplicate delivery, lease expiry and two-worker races.
- Metrics include worker heartbeat, oldest queue age, lease age, retry count and last successful complete scan.
Why this is needed
The worker reuses one database connection indefinitely. A failed PostgreSQL transaction is not rolled back before failure status is written, so one database error can poison the worker loop permanently.
Long scans use a fixed 60-minute stale threshold with no heartbeat, lease owner or fencing token. A second worker can reclaim a still-running scan and both can persist duplicate results. CVE enrichment runs in a Gunicorn daemon thread and can disappear on restart.
Evidence:
api/models/finding.py:160-231,367-450,scanner/worker.py:40-101, andapi/routes/scans.py:160-198.Acceptance criteria