fix(chain): 修评审 CRITICAL+HIGH — 链网关按 session 重建/日志脱敏/resume 原子化+所有权/死导入
- CRITICAL #1:链 write/review 节点经 gateway_builder 按节点自建 session 现建网关,
usage_ledger sink 绑活 session,随节点 commit 持久化;run_chain_job 不再转发请求网关
(其 session 在 BackgroundTask 跑时已关闭,记账行被静默丢弃)。新增 get_chain_gateway_builder
缝(仿 digest builder),get_chain_gateway 退化为纯 503 凭据预检。守不变量 #1。
- HIGH #2:chain_runner 失败日志不再记 str(exc)(可能含 key/连接串/LLM 输出),改记
_classify_job_error 脱敏文案 + exc_type(设计 §5)。
- HIGH #3:resume 端点原子抢占 awaiting→running(JobRepo.claim_awaiting_to_running 条件
UPDATE),抢不到 → 409,防并发 resume 双 Command(resume) 损坏图。
- HIGH #4:resume 校验 job.project_id == project_id(JobView 新增 project_id),不匹配 → 404。
- HIGH #5:resume 返回新 ChainResumeAccepted{job_id,chain_key},去掉无意义哨兵 start/count=0。
- HIGH #6:删 nodes.py 死导入 extract_conflicts(import + __all__)。
- 测试 #7:e2e 断言链跑后 usage_ledger 有行(#1 回归守卫)+ chapter_reviews 每章一行;
新增 claim 原子抢占单测 + resume 跨项目 404 / 并发 409 端点测。
This commit is contained in:
@@ -32,7 +32,7 @@ from ww_core.domain.job_repo import SqlJobRepo
|
||||
from ww_core.domain.review_repo import SqlReviewRepo
|
||||
from ww_core.memory import assemble
|
||||
from ww_core.memory.sql_repositories import sql_memory_repos
|
||||
from ww_core.orchestrator import REVIEW_SPECS, GatewayRun
|
||||
from ww_core.orchestrator import REVIEW_SPECS
|
||||
from ww_core.orchestrator.chain import build_chain_graph, initial_chain_state
|
||||
from ww_llm_gateway import Gateway
|
||||
|
||||
@@ -54,6 +54,10 @@ JOB_KIND_CHAIN = "chain"
|
||||
# 「按 session 建 digest 网关」缝:accept 节点在自建短事务里需 light 档网关跑终稿提炼。
|
||||
DigestGatewayBuilder = Callable[[Any], Awaitable[Gateway]]
|
||||
|
||||
# 「按 session 建链网关」缝:write/review 节点在自建短事务里现建网关(ledger 绑节点 session,
|
||||
# 不变量 #1)。BackgroundTask 跑时请求 session 已关闭,故必须用节点 session 重建网关。
|
||||
GatewayChainBuilder = Callable[[Any], Awaitable[Gateway]]
|
||||
|
||||
|
||||
def build_accept_op(
|
||||
*,
|
||||
@@ -178,7 +182,7 @@ async def run_chain_job(
|
||||
chain_key: str,
|
||||
start_chapter_no: int,
|
||||
count: int,
|
||||
gateway: GatewayRun,
|
||||
chain_gateway_builder: GatewayChainBuilder,
|
||||
checkpointer_ctx: CheckpointerCtx,
|
||||
accept_op: AcceptOp,
|
||||
chapter_repo_factory: ChapterRepoFactory,
|
||||
@@ -193,6 +197,10 @@ async def run_chain_job(
|
||||
否则 resume(`graph.ainvoke(Command(resume=decisions), config)`),从 interrupt 续跑。
|
||||
`thread_id = str(job_id)`:同 job 的初始/续跑落同一检查点 thread。
|
||||
|
||||
`chain_gateway_builder`(非具体网关实例):write/review 节点在自建短事务的**新鲜** session
|
||||
上现建网关,使 usage_ledger 绑节点 session(不变量 #1)。**绝不**把请求阶段建的网关传进来——
|
||||
BackgroundTask 跑时请求 session 已关闭,其 ledger.record() 会落到死 session 上被静默丢弃。
|
||||
|
||||
checkpointer 上下文(Postgres 连接 / MemorySaver)横跨整次 invoke;本 runner 在 task
|
||||
内打开/关闭它。异常一律被吞(后台任务边界),脱敏后置 job failed,不冒泡崩进程。
|
||||
"""
|
||||
@@ -201,7 +209,7 @@ async def run_chain_job(
|
||||
await _set_running(session_factory, job_id)
|
||||
config: RunnableConfig = {"configurable": {"thread_id": str(job_id)}}
|
||||
graph = build_chain_graph(
|
||||
gateway,
|
||||
chain_gateway_builder,
|
||||
session_factory=session_factory,
|
||||
memory_repos_factory=sql_memory_repos,
|
||||
chapter_repo_factory=chapter_repo_factory,
|
||||
@@ -236,8 +244,16 @@ async def run_chain_job(
|
||||
written_count=len(result["written"]),
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 — 后台任务边界:记错误 + 置 job failed,不冒泡。
|
||||
log.error("chain_job_failed", job_id=str(job_id), request_id=request_id, error=str(exc))
|
||||
await _mark_failed(session_factory, job_id, _classify_job_error(exc), request_id)
|
||||
# 日志只记脱敏文案 + 异常类型;绝不记 str(exc)(可能含 API key/连接串/LLM 输出,设计 §5)。
|
||||
stored_error = _classify_job_error(exc)
|
||||
log.error(
|
||||
"chain_job_failed",
|
||||
job_id=str(job_id),
|
||||
request_id=request_id,
|
||||
exc_type=type(exc).__name__,
|
||||
error=stored_error,
|
||||
)
|
||||
await _mark_failed(session_factory, job_id, stored_error, request_id)
|
||||
|
||||
|
||||
async def _set_running(session_factory: SessionFactory, job_id: uuid.UUID) -> None:
|
||||
@@ -276,11 +292,12 @@ async def _mark_failed(
|
||||
await SqlJobRepo(session).fail(job_id, error)
|
||||
await session.commit()
|
||||
except Exception as exc: # noqa: BLE001 — 置失败态本身再炸只记日志,不冒泡。
|
||||
# 同 §5:不记 str(exc),只记异常类型(避免泄露内部细节)。
|
||||
log.error(
|
||||
"chain_job_fail_mark_failed",
|
||||
job_id=str(job_id),
|
||||
request_id=request_id,
|
||||
error=str(exc),
|
||||
exc_type=type(exc).__name__,
|
||||
)
|
||||
|
||||
|
||||
@@ -289,6 +306,7 @@ __all__ = [
|
||||
"AcceptOp",
|
||||
"CheckpointerCtx",
|
||||
"DigestGatewayBuilder",
|
||||
"GatewayChainBuilder",
|
||||
"build_accept_op",
|
||||
"run_chain_job",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user