tiktak: откатить -threads 1, развести бюджеты probe и ffmpeg
build-and-push / detect (push) Successful in 11s
build-and-push / build (${{ fromJSON(needs.detect.outputs.services) }}) (push) Successful in 31s

MUMBLE "couldn't create video" -- моя регрессия из 576baa3/753d21c. В логах
у части загрузок есть Stream mapping и "Press [q]", но нет "Output #0":
ffmpeg не доработал, его снял дедлайн.

Две причины:
- "-threads 1" душил многопоточное декодирование vp9. 12-секундное 640x360
  в 2 секунды одним потоком не влезает.
- checkGeometry вызывал ffprobe ВНУТРИ того же 2-секундного контекста, что
  и кодек, отбирая у него время.

- убран -threads 1, -max_alloc оставлен (на скорость не влияет);
- probe получил свой бюджет ProbeDeadline=1s, ffmpeg -- полные
  ProcessingDeadline=2.5s. Сумма с GetDuration укладывается в 7с
  контекста handleCreate с запасом;
- семафор 16 -> 48: под флудом 16 слотов создавали очередь, и чекер
  выпадал по таймауту ещё до обработки. Бомбы теперь отсекаются дёшево,
  слоты освобождаются быстро.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 13:49:22 +03:00
co-authored by Claude Opus 5
parent 00cf1c0e55
commit e9e8f168fc
+21 -15
View File
@@ -14,21 +14,19 @@ import (
) )
var ( var (
ProcessingDeadline = 2 * time.Second // handleCreate даёт 7с на всё: probe + ffmpeg + GetDuration для субтитров.
// 75 concurrent ffmpeg processes are enough to swap the box out on their // 1.0 + 2.5 + 2.5 = 6.0, секунда про запас.
// own, bomb or no bomb. ProcessingDeadline = 2500 * time.Millisecond
semaphore = make(chan struct{}, 16) // Header-only ffprobe; cheap, но не должен отъедать бюджет у кодека.
ProbeDeadline = 1 * time.Second
semaphore = make(chan struct{}, 48)
) )
func generatePreview(ctx context.Context, inp string, out string) error { func generatePreview(ctx context.Context, inp string, out string) error {
// Reject decompression bombs before ffmpeg expands a frame into memory.
if err := checkGeometry(ctx, inp); err != nil {
return err
}
// -max_alloc caps a single ffmpeg allocation: a 1080p frame needs ~8 MB, a // -max_alloc caps a single ffmpeg allocation: a 1080p frame needs ~8 MB, a
// 16383x16383 bomb needs ~1 GB. Second line of defence behind checkGeometry, // 16383x16383 bomb needs ~1 GB. Second line of defence behind checkGeometry,
// and it works even when the probe told us nothing. // and it works even when the probe told us nothing.
cmd := exec.CommandContext(ctx, "ffmpeg", "-y", "-threads", "1", cmd := exec.CommandContext(ctx, "ffmpeg", "-y",
"-max_alloc", "134217728", "-i", inp, "-vframes", "1", out) "-max_alloc", "134217728", "-i", inp, "-vframes", "1", out)
s := strings.Builder{} s := strings.Builder{}
cmd.Stdout = &s cmd.Stdout = &s
@@ -43,12 +41,20 @@ func generatePreview(ctx context.Context, inp string, out string) error {
func GeneratePreview(ctx context.Context, inp string, out string) error { func GeneratePreview(ctx context.Context, inp string, out string) error {
select { select {
case semaphore <- struct{}{}: case semaphore <- struct{}{}:
ctx, cancel := context.WithTimeout(ctx, ProcessingDeadline) defer func() { <-semaphore }()
defer func() {
cancel() // Probe gets its OWN budget. Sharing ProcessingDeadline with ffmpeg
<-semaphore // starved the encode and killed legit uploads ("cannot create video").
}() pctx, pcancel := context.WithTimeout(ctx, ProbeDeadline)
return generatePreview(ctx, inp, out) err := checkGeometry(pctx, inp)
pcancel()
if err != nil {
return err
}
fctx, fcancel := context.WithTimeout(ctx, ProcessingDeadline)
defer fcancel()
return generatePreview(fctx, inp, out)
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
} }