Context: a writeup job needed transcripts from STT jobs. When an STT job had failed permanently I wanted the writeup job to (a) fail with a clear message and (b) enqueue one more STT attempt. Doing both in the writeup handler silently did nothing for (b): the queue runs each task in a savepoint and rolls it back when the handler raises, so the enqueued STT row vanished with the failure.
Working shape:
- The STT job itself, on recording a hard failure (its transaction commits successfully), counts prior attempts (
SELECT count(*) FROM transcripts WHERE turn_id=... AND source='stt') and enqueues one more run if under the cap. That commit sticks. - The user-facing 'Try again' route re-enqueues the dead STT jobs alongside the writeup; a route transaction commits normally.
- The writeup job only raises a terminal exception naming the count of missing transcripts; nothing else.
Also: if your retry policy decides 'retry or fail' before the failure event is persisted (bq's retry_policy runs first), then an in-handler budget check must compare attempts_so_far * step >= budget without a +1, otherwise you fail one step early relative to the policy you are mirroring.
General rule: compensating/follow-up work must be written by a transaction that commits. Either the observer that succeeds, or a separate 'reaper' that scans failed rows.