fix(worker): FFmpeg 超时保护 - 防止渲染 hang 住导致 worker 永久阻塞
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 10m46s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 13m0s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 15m43s

根因:新引擎 UnifiedRenderService 所有 FFmpeg 调用通过 run_ffmpeg 执行,
但 subprocess.run 未设置 timeout,FFmpeg hang 住时 worker 线程永久阻塞。

旧引擎 compose_video 有单独的 timeout=3600,但新引擎路径没有。

修复:
1. run_ffmpeg 新增默认超时 1800s(30分钟),支持自定义传参
2. 捕获 TimeoutExpired 并打 error 日志后重新抛出
3. probe_video_info 新增 timeout=15s 超时保护
4. 7个单元测试覆盖超时逻辑

影响范围:所有通过 run_ffmpeg 调用的 FFmpeg 命令
(unified_render_service 所有渲染/混音/合并操作)
This commit is contained in:
CI Bot
2026-07-13 10:17:14 +08:00
parent f0cf4f50dc
commit ffed853f6e
2 changed files with 109 additions and 0 deletions
@@ -42,6 +42,10 @@ XFADE_TRANSITION_MAP: dict[str, str] = {
DEFAULT_TRANSITION_DURATION = 0.5
# FFmpeg 执行默认超时(秒),防止 FFmpeg hang 住导致 worker 永久阻塞
# 默认 30 分钟,足够处理大部分短视频渲染;超长视频可单独传参覆盖
DEFAULT_FFMPEG_TIMEOUT = 1800
# ── FFmpeg 执行 ───────────────────────────────────────────────────────────────
@@ -50,12 +54,14 @@ def run_ffmpeg(
command: list[str],
*,
capture_output: bool = True,
timeout: int | None = DEFAULT_FFMPEG_TIMEOUT,
) -> tuple[str, str]:
"""执行 FFmpeg 命令。
Args:
command: 完整的 ffmpeg 命令列表(含 "ffmpeg" 本身)
capture_output: 是否捕获 stdout/stderr
timeout: 超时时间(秒),默认 1800s(30分钟);None 表示不设超时(不推荐)
Returns:
(stdout, stderr) 元组
@@ -63,6 +69,7 @@ def run_ffmpeg(
Raises:
subprocess.CalledProcessError: 命令执行失败时抛出,
异常信息包含完整 stderr 以便排查。
subprocess.TimeoutExpired: 超时未完成时抛出,FFmpeg 进程会被 kill。
"""
try:
result = subprocess.run( # nosec B603
@@ -71,8 +78,16 @@ def run_ffmpeg(
stdout=subprocess.PIPE if capture_output else None,
stderr=subprocess.PIPE if capture_output else None,
text=True,
timeout=timeout,
)
return (result.stdout or "", result.stderr or "")
except subprocess.TimeoutExpired as e:
logger.error(
"FFmpeg 命令超时 (%ds): command=%s",
timeout or -1,
" ".join(str(c) for c in command[:20]),
)
raise
except subprocess.CalledProcessError as e:
# 把完整 stderr 打到日志,方便排查 exit code 183 等问题
stderr_text = (e.stderr or "").strip()
@@ -174,6 +189,7 @@ def probe_video_info(video_path: str) -> dict[str, Any]:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=15,
)
import json