fix: 兜底关联edit plan和回写config移到enqueue之前,消除竞态条件 #1482

Merged
auto-approve-bot merged 2 commits from fix/race-condition-fallback-plan-association into develop 2026-08-24 16:51:04 +08:00
2 changed files with 141 additions and 39 deletions
+43 -39
View File
@@ -358,6 +358,49 @@ def create_generation_task(
)
)
try:
# 兜底关联编辑计划:前端未传 source_edit_plan_id 时,
# 通过 template_id + user_id 在 DB 层直接查找最新的 plan。
# 必须在 enqueue 之前执行,避免 worker 读取时 source_edit_plan_id 为空(竞态条件)
if not task.source_edit_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_plan_model = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id,
EditPlanModel.created_by_user_id == user_id,
)
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _plan_model:
task.source_edit_plan_id = _plan_model.id
generation_task_repository.update(task)
logger.info(
"[生成任务] 自动关联编辑计划: task_id=%s plan_id=%s",
task.id,
_plan_model.id,
)
except Exception:
logger.warning(
"[生成任务] 查找关联编辑计划失败(不影响主流程): task_id=%s",
task.id,
exc_info=True,
)
# 回写 plan.config:必须在 enqueue 之前执行,
# 确保 worker 读取 plan 时 config 中已包含 generation_task_id。
# 只在首个任务时回写一次,避免批量生成时循环覆盖。
_effective_plan_id = task.source_edit_plan_id
if _effective_plan_id and len(created_tasks) == 0:
_writeback_edit_plan_config(
plan_id=_effective_plan_id,
task_id=task.id,
title_config=request.title_config,
db=db,
)
if safe_enqueue_generation_task(
task,
generation_task_repository,
@@ -366,45 +409,6 @@ def create_generation_task(
log_task_status=True,
):
created_tasks.append(task)
# 只在首个成功任务时回写一次 plan.config
# 避免批量生成时循环覆盖 generation_task_id
if request.source_edit_plan_id and len(created_tasks) == 1:
_writeback_edit_plan_config(
plan_id=request.source_edit_plan_id,
task_id=task.id,
title_config=request.title_config,
db=db,
)
# 兜底关联编辑计划:前端未传 source_edit_plan_id 时,
# 通过 template_id + user_id 在 DB 层直接查找最新的 plan
if not task.source_edit_plan_id and request.template_id:
try:
from packages.adapters.sqlalchemy_impl.models import EditPlanModel
_plan_model = (
db.query(EditPlanModel)
.filter(
EditPlanModel.template_id == request.template_id,
EditPlanModel.created_by_user_id == user_id,
)
.order_by(EditPlanModel.created_at.desc())
.first()
)
if _plan_model:
task.source_edit_plan_id = _plan_model.id
generation_task_repository.update(task)
logger.info(
"[生成任务] 自动关联编辑计划: task_id=%s plan_id=%s",
task.id,
_plan_model.id,
)
except Exception:
logger.warning(
"[生成任务] 查找关联编辑计划失败(不影响主流程): task_id=%s",
task.id,
exc_info=True,
)
else:
failed_tasks.append(task)
except UserPendingLimitExceeded as _e:
@@ -0,0 +1,98 @@
"""
回归测试:验证 generation_tasks.py 中兜底关联 edit plan 在 enqueue 之前执行。
根因(PR #1481 后续修复):兜底关联逻辑原来在 safe_enqueue_generation_task 之后执行,
导致 worker 在 enqueue 后立即读取 task 时,source_edit_plan_id 仍为空(竞态条件)。
"""
import ast
import textwrap
def _get_function_source(filepath, func_name):
"""提取函数源码"""
with open(filepath) as f:
source = f.read()
tree = ast.parse(source)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == func_name:
lines = source.splitlines()
start = node.lineno - 1
end = node.end_lineno
return textwrap.dedent("\n".join(lines[start:end]))
return None
def _find_try_block_source(func_source):
"""在函数源码中找到包含 safe_enqueue_generation_task 的 try 块"""
tree = ast.parse(textwrap.dedent(func_source))
for node in ast.walk(tree):
if isinstance(node, ast.Try):
# 检查 try 块中是否包含 safe_enqueue_generation_task
block_lines = func_source.splitlines()
block_text = "\n".join(block_lines[node.lineno - 1 : node.end_lineno])
if "safe_enqueue_generation_task" in block_text:
return block_text
return None
def test_fallback_before_enqueue():
"""验证兜底关联 edit plan 的代码在 safe_enqueue_generation_task 调用之前"""
filepath = "apps/api/app/api/routes/generation_tasks.py"
func_source = _get_function_source(filepath, "create_generation_task")
assert func_source is not None, "create_generation_task function not found"
try_block = _find_try_block_source(func_source)
assert try_block is not None, "try block with safe_enqueue_generation_task not found"
# 定位关键标记在 try 块中的行号
lines = try_block.splitlines()
fallback_line = None
enqueue_line = None
writeback_line = None
for i, line in enumerate(lines):
if "not task.source_edit_plan_id and request.template_id" in line and fallback_line is None:
fallback_line = i
if "safe_enqueue_generation_task(" in line and enqueue_line is None:
enqueue_line = i
if "_writeback_edit_plan_config(" in line and "def " not in line and writeback_line is None:
writeback_line = i
assert fallback_line is not None, "兜底关联逻辑 not found in try block"
assert enqueue_line is not None, "safe_enqueue_generation_task call not found in try block"
assert writeback_line is not None, "_writeback_edit_plan_config call not found in try block"
# 核心断言:兜底关联和回写都在 enqueue 之前
assert fallback_line < enqueue_line, f"兜底关联(行{fallback_line})应在 enqueue(行{enqueue_line})之前"
assert writeback_line < enqueue_line, f"回写 config(行{writeback_line})应在 enqueue(行{enqueue_line})之前"
def test_fallback_sets_source_edit_plan_id():
"""验证兜底关联逻辑会设置 task.source_edit_plan_id"""
filepath = "apps/api/app/api/routes/generation_tasks.py"
func_source = _get_function_source(filepath, "create_generation_task")
assert func_source is not None
try_block = _find_try_block_source(func_source)
assert try_block is not None
# 验证兜底逻辑包含赋值语句
assert "task.source_edit_plan_id = _plan_model.id" in try_block, "兜底关联逻辑应设置 task.source_edit_plan_id"
assert "generation_task_repository.update(task)" in try_block, "兜底关联后应持久化 task 到 DB"
def test_writeback_uses_effective_plan_id():
"""验证回写 config 使用的是 effective_plan_id(包含兜底结果),而非仅 request.source_edit_plan_id"""
filepath = "apps/api/app/api/routes/generation_tasks.py"
func_source = _get_function_source(filepath, "create_generation_task")
assert func_source is not None
try_block = _find_try_block_source(func_source)
assert try_block is not None
# 验证使用了 _effective_plan_id 或 task.source_edit_plan_id,而非仅 request.source_edit_plan_id
# 修复前用的是 request.source_edit_plan_id,修复后应该用 task.source_edit_plan_id
uses_effective = "_effective_plan_id" in try_block or "task.source_edit_plan_id" in try_block
assert uses_effective, "回写 config 应使用包含兜底结果的有效 plan_id"