fix: back off preview on ffmpeg kill

This commit is contained in:
2026-04-18 22:01:15 +02:00
parent c29a69d381
commit e0cc8fbd50
3 changed files with 69 additions and 1 deletions

View File

@@ -256,7 +256,9 @@ func (h *Handler) HandleProxyPreviewMap(w http.ResponseWriter, r *http.Request)
Duration: duration,
})
if previewErr != nil {
log.Printf("preview map error mal_id=%d ep=%s mode=%s: %v", malID, episode, mode, previewErr)
if previewErr.Error() != "preview temporarily disabled" {
log.Printf("preview map error mal_id=%d ep=%s mode=%s: %v", malID, episode, mode, previewErr)
}
http.Error(w, "failed to generate preview map", http.StatusBadGateway)
return
}

View File

@@ -5,6 +5,7 @@ import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"math"
@@ -26,6 +27,7 @@ const (
previewFrameInterval = 10
previewFrameLimit = previewGridColumns * previewGridRows
previewGenerationLimit = 120 * time.Second
previewFailureTTL = 2 * time.Minute
previewManifestName = "map.json"
spriteFileName = "sprite.jpg"
)
@@ -67,6 +69,9 @@ func (s *Service) EnsurePreviewMap(ctx context.Context, req PreviewRequest) (Pre
previewHash := hashPreviewIdentity(req.MalID, normalizedEpisode, normalizedMode, req.Source, req.Referer)
previewKey := fmt.Sprintf("%d-%s", req.MalID, previewHash)
if s.previewFailureActive(previewKey) {
return PreviewMap{}, "", fmt.Errorf("preview temporarily disabled")
}
previewDir := filepath.Join(s.previewRoot, previewKey)
manifestPath := filepath.Join(previewDir, previewManifestName)
spritePath := filepath.Join(previewDir, spriteFileName)
@@ -120,8 +125,12 @@ func (s *Service) EnsurePreviewMap(ctx context.Context, req PreviewRequest) (Pre
interval := selectPreviewInterval(duration)
if err := generatePreviewSprite(ctxWithTimeout, ffmpegPath, req.Source, req.Referer, spritePath, interval); err != nil {
if shouldBackoffPreview(err) {
s.markPreviewFailure(previewKey)
}
return PreviewMap{}, "", err
}
s.clearPreviewFailure(previewKey)
mapData := buildPreviewMap(duration, interval)
if err := writePreviewManifest(manifestPath, mapData); err != nil {
@@ -159,6 +168,37 @@ func (s *Service) previewLock(key string) *sync.Mutex {
return newLock
}
func (s *Service) previewFailureActive(key string) bool {
s.previewFailMu.Lock()
defer s.previewFailMu.Unlock()
expiresAt, ok := s.previewFailTTL[key]
if !ok {
return false
}
if time.Now().After(expiresAt) {
delete(s.previewFailTTL, key)
return false
}
return true
}
func (s *Service) markPreviewFailure(key string) {
s.previewFailMu.Lock()
defer s.previewFailMu.Unlock()
s.previewFailTTL[key] = time.Now().Add(previewFailureTTL)
}
func (s *Service) clearPreviewFailure(key string) {
s.previewFailMu.Lock()
defer s.previewFailMu.Unlock()
delete(s.previewFailTTL, key)
}
func hashPreviewIdentity(malID int, episode string, mode string, source string, referer string) string {
payload := fmt.Sprintf("%d|%s|%s|%s|%s", malID, episode, mode, source, referer)
sum := sha1.Sum([]byte(payload))
@@ -398,3 +438,26 @@ func isFinitePositive(value float64) bool {
return true
}
func shouldBackoffPreview(err error) bool {
if err == nil {
return false
}
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return true
}
errText := strings.ToLower(err.Error())
if strings.Contains(errText, "signal: killed") {
return true
}
if strings.Contains(errText, "context canceled") {
return true
}
if strings.Contains(errText, "cannot allocate memory") {
return true
}
return false
}

View File

@@ -28,6 +28,8 @@ type Service struct {
previewRoot string
previewMu sync.Mutex
previewLocks map[string]*sync.Mutex
previewFailMu sync.Mutex
previewFailTTL map[string]time.Time
}
type sourceScore struct {
@@ -47,6 +49,7 @@ func NewService(jikanClient *jikan.Client, db database.Querier) *Service {
db: db,
previewRoot: newPreviewRootDir(),
previewLocks: make(map[string]*sync.Mutex),
previewFailTTL: make(map[string]time.Time),
}
}