fix(P0): 修复取消链路断裂,前端取消后 GPU 仍继续推理 (#2009)
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 42s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 48s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m23s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (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 / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m24s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 6m51s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 2s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 4m7s
CI/CD Pipeline / Build Staging API Image (push) Successful in 4m14s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 4m28s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m47s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 4m49s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (push) Successful in 5m7s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 6m58s
CI/CD Pipeline / Unit Tests (push) Successful in 12m19s
CI/CD Pipeline / Validate - Security (push) Successful in 13m1s
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Failing after 10m9s
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #2009.
This commit is contained in:
2026-09-22 01:24:07 +08:00
committed by auto-approve-bot
parent 9927413370
commit f032152eaa
13 changed files with 457 additions and 35 deletions
+123
View File
@@ -323,3 +323,126 @@ class TestGpuServiceHelpers:
svc = GpuLipsyncService(db=fake_db)
fake_db.query.return_value.filter.return_value.first.return_value = None
assert svc.has_available_worker() is False
# ── cancel_job 取消链路 (#2009) ─────────────────────────────────────
def _build_sqlite_session():
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from packages.adapters.sqlalchemy_impl import models as _ # noqa: F401
from packages.adapters.sqlalchemy_impl.models import Base
engine = create_engine("sqlite:///:memory:", future=True)
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine, future=True)
return Session()
def _make_real_job(db, *, status="processing", mediakit_task_id="gpu:gpu-task-1"):
import uuid
from packages.adapters.sqlalchemy_impl.models import LipsyncJobModel
job = LipsyncJobModel(
id=str(uuid.uuid4()),
user_id="u1",
project_id="p1",
video_url="videos/v.mp4",
audio_url="audios/a.wav",
enable_video_loop=True,
mediakit_task_id=mediakit_task_id,
status=status,
)
db.add(job)
db.commit()
return job
def test_cancel_processing_gpu_job_marks_gpu_task_cancelled():
"""processing 的 GPU job 取消时,关联 GpuLipsyncTask 必须同步置 cancelled."""
import uuid
from packages.adapters.sqlalchemy_impl.models import GpuLipsyncTaskModel
db = _build_sqlite_session()
gpu_task_id = str(uuid.uuid4())
gpu_task = GpuLipsyncTaskModel(
id=gpu_task_id,
video_url="v",
audio_url="a",
status="processing",
worker_id="w-1",
attempt=1,
)
db.add(gpu_task)
db.commit()
job = _make_real_job(db, mediakit_task_id=f"gpu:{gpu_task_id}")
from app.services.lipsync_service import LipsyncService
svc = LipsyncService(db=db, client=MagicMock())
result = svc.cancel_job(job.id, "u1")
assert result.status == "cancelled"
db.refresh(gpu_task)
assert gpu_task.status == "cancelled"
assert gpu_task.error_msg == "用户取消"
assert gpu_task.finished_at is not None
def test_cancel_processing_gpu_job_skips_non_processing_gpu_task():
"""GPU task 已不在 processing(如已 done)时,取消 job 不应改它,也不报错."""
import uuid
from packages.adapters.sqlalchemy_impl.models import GpuLipsyncTaskModel
db = _build_sqlite_session()
gpu_task_id = str(uuid.uuid4())
gpu_task = GpuLipsyncTaskModel(
id=gpu_task_id, video_url="v", audio_url="a", status="done", worker_id="w-1", attempt=1
)
db.add(gpu_task)
db.commit()
job = _make_real_job(db, mediakit_task_id=f"gpu:{gpu_task_id}")
from app.services.lipsync_service import LipsyncService
svc = LipsyncService(db=db, client=MagicMock())
result = svc.cancel_job(job.id, "u1")
assert result.status == "cancelled"
db.refresh(gpu_task)
assert gpu_task.status == "done" # 没被动
def test_cancel_processing_non_gpu_job_does_not_touch_gpu_table():
"""mediakit_task_id 不是 gpu: 前缀(普通 MediaKit 任务)时,不查 GPU task."""
db = _build_sqlite_session()
job = _make_real_job(db, mediakit_task_id="mk-task-99")
from app.services.lipsync_service import LipsyncService
svc = LipsyncService(db=db, client=MagicMock())
result = svc.cancel_job(job.id, "u1")
assert result.status == "cancelled"
def test_cancel_completed_job_unchanged():
"""completed 状态不可取消,cancel_job 原样返回."""
db = _build_sqlite_session()
job = _make_real_job(db, status="completed", mediakit_task_id="gpu:x")
from app.services.lipsync_service import LipsyncService
svc = LipsyncService(db=db, client=MagicMock())
result = svc.cancel_job(job.id, "u1")
assert result.status == "completed"
def test_cancel_job_not_found_returns_none():
db = _build_sqlite_session()
from app.services.lipsync_service import LipsyncService
svc = LipsyncService(db=db, client=MagicMock())
assert svc.cancel_job("nonexistent", "u1") is None