From 9a2f24b3c9e0dbec38689f9968e57822920af8c5 Mon Sep 17 00:00:00 2001 From: Mathias Date: Fri, 2 Oct 2026 17:22:41 +0200 Subject: [PATCH] fix(uploads): refuse a full queue before spooling, probing or creating a row (second review) Co-Authored-By: Claude Sonnet 5.5 --- internal/uploads/intake.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/uploads/intake.go b/internal/uploads/intake.go index c1e3664..d7afd85 100644 --- a/internal/uploads/intake.go +++ b/internal/uploads/intake.go @@ -58,12 +58,21 @@ func NewIntake(s Store, p Prober, sub Submitter, l Limits, tmpDir string) *Intak return &Intake{store: s, prober: p, sub: sub, limits: l, tmpDir: tmpDir} } +// busyMessage is what a user is told when the queue is full. +const busyMessage = "Too many recordings are waiting to be transcribed. Please try again in a few minutes." + // Accept spools src to disk (bounded by MaxBytes), probes it, validates it, records // a queued upload and submits the job. It returns the upload id. A file that fails // validation yields a *RejectError and creates NO row — rejection is synchronous // and leaves nothing behind. On success the temp file is handed to the job and the // worker deletes it; on every failure path Accept deletes it itself. func (in *Intake) Accept(ctx context.Context, userID, title string, src io.Reader) (string, error) { + // Refuse before spooling when the queue has no room, if the submitter can say so: a full + // queue otherwise cost up to the upload cap on disk, an ffprobe run and a failed row for + // every refused attempt. + if rc, ok := in.sub.(interface{ HasRoom(userID string) bool }); ok && !rc.HasRoom(userID) { + return "", &RejectError{Reason: ReasonBusy, Message: busyMessage} + } f, err := os.CreateTemp(in.tmpDir, "upload-*") if err != nil { return "", fmt.Errorf("uploads: spool: %w", err) @@ -114,7 +123,7 @@ func (in *Intake) Accept(ctx context.Context, userID, title string, src io.Reade if ferr := in.store.FailUpload(ctx, userID, id, domain.UploadErrBusy); ferr != nil { return "", fmt.Errorf("uploads: submit: %w (and failing the row: %v)", err, ferr) } - return "", &RejectError{Reason: ReasonBusy, Message: "Too many recordings are waiting to be transcribed. Please try again in a few minutes."} + return "", &RejectError{Reason: ReasonBusy, Message: busyMessage} } // A row that will never run must not sit queued: that is the stuck spinner. if ferr := in.store.FailUpload(ctx, userID, id, domain.UploadErrInterrupted); ferr != nil {