diff --git a/docker-compose.yml b/docker-compose.yml index e41a8b6..17fc59e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -178,9 +178,22 @@ services: # Bounds a runaway ffmpeg transcode (large uploads, 2 workers) so it can't # OOM the single box and take down Postgres. memory: 1G - # CPU ceiling for the two image workers + ffmpeg poster extraction. Bounded below - # 2.0 so the app can never take both cores on its own. + # CPU ceiling for the WHOLE app container. Bounded below 2.0 so it can never take both + # cores on its own, which is what protects Postgres. # COMPRESSION_WORKER_CONCURRENCY=2 is the memory bound; this is the CPU one. + # + # It does NOT cap "the two image workers + ffmpeg" separately from the request path, as + # this used to claim. `compression.rs` runs that work in `tokio::task::spawn_blocking` — + # same process, same cgroup as every Axum handler — and `cpus`/`cpu_shares` are + # per-container, so nothing here can tell them apart. Concretely: `cpu.max` is + # `120000 100000`, so two CPU-pegged blocking workers exhaust the 120 ms quota after + # ~60 ms of each 100 ms period and the kernel then freezes the ENTIRE cgroup — uploads, + # feed and SSE included — for the remainder. Across a 100-photo burst (~210 s of + # draining) every request in that window can eat up to 40 ms of throttle stall. + # + # Kept anyway: an app that can take both cores starves Postgres, and every request path + # goes through Postgres. A slightly stalled request beats a starved database. If the + # backlog needs to drain faster, the knob is COMPRESSION_WORKER_CONCURRENCY, not this. cpus: '1.2' # Half the default weight, and this is the ceiling's other half: the cap alone leaves # 0.8 vCPU for db + frontend + caddy, which frontend and caddy can consume between them.