Add Chinese comments for Temporal workflow flow

This commit is contained in:
Codex
2026-03-27 00:13:54 +08:00
parent cc03da8a94
commit d02fc8565f
7 changed files with 205 additions and 49 deletions

View File

@@ -1,10 +1,18 @@
"""Low-end image pipeline workflow."""
"""低端图片流水线工作流。
这个文件里的 `workflow` 只负责“编排顺序”和“等待结果”,
不直接访问数据库、不做真实 I/O。
真正的落库和 mock 执行都放在 activity 里完成。
"""
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
# Temporal workflow 代码需要尽量保持可重放deterministic
# 这里导入的模块会在 workflow 外部执行,所以用 imports_passed_through
# 明确告诉 SDK这些导入不是 workflow 重放逻辑的一部分。
with workflow.unsafe.imports_passed_through():
from app.domain.enums import OrderStatus, WorkflowStepName
from app.infra.temporal.task_queues import (
@@ -35,14 +43,27 @@ ACTIVITY_RETRY_POLICY = RetryPolicy(
@workflow.defn
class LowEndPipelineWorkflow:
"""Low-end fully automated image pipeline."""
"""低端全自动工作流。
它对应的是一条从头跑到尾、不需要人工介入的流水线:
prepare_model -> tryon -> scene -> qc -> export
"""
@workflow.run
async def run(self, payload: PipelineWorkflowInput) -> dict[str, int | str | None]:
"""Execute the low-end workflow from start to finish."""
"""执行低端工作流主流程。
可以把这里理解成“时序控制器”:
1. 按顺序调 activity
2. 把上一步产物传给下一步
3. 出错时统一标记 workflow 失败
"""
# current_step 用来在异常时把失败位置持久化到数据库。
current_step = WorkflowStepName.PREPARE_MODEL
try:
# 每个步骤都通过 execute_activity 触发真正执行。
# workflow 自己不做计算,只负责调度。
prepared = await workflow.execute_activity(
prepare_model_activity,
StepActivityInput(
@@ -60,6 +81,7 @@ class LowEndPipelineWorkflow:
)
current_step = WorkflowStepName.TRYON
# 下游步骤通过 source_asset_id 引用上一步生成的资产。
tryon_result = await workflow.execute_activity(
run_tryon_activity,
StepActivityInput(
@@ -90,6 +112,8 @@ class LowEndPipelineWorkflow:
)
current_step = WorkflowStepName.QC
# QC 是流程里的“闸门”。
# 如果这里不通过,低端流程直接失败,不会再 export。
qc_result = await workflow.execute_activity(
run_qc_activity,
StepActivityInput(
@@ -108,6 +132,8 @@ class LowEndPipelineWorkflow:
return {"order_id": payload.order_id, "status": OrderStatus.FAILED.value, "final_asset_id": None}
current_step = WorkflowStepName.EXPORT
# candidate_asset_ids 是 QC 推荐可导出的候选资产。
# 当前 MVP 只会返回一个候选;如果没有,就退回 scene 结果导出。
final_result = await workflow.execute_activity(
run_export_activity,
StepActivityInput(
@@ -126,6 +152,7 @@ class LowEndPipelineWorkflow:
"final_asset_id": final_result.asset_id,
}
except Exception as exc:
# workflow 出异常时,额外调一个 activity 把数据库状态补齐。
await self._mark_failed(payload, current_step, str(exc))
raise
@@ -135,7 +162,11 @@ class LowEndPipelineWorkflow:
current_step: WorkflowStepName,
message: str,
) -> None:
"""Persist workflow failure state."""
"""持久化失败状态。
注意这里仍然通过 activity 落库,而不是在 workflow 里直连数据库。
这样能保持 workflow 的职责单一:只编排,不做外部副作用。
"""
await workflow.execute_activity(
mark_workflow_failed_activity,
@@ -149,4 +180,3 @@ class LowEndPipelineWorkflow:
start_to_close_timeout=ACTIVITY_TIMEOUT,
retry_policy=ACTIVITY_RETRY_POLICY,
)

View File

@@ -1,10 +1,18 @@
"""Mid-end image pipeline workflow with review signal support."""
"""中端图片流水线工作流。
和低端流程相比,这里最大的区别是:
1. 会在 QC 之后停在 waiting_review
2. 通过 Temporal signal 接收人工审核结果
3. 可以按审核意见回流到 scene / face / fusion 重新跑
"""
from datetime import timedelta
from temporalio import workflow
from temporalio.common import RetryPolicy
# 这些导入属于 workflow 外部世界的对象,明确标记为 pass-through
# 避免把它们当成需要重放的 workflow 逻辑一部分。
with workflow.unsafe.imports_passed_through():
from app.domain.enums import OrderStatus, ReviewDecision, WorkflowStepName
from app.infra.temporal.task_queues import (
@@ -47,21 +55,36 @@ ACTIVITY_RETRY_POLICY = RetryPolicy(
@workflow.defn
class MidEndPipelineWorkflow:
"""Mid-end workflow that pauses for human review and supports reruns."""
"""中端半自动工作流。
这个 workflow 会经历“自动生成 -> 等待人工审核 -> 按审核意见继续”。
"""
def __init__(self) -> None:
# signal 到达后,先暂存在 workflow 内存里,
# 主流程再通过 wait_condition 继续往下走。
self._review_payload: ReviewSignalPayload | None = None
@workflow.signal
def submit_review(self, payload: ReviewSignalPayload) -> None:
"""Receive a review decision from the API layer."""
"""接收 API 层发来的审核 signal。
这一步不会直接继续执行,只是把审核结果写进 workflow 内存状态。
真正恢复主流程是在 `_wait_for_review` 里。
"""
self._review_payload = payload
@workflow.run
async def run(self, payload: PipelineWorkflowInput) -> dict[str, int | str | None]:
"""Execute the mid-end workflow with a human review loop."""
"""执行中端工作流主流程。
主线是:
prepare_model -> tryon -> scene -> texture -> face -> fusion -> qc
-> waiting_review -> approve/export 或 rerun
"""
# current_step 用于失败时记录“最后跑到哪一步”。
current_step = WorkflowStepName.PREPARE_MODEL
try:
prepared = await workflow.execute_activity(
@@ -113,8 +136,14 @@ class MidEndPipelineWorkflow:
await self._mark_failed(payload, current_step, qc_result.message)
return {"order_id": payload.order_id, "status": OrderStatus.FAILED.value, "final_asset_id": None}
# 中端流程会一直循环到:
# 1. 审核 approve 然后 export 成功
# 2. 审核 reject 直接结束
# 3. rerun 后再次回到 waiting_review继续等下一次人工输入
while True:
current_step = WorkflowStepName.REVIEW
# 这里通过 activity 把数据库里的订单状态更新成 waiting_review
# 同时创建 review_task供 API 查询待审核列表。
await workflow.execute_activity(
mark_waiting_for_review_activity,
ReviewWaitActivityInput(
@@ -127,7 +156,11 @@ class MidEndPipelineWorkflow:
retry_policy=ACTIVITY_RETRY_POLICY,
)
# workflow 在这里“停住”,直到外部 signal 进来。
review_payload = await self._wait_for_review()
# signal 到达后,先把 review 这一步的等待态收口成已处理,
# 这样数据库里的 review_step / review_task 状态是完整的。
await workflow.execute_activity(
complete_review_wait_activity,
ReviewResolutionActivityInput(
@@ -145,6 +178,8 @@ class MidEndPipelineWorkflow:
if review_payload.decision == ReviewDecision.APPROVE:
current_step = WorkflowStepName.EXPORT
# 如果审核人显式选了资产,就导出该资产;
# 否则默认导出 QC 候选资产。
export_source_id = review_payload.selected_asset_id
if export_source_id is None:
export_source_id = (qc_result.candidate_asset_ids or [fusion_result.asset_id])[0]
@@ -167,8 +202,11 @@ class MidEndPipelineWorkflow:
}
if review_payload.decision == ReviewDecision.REJECT:
# reject 不再重跑,直接结束。
return {"order_id": payload.order_id, "status": OrderStatus.FAILED.value, "final_asset_id": None}
# rerun 的核心思想是:
# 把指定节点后的链路重新跑一遍,然后再次进入 QC 和 waiting_review。
if review_payload.decision == ReviewDecision.RERUN_SCENE:
current_step = WorkflowStepName.SCENE
scene_result = await self._run_scene(payload, tryon_result.asset_id)
@@ -197,7 +235,11 @@ class MidEndPipelineWorkflow:
raise
async def _wait_for_review(self) -> ReviewSignalPayload:
"""Suspend the workflow until a review signal arrives."""
"""等待人工审核 signal。
`workflow.wait_condition` 是 Temporal 里很常见的等待方式:
workflow 会被安全地挂起,不会像普通 while + sleep 那样空转占资源。
"""
if self._review_payload is None:
await workflow.wait_condition(lambda: self._review_payload is not None)
@@ -207,7 +249,10 @@ class MidEndPipelineWorkflow:
return review_payload
async def _run_scene(self, payload: PipelineWorkflowInput, source_asset_id: int | None) -> MockActivityResult:
"""Execute the scene activity."""
"""执行 scene activity
抽成私有方法后rerun_scene 时可以直接复用,不需要复制整段 activity 调用代码。
"""
return await workflow.execute_activity(
run_scene_activity,
@@ -224,7 +269,7 @@ class MidEndPipelineWorkflow:
)
async def _run_texture(self, payload: PipelineWorkflowInput, source_asset_id: int | None) -> MockActivityResult:
"""Execute the texture activity."""
"""执行 texture activity"""
return await workflow.execute_activity(
run_texture_activity,
@@ -240,7 +285,7 @@ class MidEndPipelineWorkflow:
)
async def _run_face(self, payload: PipelineWorkflowInput, source_asset_id: int | None) -> MockActivityResult:
"""Execute the face activity."""
"""执行 face activity"""
return await workflow.execute_activity(
run_face_activity,
@@ -261,7 +306,7 @@ class MidEndPipelineWorkflow:
source_asset_id: int | None,
face_asset_id: int | None,
) -> MockActivityResult:
"""Execute the fusion activity."""
"""执行 fusion activity"""
return await workflow.execute_activity(
run_fusion_activity,
@@ -278,7 +323,7 @@ class MidEndPipelineWorkflow:
)
async def _run_qc(self, payload: PipelineWorkflowInput, source_asset_id: int | None) -> MockActivityResult:
"""Execute the QC activity."""
"""执行 QC activity"""
return await workflow.execute_activity(
run_qc_activity,
@@ -299,7 +344,7 @@ class MidEndPipelineWorkflow:
current_step: WorkflowStepName,
message: str,
) -> None:
"""Persist workflow failure state."""
"""持久化 workflow 失败状态。"""
await workflow.execute_activity(
mark_workflow_failed_activity,

View File

@@ -1,4 +1,8 @@
"""Shared workflow and activity payload types."""
"""workflow / activity 之间共享的数据类型。
这些 dataclass 是 Temporal 编排层最关键的“消息格式”:
workflow 把它们发给 activityactivity 再把结果返回给 workflow。
"""
from dataclasses import dataclass, field
from enum import Enum
@@ -8,7 +12,11 @@ from app.domain.enums import CustomerLevel, OrderStatus, ReviewDecision, Service
def _coerce_enum(value: Any, enum_cls: type[Enum]) -> Any:
"""Coerce raw Temporal payload values back into enum instances."""
"""把 Temporal 反序列化后的原始值转回枚举。
在某些序列化场景下,枚举值可能先变成字符串,甚至被拆成字符列表。
这里统一做一次归一化,避免后面写数据库时类型不对。
"""
if value is None or isinstance(value, enum_cls):
return value
@@ -19,7 +27,10 @@ def _coerce_enum(value: Any, enum_cls: type[Enum]) -> Any:
@dataclass(slots=True)
class PipelineWorkflowInput:
"""Temporal workflow input for an image pipeline order."""
"""工作流启动输入。
这是一张订单进入 workflow 时携带的最小上下文。
"""
order_id: int
workflow_run_id: int
@@ -31,7 +42,7 @@ class PipelineWorkflowInput:
scene_ref_asset_id: int
def __post_init__(self) -> None:
"""Normalize enum-like values after Temporal deserialization."""
"""在反序列化后把枚举字段修正回来。"""
self.customer_level = _coerce_enum(self.customer_level, CustomerLevel)
self.service_mode = _coerce_enum(self.service_mode, ServiceMode)
@@ -39,7 +50,14 @@ class PipelineWorkflowInput:
@dataclass(slots=True)
class StepActivityInput:
"""Input payload shared by the mock pipeline activities."""
"""通用 activity 输入。
大多数图片处理步骤都只需要:
- 当前订单
- 当前 workflow_run
- 这一步叫什么
- 上一步产出的 asset_id
"""
order_id: int
workflow_run_id: int
@@ -53,14 +71,14 @@ class StepActivityInput:
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
"""Normalize enum-like values after Temporal deserialization."""
"""在反序列化后把枚举字段修正回来。"""
self.step_name = _coerce_enum(self.step_name, WorkflowStepName)
@dataclass(slots=True)
class MockActivityResult:
"""Common mock activity result structure."""
"""通用 mock activity 返回结构。"""
step_name: WorkflowStepName
success: bool
@@ -73,14 +91,14 @@ class MockActivityResult:
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
"""Normalize enum-like values after Temporal deserialization."""
"""在反序列化后把枚举字段修正回来。"""
self.step_name = _coerce_enum(self.step_name, WorkflowStepName)
@dataclass(slots=True)
class ReviewSignalPayload:
"""Signal payload sent from the API to the mid-end workflow."""
"""API 发给中端 workflow 的审核 signal 载荷。"""
decision: ReviewDecision
reviewer_id: int
@@ -88,14 +106,14 @@ class ReviewSignalPayload:
comment: str | None = None
def __post_init__(self) -> None:
"""Normalize enum-like values after Temporal deserialization."""
"""在反序列化后把枚举字段修正回来。"""
self.decision = _coerce_enum(self.decision, ReviewDecision)
@dataclass(slots=True)
class ReviewWaitActivityInput:
"""Input for marking a workflow as waiting for review."""
"""把流程切到 waiting_review 时传给 activity 的输入。"""
order_id: int
workflow_run_id: int
@@ -105,7 +123,7 @@ class ReviewWaitActivityInput:
@dataclass(slots=True)
class ReviewResolutionActivityInput:
"""Input for completing a waiting review state."""
"""审核结果到达后,用于结束 waiting_review 的输入。"""
order_id: int
workflow_run_id: int
@@ -115,14 +133,14 @@ class ReviewResolutionActivityInput:
comment: str | None = None
def __post_init__(self) -> None:
"""Normalize enum-like values after Temporal deserialization."""
"""在反序列化后把枚举字段修正回来。"""
self.decision = _coerce_enum(self.decision, ReviewDecision)
@dataclass(slots=True)
class WorkflowFailureActivityInput:
"""Input for marking a workflow as failed."""
"""流程失败收尾 activity 的输入。"""
order_id: int
workflow_run_id: int
@@ -131,7 +149,7 @@ class WorkflowFailureActivityInput:
status: OrderStatus = OrderStatus.FAILED
def __post_init__(self) -> None:
"""Normalize enum-like values after Temporal deserialization."""
"""在反序列化后把枚举字段修正回来。"""
self.current_step = _coerce_enum(self.current_step, WorkflowStepName)
self.status = _coerce_enum(self.status, OrderStatus)