Found by the adversarial review of the production enablement of audio upload (infra#520). Both are weaknesses in v0.31.0 that the production manifest cannot fix.
1. Concurrent uploads are uncapped (medium).Intake.Accept checks HasRoom only before spooling, and the per-user limit of 3 is enforced only at Submit, after the whole body has been received. A user can send N parallel POSTs of 200 MiB each: every one passes HasRoom while the queue is still empty. The production spool is an emptyDir with sizeLimit 6 Gi, whose headroom after a full queue is about 1.7 GiB (about 8 uploads); beyond it the kubelet evicts the tapir pod (tapir only; the node has free disk), every job in flight is lost and the service restarts. Not reproduced. Accepted for the first production enablement because the service is invite-only (two registered users), so it needs an authenticated user to do it deliberately.
Fix: a counting semaphore on concurrent receives (for example 4 in total and 2 per user), answered with the same 429 busy rejection, released when Accept returns. Test with a fake Submitter and N concurrent Accept calls blocked in the reader.
The same hole exists for a very slow body: Traefik's readTimeout is 3600 s, so a slow-drip upload holds one connection and one spool file for up to an hour. The semaphore bounds it too.
2. ffprobe has no deadline of its own (low). It runs under the request context, so a hostile file that makes it hang pins the process and the CPU quota until the client disconnects (up to an hour at Traefik); several in parallel would starve the 1-CPU pod. Fix: context.WithTimeout of about 30 to 60 seconds around the probe, and a test with a stub ffprobe that sleeps.
3. (info) Every tapir CI deploy restarts the pod and fails any transcription in flight (FailInterruptedUploads, ADR-033/035). Now that uploads are on, a deploy during a long job loses it. A drain on SIGTERM (finish the current job, up to the grace period) would remove it; not scheduled.
Found by the adversarial review of the production enablement of audio upload (infra#520). Both are weaknesses in v0.31.0 that the production manifest cannot fix.
**1. Concurrent uploads are uncapped (medium).** `Intake.Accept` checks `HasRoom` only before spooling, and the per-user limit of 3 is enforced only at `Submit`, after the whole body has been received. A user can send N parallel POSTs of 200 MiB each: every one passes `HasRoom` while the queue is still empty. The production spool is an `emptyDir` with `sizeLimit` 6 Gi, whose headroom after a full queue is about 1.7 GiB (about 8 uploads); beyond it the kubelet **evicts the tapir pod** (tapir only; the node has free disk), every job in flight is lost and the service restarts. Not reproduced. Accepted for the first production enablement because the service is invite-only (two registered users), so it needs an authenticated user to do it deliberately.
- Fix: a counting semaphore on concurrent receives (for example 4 in total and 2 per user), answered with the same 429 busy rejection, released when `Accept` returns. Test with a fake Submitter and N concurrent `Accept` calls blocked in the reader.
- The same hole exists for a very slow body: Traefik's `readTimeout` is 3600 s, so a slow-drip upload holds one connection and one spool file for up to an hour. The semaphore bounds it too.
**2. ffprobe has no deadline of its own (low).** It runs under the request context, so a hostile file that makes it hang pins the process and the CPU quota until the client disconnects (up to an hour at Traefik); several in parallel would starve the 1-CPU pod. Fix: `context.WithTimeout` of about 30 to 60 seconds around the probe, and a test with a stub ffprobe that sleeps.
**3. (info) Every tapir CI deploy restarts the pod and fails any transcription in flight** (`FailInterruptedUploads`, ADR-033/035). Now that uploads are on, a deploy during a long job loses it. A drain on SIGTERM (finish the current job, up to the grace period) would remove it; not scheduled.
Refs: infra#520, #46, ADR-035.
---
_Created via git-mcp on behalf of @mathias_
Sprint triage (phase 1). Item 2 (ffprobe deadline) moved to #49 (it also covers the memory finding). Item 3 (deploy kills a job) reduced to #50: the process handles SIGINT only, so SIGTERM on every deploy skips all shutdown code (exit 143); interim fix there, full drain recorded as a limitation (D2). What stays on this issue: item 1, the concurrent-upload cap against the 6Gi spool. Milestone v0.31.1, control code, adversarial review with #49. Plan: docs/sprints/audio-upload-followups-2026-10.md
Sprint triage (phase 1). Item 2 (ffprobe deadline) moved to #49 (it also covers the memory finding). Item 3 (deploy kills a job) reduced to #50: the process handles SIGINT only, so SIGTERM on every deploy skips all shutdown code (exit 143); interim fix there, full drain recorded as a limitation (D2). What stays on this issue: item 1, the concurrent-upload cap against the 6Gi spool. Milestone v0.31.1, control code, adversarial review with #49. Plan: docs/sprints/audio-upload-followups-2026-10.md
---
_Created via git-mcp on behalf of @mathias_
Item 1 shipped in v0.31.1 (e037c6b): at most 4 request bodies received at once, 2 per user, busy rejection before anything is spooled; production defaults pinned by TestNewIntake_DefaultsAllowTwoReceivesPerUserAndFourOverall (review found they were untested, fixed). Spool arithmetic (4+20+1) x 200 MiB = ~5 GiB of 6 Gi is recorded in ADR-033. Item 2 (ffprobe deadline) shipped under #49; item 3 (deploys) reduced to #50 (SIGTERM interim; full drain recorded as a limitation). Slow-drip bodies holding a slot without a time bound: #56. Closing this issue; #50 and #56 carry the rest.
Item 1 shipped in v0.31.1 (e037c6b): at most 4 request bodies received at once, 2 per user, busy rejection before anything is spooled; production defaults pinned by `TestNewIntake_DefaultsAllowTwoReceivesPerUserAndFourOverall` (review found they were untested, fixed). Spool arithmetic (4+20+1) x 200 MiB = ~5 GiB of 6 Gi is recorded in ADR-033. Item 2 (ffprobe deadline) shipped under #49; item 3 (deploys) reduced to #50 (SIGTERM interim; full drain recorded as a limitation). Slow-drip bodies holding a slot without a time bound: #56. Closing this issue; #50 and #56 carry the rest.
---
_Created via git-mcp on behalf of @mathias_
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by the adversarial review of the production enablement of audio upload (infra#520). Both are weaknesses in v0.31.0 that the production manifest cannot fix.
1. Concurrent uploads are uncapped (medium).
Intake.AcceptchecksHasRoomonly before spooling, and the per-user limit of 3 is enforced only atSubmit, after the whole body has been received. A user can send N parallel POSTs of 200 MiB each: every one passesHasRoomwhile the queue is still empty. The production spool is anemptyDirwithsizeLimit6 Gi, whose headroom after a full queue is about 1.7 GiB (about 8 uploads); beyond it the kubelet evicts the tapir pod (tapir only; the node has free disk), every job in flight is lost and the service restarts. Not reproduced. Accepted for the first production enablement because the service is invite-only (two registered users), so it needs an authenticated user to do it deliberately.Acceptreturns. Test with a fake Submitter and N concurrentAcceptcalls blocked in the reader.readTimeoutis 3600 s, so a slow-drip upload holds one connection and one spool file for up to an hour. The semaphore bounds it too.2. ffprobe has no deadline of its own (low). It runs under the request context, so a hostile file that makes it hang pins the process and the CPU quota until the client disconnects (up to an hour at Traefik); several in parallel would starve the 1-CPU pod. Fix:
context.WithTimeoutof about 30 to 60 seconds around the probe, and a test with a stub ffprobe that sleeps.3. (info) Every tapir CI deploy restarts the pod and fails any transcription in flight (
FailInterruptedUploads, ADR-033/035). Now that uploads are on, a deploy during a long job loses it. A drain on SIGTERM (finish the current job, up to the grace period) would remove it; not scheduled.Refs: infra#520, #46, ADR-035.
Created via git-mcp on behalf of @mathias
Sprint triage (phase 1). Item 2 (ffprobe deadline) moved to #49 (it also covers the memory finding). Item 3 (deploy kills a job) reduced to #50: the process handles SIGINT only, so SIGTERM on every deploy skips all shutdown code (exit 143); interim fix there, full drain recorded as a limitation (D2). What stays on this issue: item 1, the concurrent-upload cap against the 6Gi spool. Milestone v0.31.1, control code, adversarial review with #49. Plan: docs/sprints/audio-upload-followups-2026-10.md
Created via git-mcp on behalf of @mathias
Item 1 shipped in v0.31.1 (
e037c6b): at most 4 request bodies received at once, 2 per user, busy rejection before anything is spooled; production defaults pinned byTestNewIntake_DefaultsAllowTwoReceivesPerUserAndFourOverall(review found they were untested, fixed). Spool arithmetic (4+20+1) x 200 MiB = ~5 GiB of 6 Gi is recorded in ADR-033. Item 2 (ffprobe deadline) shipped under #49; item 3 (deploys) reduced to #50 (SIGTERM interim; full drain recorded as a limitation). Slow-drip bodies holding a slot without a time bound: #56. Closing this issue; #50 and #56 carry the rest.Created via git-mcp on behalf of @mathias