docs: state what the save timeout and a racing delete actually guarantee (#51, #37)
CI / Lint / Test / Vet (pull_request) Successful in 29s
CI / Build & Import (pull_request) Skipped
CI / Deploy via GitOps (pull_request) Skipped

The tracker comment said 'never a stalled job'; each transition can wait up to the timeout.
The delete refusal is judged at the moment of the attempt. Found by the PR #58 review.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 21:32:14 +02:00
co-authored by Claude Sonnet 5.5
parent fc1e8e7ba8
commit c24573bd9f
2 changed files with 8 additions and 3 deletions
+3 -1
View File
@@ -1556,7 +1556,9 @@ deferred. The scope cut — chat first, no rich analysis, no video — is what k
mechanism), confirm-then-delete on the recording's page. A recording that is **queued or mechanism), confirm-then-delete on the recording's page. A recording that is **queued or
transcribing is refused (409) until it ends**, because the worker would otherwise write to a transcribing is refused (409) until it ends**, because the worker would otherwise write to a
row that is gone; a job whose service was lost stays undeletable until the next start's sweep row that is gone; a job whose service was lost stays undeletable until the next start's sweep
marks it failed. The home list now links to `/uploads` when uploads are mounted. marks it failed. The refusal reflects the row's state when the delete was attempted: a
recording that finishes in the same instant still gets a 409 once, and a retry deletes it.
The home list now links to `/uploads` when uploads are mounted.
**Reversibility.** Additive: one table (down-migration drops it), one adapter, one upload **Reversibility.** Additive: one table (down-migration drops it), one adapter, one upload
surface, one image change. Removing the upload route removes the feature; `transcripts`, `videos` surface, one image change. Removing the upload route removes the feature; `transcripts`, `videos`
+5 -2
View File
@@ -57,8 +57,11 @@ func WithClock(now func() time.Time) Option { return func(t *Tracker) { t.now =
func WithMinSave(d time.Duration) Option { return func(t *Tracker) { t.minSave = d } } func WithMinSave(d time.Duration) Option { return func(t *Tracker) { t.minSave = d } }
// WithSaveTimeout bounds one save or heartbeat write. Progress is cosmetic, so a database // WithSaveTimeout bounds one save or heartbeat write. Progress is cosmetic, so a database
// that stops answering costs a dropped write, never a stalled job: saves run under the // that stops answering costs a dropped write rather than an unbounded stall. It is still a
// tracker's lock, which Begin and Advance (called by the engines) need (#51). // delay: saves run under the tracker's lock, which Begin and Advance (called by the engines)
// need, so each transition can wait up to this long, and a hung database slows a job by
// that much per call, out of the job's own time budget (#51). A dropped final save is not
// retried; the row keeps its last saved step.
func WithSaveTimeout(d time.Duration) Option { return func(t *Tracker) { t.saveTimeout = d } } func WithSaveTimeout(d time.Duration) Option { return func(t *Tracker) { t.saveTimeout = d } }
// Tracker implements Reporter for one job. // Tracker implements Reporter for one job.