feat: move unified generation to background worker
This commit is contained in:
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import desc, select
|
||||
from sqlalchemy import desc, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.config import settings
|
||||
@@ -59,6 +59,7 @@ def _job_progress(job: GenerationJob) -> dict[str, Any]:
|
||||
|
||||
progress_map: dict[str, tuple[int, str]] = {
|
||||
"request_accepted": (5, "已接收请求"),
|
||||
"worker_started": (12, "后台任务已开始"),
|
||||
"context_prepared": (20, "上下文已准备"),
|
||||
"narrative_generated": (45, "正文已生成"),
|
||||
"story_saved": (60, "主记录已保存"),
|
||||
@@ -66,8 +67,18 @@ def _job_progress(job: GenerationJob) -> dict[str, Any]:
|
||||
"provider_call_succeeded": (72, "Provider 调用成功"),
|
||||
"provider_call_failed": (72, "Provider 调用失败,尝试恢复"),
|
||||
"cover_image_started": (75, "封面生成中"),
|
||||
"cover_image_succeeded": (88, "封面已生成"),
|
||||
"cover_image_failed": (88, "封面生成失败"),
|
||||
"storybook_images_started": (75, "绘本插图生成中"),
|
||||
"storybook_cover_image_succeeded": (82, "绘本封面已生成"),
|
||||
"storybook_cover_image_failed": (82, "绘本封面生成失败"),
|
||||
"storybook_page_image_succeeded": (86, "分页插图已生成"),
|
||||
"storybook_page_image_failed": (86, "分页插图生成失败"),
|
||||
"storybook_images_completed": (92, "绘本插图已完成"),
|
||||
"audio_started": (75, "音频生成中"),
|
||||
"audio_cache_hit": (88, "音频缓存已复用"),
|
||||
"audio_succeeded": (88, "音频已生成"),
|
||||
"audio_failed": (88, "音频生成失败"),
|
||||
"asset_retry_started": (25, "资源重试中"),
|
||||
"postprocessing_queued": (90, "后处理已排队"),
|
||||
"asset_generation_completed": (100, "资源已完成"),
|
||||
@@ -155,6 +166,10 @@ async def record_generation_event(
|
||||
) -> GenerationJobEvent:
|
||||
"""Append one event to an existing generation job."""
|
||||
|
||||
job.current_step = event_type
|
||||
if story_id is not None:
|
||||
job.story_id = story_id
|
||||
|
||||
event = GenerationJobEvent(
|
||||
job_id=job.id,
|
||||
story_id=story_id if story_id is not None else job.story_id,
|
||||
@@ -169,6 +184,42 @@ async def record_generation_event(
|
||||
return event
|
||||
|
||||
|
||||
async def claim_generation_job_for_worker(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
job_id: str,
|
||||
) -> GenerationJob | None:
|
||||
"""Claim one queued generation job for worker execution once."""
|
||||
|
||||
claim_result = await db.execute(
|
||||
update(GenerationJob)
|
||||
.where(
|
||||
GenerationJob.id == job_id,
|
||||
GenerationJob.status == "running",
|
||||
GenerationJob.current_step == "request_accepted",
|
||||
)
|
||||
.values(current_step="worker_started")
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
if not claim_result.rowcount:
|
||||
return None
|
||||
|
||||
result = await db.execute(select(GenerationJob).where(GenerationJob.id == job_id))
|
||||
job = result.scalar_one_or_none()
|
||||
if job is None:
|
||||
return None
|
||||
|
||||
await record_generation_event(
|
||||
db,
|
||||
job=job,
|
||||
event_type="worker_started",
|
||||
status="running",
|
||||
message="Generation worker started processing the accepted request.",
|
||||
)
|
||||
return job
|
||||
|
||||
|
||||
async def finish_generation_job(
|
||||
db: AsyncSession,
|
||||
*,
|
||||
|
||||
@@ -12,7 +12,7 @@ from sqlalchemy.orm import joinedload
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
from app.db.models import ChildProfile, Story, StoryUniverse
|
||||
from app.db.models import ChildProfile, GenerationJob, Story, StoryUniverse
|
||||
from app.schemas.story_schemas import (
|
||||
AchievementItem,
|
||||
FullStoryResponse,
|
||||
@@ -33,6 +33,7 @@ from app.services.audio_storage import (
|
||||
write_story_audio_cache,
|
||||
)
|
||||
from app.services.generation_jobs import (
|
||||
claim_generation_job_for_worker,
|
||||
create_generation_job,
|
||||
ensure_no_active_story_generation_job,
|
||||
finish_generation_job,
|
||||
@@ -1113,7 +1114,7 @@ async def generate_generation_service(
|
||||
user_id: str,
|
||||
db: AsyncSession,
|
||||
) -> GenerationResponse:
|
||||
"""Unified generation workflow entry point for stories and storybooks."""
|
||||
"""Queue one unified generation workflow for background execution."""
|
||||
|
||||
job = await create_generation_job(
|
||||
db,
|
||||
@@ -1124,7 +1125,65 @@ async def generate_generation_service(
|
||||
)
|
||||
|
||||
try:
|
||||
response = await _generate_generation_service_with_job(request, user_id, db, job=job)
|
||||
from app.tasks.generation_workflow import run_generation_workflow_task
|
||||
|
||||
run_generation_workflow_task.delay(job.id)
|
||||
except Exception as exc:
|
||||
await finish_generation_job(
|
||||
db,
|
||||
job=job,
|
||||
story=None,
|
||||
status="failed",
|
||||
current_step="generation_failed",
|
||||
error_message="Background generation dispatch failed.",
|
||||
message="Generation failed before the worker could start processing the job.",
|
||||
metadata={"dispatch_error": str(exc)},
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="后台生成任务派发失败,请确认 worker 可用后重试。",
|
||||
) from exc
|
||||
|
||||
return _build_queued_generation_response(request, job_id=job.id)
|
||||
|
||||
|
||||
def _build_queued_generation_response(
|
||||
request: GenerationRequest,
|
||||
*,
|
||||
job_id: str,
|
||||
) -> GenerationResponse:
|
||||
"""Build the immediate API response after a generation job is accepted."""
|
||||
|
||||
return GenerationResponse(
|
||||
id=None,
|
||||
generation_job_id=job_id,
|
||||
title="生成任务已提交",
|
||||
mode="storybook" if request.output_mode == "storybook" else "generated",
|
||||
generation_status="queued",
|
||||
text_status="generating",
|
||||
image_status="not_requested",
|
||||
audio_status="not_requested",
|
||||
last_error=None,
|
||||
retryable_assets=[],
|
||||
child_profile_id=request.child_profile_id,
|
||||
universe_id=request.universe_id,
|
||||
)
|
||||
|
||||
|
||||
async def execute_generation_job_service(
|
||||
job: GenerationJob,
|
||||
db: AsyncSession,
|
||||
) -> GenerationResponse:
|
||||
"""Execute one previously accepted generation job inside the worker."""
|
||||
|
||||
try:
|
||||
request = GenerationRequest.model_validate(job.request_payload or {})
|
||||
response = await _generate_generation_service_with_job(
|
||||
request,
|
||||
job.user_id,
|
||||
db,
|
||||
job=job,
|
||||
)
|
||||
except HTTPException as exc:
|
||||
await finish_generation_job(
|
||||
db,
|
||||
@@ -1151,6 +1210,21 @@ async def generate_generation_service(
|
||||
return response
|
||||
|
||||
|
||||
async def run_generation_job_service(
|
||||
job_id: str,
|
||||
db: AsyncSession,
|
||||
) -> GenerationJob | None:
|
||||
"""Claim and execute one generation job from the background queue."""
|
||||
|
||||
job = await claim_generation_job_for_worker(db, job_id=job_id)
|
||||
if job is None:
|
||||
logger.info("generation_job_execution_skipped", job_id=job_id)
|
||||
return None
|
||||
|
||||
await execute_generation_job_service(job, db)
|
||||
return job
|
||||
|
||||
|
||||
async def _generate_generation_service_with_job(
|
||||
request: GenerationRequest,
|
||||
user_id: str,
|
||||
|
||||
Reference in New Issue
Block a user