mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-02 05:56:47 +08:00
refactor: move workflow execution writes to uow
This commit is contained in:
+115
-1
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
||||
import json
|
||||
from collections.abc import Awaitable
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Mapping, Optional, Protocol
|
||||
from typing import Any, Callable, Mapping, Optional, Protocol, TypeVar
|
||||
|
||||
|
||||
WORKFLOW_TRIGGER_TIMER = "timer"
|
||||
@@ -101,6 +101,117 @@ class UnitOfWork(Protocol):
|
||||
...
|
||||
|
||||
|
||||
class WorkflowExecutionRepository(Protocol):
|
||||
"""工作流执行状态写入所需的最小暂存端口。"""
|
||||
|
||||
def stage_start(self, workflow_id: int) -> bool:
|
||||
"""暂存运行中状态。"""
|
||||
...
|
||||
|
||||
def stage_success(
|
||||
self,
|
||||
workflow_id: int,
|
||||
result: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""暂存成功状态和执行次数。"""
|
||||
...
|
||||
|
||||
def stage_fail(self, workflow_id: int, result: str) -> bool:
|
||||
"""暂存失败状态和错误信息。"""
|
||||
...
|
||||
|
||||
def stage_step(
|
||||
self,
|
||||
workflow_id: int,
|
||||
action_id: str,
|
||||
context: dict[str, Any],
|
||||
execution_state: Optional[dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""暂存动作进度和执行上下文。"""
|
||||
...
|
||||
|
||||
def stage_execution_reset(
|
||||
self,
|
||||
workflow_id: int,
|
||||
reset_count: bool = False,
|
||||
) -> bool:
|
||||
"""暂存执行状态重置。"""
|
||||
...
|
||||
|
||||
|
||||
_ExecutionResult = TypeVar("_ExecutionResult")
|
||||
|
||||
|
||||
class WorkflowExecutionCommand:
|
||||
"""在一个显式 UnitOfWork 中提交单次工作流执行状态变更。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
repository: WorkflowExecutionRepository,
|
||||
unit_of_work: UnitOfWork,
|
||||
) -> None:
|
||||
"""保存工作流执行仓储和事务端口。"""
|
||||
self._repository = repository
|
||||
self._unit_of_work = unit_of_work
|
||||
|
||||
def start(self, workflow_id: int) -> bool:
|
||||
"""提交工作流运行中状态。"""
|
||||
return self._commit(lambda: self._repository.stage_start(workflow_id))
|
||||
|
||||
def success(
|
||||
self,
|
||||
workflow_id: int,
|
||||
result: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""提交工作流成功状态。"""
|
||||
return self._commit(
|
||||
lambda: self._repository.stage_success(workflow_id, result)
|
||||
)
|
||||
|
||||
def fail(self, workflow_id: int, result: str) -> bool:
|
||||
"""提交工作流失败状态。"""
|
||||
return self._commit(
|
||||
lambda: self._repository.stage_fail(workflow_id, result)
|
||||
)
|
||||
|
||||
def step(
|
||||
self,
|
||||
workflow_id: int,
|
||||
action_id: str,
|
||||
context: dict[str, Any],
|
||||
execution_state: Optional[dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""提交工作流动作进度。"""
|
||||
return self._commit(
|
||||
lambda: self._repository.stage_step(
|
||||
workflow_id,
|
||||
action_id,
|
||||
context,
|
||||
execution_state,
|
||||
)
|
||||
)
|
||||
|
||||
def reset(self, workflow_id: int, reset_count: bool = False) -> bool:
|
||||
"""提交工作流执行状态重置。"""
|
||||
return self._commit(
|
||||
lambda: self._repository.stage_execution_reset(
|
||||
workflow_id,
|
||||
reset_count,
|
||||
)
|
||||
)
|
||||
|
||||
def _commit(self, operation: Callable[[], _ExecutionResult]) -> _ExecutionResult:
|
||||
"""提交暂存操作;失败时回滚并原样传播异常。"""
|
||||
try:
|
||||
result = operation()
|
||||
self._unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
self._unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
|
||||
class WorkflowMutationCommand:
|
||||
"""协调工作流状态、定义、调度和事件注册变更。"""
|
||||
|
||||
@@ -175,6 +286,9 @@ class WorkflowMutationCommand:
|
||||
values["trigger_type"] = WORKFLOW_TRIGGER_TIMER
|
||||
|
||||
updated = self._repository.stage_update(workflow_id, values)
|
||||
if not updated:
|
||||
self._unit_of_work.rollback()
|
||||
return WorkflowMutationResult(False, "工作流不存在")
|
||||
self._commit()
|
||||
self._remove_timer(updated)
|
||||
if (
|
||||
|
||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.base import Base, get_id_column
|
||||
from app.db.decorators import db_query, db_update, async_db_query, async_db_update
|
||||
from app.db.decorators import db_query, async_db_query, async_db_update
|
||||
|
||||
|
||||
class Workflow(Base):
|
||||
@@ -130,7 +130,6 @@ class Workflow(Base):
|
||||
return result.scalars().first()
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def update_state(cls, db, wid: int, state: str):
|
||||
db.execute(update(cls).where(cls.id == wid).values(state=state))
|
||||
return True
|
||||
@@ -142,7 +141,6 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def start(cls, db, wid: int):
|
||||
db.execute(update(cls).where(cls.id == wid).values(state='R'))
|
||||
return True
|
||||
@@ -154,7 +152,6 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def fail(cls, db, wid: int, result: str):
|
||||
db.execute(update(cls).where(
|
||||
and_(cls.id == wid, cls.state != "P")
|
||||
@@ -178,7 +175,6 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def success(cls, db, wid: int, result: Optional[str] = None):
|
||||
db.execute(update(cls).where(
|
||||
and_(cls.id == wid, cls.state != "P")
|
||||
@@ -204,7 +200,6 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def reset(cls, db, wid: int, reset_count: Optional[bool] = False):
|
||||
db.execute(update(cls).where(cls.id == wid).values(
|
||||
state='W',
|
||||
@@ -230,7 +225,6 @@ class Workflow(Base):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@db_update
|
||||
def update_current_action(cls, db, wid: int, action_id: str, context: dict,
|
||||
execution_state: Optional[dict] = None):
|
||||
workflow = db.execute(select(cls).where(cls.id == wid)).scalars().first()
|
||||
|
||||
+103
-2
@@ -1,4 +1,4 @@
|
||||
from typing import List, Mapping, Tuple, Optional, Any
|
||||
from typing import List, Mapping, Tuple, Optional, Any, Protocol
|
||||
|
||||
from sqlalchemy import delete as sqlalchemy_delete
|
||||
|
||||
@@ -6,6 +6,56 @@ from app.db.base import DbOper
|
||||
from app.db.models.workflow import Workflow
|
||||
|
||||
|
||||
class WorkflowLegacyWriter(Protocol):
|
||||
"""无显式 Session 的旧 Oper 写入口所需事务服务。"""
|
||||
|
||||
def start(self, workflow_id: int) -> bool:
|
||||
"""提交工作流运行中状态。"""
|
||||
...
|
||||
|
||||
def success(
|
||||
self,
|
||||
workflow_id: int,
|
||||
result: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""提交工作流成功状态。"""
|
||||
...
|
||||
|
||||
def fail(self, workflow_id: int, result: str) -> bool:
|
||||
"""提交工作流失败状态。"""
|
||||
...
|
||||
|
||||
def step(
|
||||
self,
|
||||
workflow_id: int,
|
||||
action_id: str,
|
||||
context: dict[str, Any],
|
||||
execution_state: Optional[dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""提交工作流动作进度。"""
|
||||
...
|
||||
|
||||
def reset(self, workflow_id: int, reset_count: bool = False) -> bool:
|
||||
"""提交工作流执行状态重置。"""
|
||||
...
|
||||
|
||||
|
||||
_legacy_writer: Optional[WorkflowLegacyWriter] = None
|
||||
|
||||
|
||||
def configure_workflow_legacy_writer(writer: WorkflowLegacyWriter) -> None:
|
||||
"""由启动组合根为旧的无 Session Oper 写入口注入事务服务。"""
|
||||
global _legacy_writer
|
||||
_legacy_writer = writer
|
||||
|
||||
|
||||
def _get_workflow_legacy_writer() -> WorkflowLegacyWriter:
|
||||
"""返回已装配的兼容事务服务,避免 Oper 自行创建会话。"""
|
||||
if _legacy_writer is None:
|
||||
raise RuntimeError("工作流兼容写服务尚未配置")
|
||||
return _legacy_writer
|
||||
|
||||
|
||||
class WorkflowOper(DbOper):
|
||||
"""
|
||||
工作流管理
|
||||
@@ -132,24 +182,65 @@ class WorkflowOper(DbOper):
|
||||
"""
|
||||
启动
|
||||
"""
|
||||
if self._db is None:
|
||||
return _get_workflow_legacy_writer().start(wid)
|
||||
return self.stage_start(wid)
|
||||
|
||||
def stage_start(self, wid: int) -> bool:
|
||||
"""在调用方持有的会话中暂存运行中状态。"""
|
||||
return Workflow.start(self._db, wid)
|
||||
|
||||
def success(self, wid: int, result: Optional[str] = None) -> bool:
|
||||
"""
|
||||
成功
|
||||
"""
|
||||
if self._db is None:
|
||||
return _get_workflow_legacy_writer().success(wid, result)
|
||||
return self.stage_success(wid, result)
|
||||
|
||||
def stage_success(self, wid: int, result: Optional[str] = None) -> bool:
|
||||
"""在调用方持有的会话中暂存成功状态。"""
|
||||
return Workflow.success(self._db, wid, result)
|
||||
|
||||
def fail(self, wid: int, result: str) -> bool:
|
||||
"""
|
||||
失败
|
||||
"""
|
||||
if self._db is None:
|
||||
return _get_workflow_legacy_writer().fail(wid, result)
|
||||
return self.stage_fail(wid, result)
|
||||
|
||||
def stage_fail(self, wid: int, result: str) -> bool:
|
||||
"""在调用方持有的会话中暂存失败状态。"""
|
||||
return Workflow.fail(self._db, wid, result)
|
||||
|
||||
def step(self, wid: int, action_id: str, context: dict, execution_state: Optional[dict] = None) -> bool:
|
||||
def step(
|
||||
self,
|
||||
wid: int,
|
||||
action_id: str,
|
||||
context: dict[str, Any],
|
||||
execution_state: Optional[dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
步进
|
||||
"""
|
||||
if self._db is None:
|
||||
return _get_workflow_legacy_writer().step(
|
||||
wid,
|
||||
action_id,
|
||||
context,
|
||||
execution_state,
|
||||
)
|
||||
return self.stage_step(wid, action_id, context, execution_state)
|
||||
|
||||
def stage_step(
|
||||
self,
|
||||
wid: int,
|
||||
action_id: str,
|
||||
context: dict[str, Any],
|
||||
execution_state: Optional[dict[str, Any]] = None,
|
||||
) -> bool:
|
||||
"""在调用方持有的会话中暂存动作进度。"""
|
||||
return Workflow.update_current_action(
|
||||
self._db,
|
||||
wid,
|
||||
@@ -162,4 +253,14 @@ class WorkflowOper(DbOper):
|
||||
"""
|
||||
重置
|
||||
"""
|
||||
if self._db is None:
|
||||
return _get_workflow_legacy_writer().reset(wid, reset_count)
|
||||
return self.stage_execution_reset(wid, reset_count)
|
||||
|
||||
def stage_execution_reset(
|
||||
self,
|
||||
wid: int,
|
||||
reset_count: bool = False,
|
||||
) -> bool:
|
||||
"""在调用方持有的会话中暂存执行状态重置。"""
|
||||
return Workflow.reset(self._db, wid, reset_count=reset_count)
|
||||
|
||||
@@ -96,7 +96,7 @@ from app.db.oper.message import MessageOper
|
||||
from app.db.oper.subscribehistory import SubscribeHistoryOper
|
||||
from app.db.oper.plugindata import PluginDataOper
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.oper.workflow import WorkflowOper, configure_workflow_legacy_writer
|
||||
from app.command import CommandChain
|
||||
from app.schemas.message import Message
|
||||
from app.schemas.message import MessageType
|
||||
@@ -113,6 +113,7 @@ from app.startup.subscription import (
|
||||
)
|
||||
from app.startup.chain_events import TransactionalChainDurableEventWriter
|
||||
from app.startup.download_failure import TransactionalDownloadFailureRepository
|
||||
from app.startup.workflow import TransactionalWorkflowExecutionService
|
||||
from app.startup.context import AgentChatRuntime, HostRuntime, SubscriptionRuntime
|
||||
from app.adapters.web.security.access import set_superuser_token_payload_provider
|
||||
from app.application.security.auth import build_superuser_token_payload
|
||||
@@ -570,6 +571,8 @@ async def init_modules() -> HostRuntime:
|
||||
configure_runtime_configuration(host_runtime.configuration)
|
||||
configure_api_data_runtime(host_runtime.compatibility_api_data)
|
||||
configure_runtime_data_providers()
|
||||
workflow_execution = TransactionalWorkflowExecutionService(SessionFactory)
|
||||
configure_workflow_legacy_writer(workflow_execution)
|
||||
configure_chain_data_ports(
|
||||
site=lambda: SiteOper(),
|
||||
subscribe=lambda: SubscribeOper(),
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""工作流执行状态事务适配器。"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.workflow import WorkflowExecutionCommand
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.uow import SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
_Result = TypeVar("_Result")
|
||||
|
||||
|
||||
class TransactionalWorkflowExecutionService:
|
||||
"""为每次工作流执行状态写入创建独立短会话和 UnitOfWork。"""
|
||||
|
||||
def __init__(self, session_factory: Callable[[], Session]) -> None:
|
||||
"""保存由启动组合根提供的同步 Session 工厂。"""
|
||||
self._session_factory = session_factory
|
||||
|
||||
def start(self, workflow_id: int) -> bool:
|
||||
"""以独立事务提交运行中状态。"""
|
||||
return self._run(lambda command: command.start(workflow_id))
|
||||
|
||||
def success(self, workflow_id: int, result: str | None = None) -> bool:
|
||||
"""以独立事务提交成功状态。"""
|
||||
return self._run(lambda command: command.success(workflow_id, result))
|
||||
|
||||
def fail(self, workflow_id: int, result: str) -> bool:
|
||||
"""以独立事务提交失败状态。"""
|
||||
return self._run(lambda command: command.fail(workflow_id, result))
|
||||
|
||||
def step(
|
||||
self,
|
||||
workflow_id: int,
|
||||
action_id: str,
|
||||
context: dict[str, Any],
|
||||
execution_state: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
"""以独立事务提交动作进度。"""
|
||||
return self._run(
|
||||
lambda command: command.step(
|
||||
workflow_id,
|
||||
action_id,
|
||||
context,
|
||||
execution_state,
|
||||
)
|
||||
)
|
||||
|
||||
def reset(self, workflow_id: int, reset_count: bool = False) -> bool:
|
||||
"""以独立事务提交执行状态重置。"""
|
||||
return self._run(
|
||||
lambda command: command.reset(workflow_id, reset_count)
|
||||
)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
operation: Callable[[WorkflowExecutionCommand], _Result],
|
||||
) -> _Result:
|
||||
"""创建短会话并把提交/回滚交给 Application command。"""
|
||||
session = self._session_factory()
|
||||
try:
|
||||
command = WorkflowExecutionCommand(
|
||||
repository=WorkflowOper(db=session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
)
|
||||
return operation(command)
|
||||
finally:
|
||||
session.close()
|
||||
@@ -370,7 +370,7 @@ flowchart LR
|
||||
成功后执行。订阅新增样板由 `startup/subscription.py` 创建独占 Session,
|
||||
`application/subscription/write.py` 决定事务与 post-commit 边界,`SubscribeOper.stage_add()`
|
||||
只查重、`add` 和 `flush`。旧 SDK 显式构造的无会话 Oper 暂留兼容自动短会话,不得被新代码复用。
|
||||
`transaction-debt-baseline.json` 将存量 178 个 Model 事务装饰器冻结为只降不增低水位。
|
||||
`transaction-debt-baseline.json` 将存量 168 个 Model 事务装饰器冻结为只降不增低水位。
|
||||
- 站点、历史、工作流、Agent 会话删除和插件数据重置已经形成同构事务切片;对应 Application
|
||||
Command/Service 持有 UoW,Oper 的 `stage_*` 方法只修改当前会话。插件数据重置从
|
||||
`startup/plugins_initializer.py` 创建独占会话,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。
|
||||
|
||||
@@ -772,6 +772,10 @@ ADR 必须逐个映射当前 Event、BackgroundTasks、Scheduler job、Agent tas
|
||||
- 下载失败冷却切片继续迁移到 `TransactionalDownloadFailureRepository`:Chain 每次读写使用独立短会话,
|
||||
写成功由显式 `SqlAlchemyUnitOfWork` commit,异常 rollback;`DownloadFailure` 查询和记录方法不再拥有
|
||||
自动会话/提交装饰器。Model decorator 基线继续从 176 降到 174,Oper 内显式 commit/rollback 仍为 0。
|
||||
- Workflow 执行状态切片新增 `WorkflowExecutionCommand` 与短会话事务适配器;运行中、动作进度、成功、
|
||||
失败和重置均由 Application command 显式 commit/rollback。`WorkflowOper()` 的旧方法名、参数和返回值
|
||||
继续可用,无 Session 调用委托组合根服务,显式 Session 调用只暂存;同步 Model 自动提交装饰器移除 6 个,
|
||||
事务低水位从 174 降到 168,Oper 仍不创建 Session、也不直接 commit/rollback。
|
||||
|
||||
**禁止**:本阶段不引入 Celery、Kafka、RabbitMQ 等新基础设施。
|
||||
|
||||
@@ -886,6 +890,9 @@ OTel 初始化只能位于 Startup/Adapter;Domain/Application 只依赖 no-op-
|
||||
**扩展实施记录(2026-08-22)**:mypy 目标运行时更新到 Python 3.14,严格清单扩大到 20 个源文件;
|
||||
新增纳管配置快照和下载失败事务适配器,仍保持零错误、无全局 ignore。
|
||||
|
||||
Workflow 执行状态 UoW 切片将 `app/application/workflow.py` 与 `app/startup/workflow.py` 纳入 strict 清单,
|
||||
治理范围扩大到 22 个源文件;事务命令、仓储 Protocol 和短会话适配器保持零错误。
|
||||
|
||||
#### ARCH-271:复杂度和端点预算 ratchet
|
||||
|
||||
**目标**:阻止大方法继续增长,并让拆分对应真实阶段,而不是机械 helper 化。
|
||||
|
||||
@@ -84,7 +84,7 @@ Oper classes accept and return persistence values. Turning a `MediaInfo` or
|
||||
### Transaction ownership ratchet
|
||||
|
||||
- `tests/fixtures/architecture/transaction-debt-baseline.json` records the
|
||||
existing Model transaction decorators. The current 178 legacy decorators are
|
||||
existing Model transaction decorators. The current 168 legacy decorators are
|
||||
migration debt: they may decrease but must never increase or move to a new
|
||||
Model method.
|
||||
- New Model methods must not use `db_query`, `db_update`, `async_db_query`, or
|
||||
|
||||
@@ -17,6 +17,7 @@ files =
|
||||
app/runtime/extensions/module/contracts.py,
|
||||
app/application/outbox.py,
|
||||
app/application/configuration.py,
|
||||
app/application/workflow.py,
|
||||
app/application/chain/context.py,
|
||||
app/application/chain/durable_events.py,
|
||||
app/application/subscription/delete.py,
|
||||
@@ -25,5 +26,6 @@ files =
|
||||
app/startup/context.py,
|
||||
app/startup/chain_events.py,
|
||||
app/startup/download_failure.py,
|
||||
app/startup/workflow.py,
|
||||
app/api/context.py,
|
||||
app/api/dependencies/subscription.py
|
||||
|
||||
+6
-1
@@ -108,10 +108,15 @@ def configure_plugin_system_services():
|
||||
from app.db.oper.transferhistory import TransferHistoryOper
|
||||
from app.db.oper.transferpending import TransferPendingOper
|
||||
from app.db.oper.user import UserOper
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.oper.workflow import WorkflowOper, configure_workflow_legacy_writer
|
||||
from app.db.oper.message import MessageOper
|
||||
from app.db.oper.passkey import PassKeyOper
|
||||
from app.startup.subscription import TransactionalSubscribeWriter
|
||||
from app.startup.workflow import TransactionalWorkflowExecutionService
|
||||
|
||||
configure_workflow_legacy_writer(
|
||||
TransactionalWorkflowExecutionService(SessionFactory)
|
||||
)
|
||||
|
||||
configure_api_data_ports(
|
||||
sync_session=get_db,
|
||||
|
||||
+11
-3
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6360,
|
||||
"edge_sha256": "09c923d8b167a889e22829c320e05f8801c62400f672b1386d6524c55b064564",
|
||||
"edge_count": 6367,
|
||||
"edge_sha256": "62c413857cd12dbaf4d31e4fad4c6b6c80f7dc747d0d5a74ecb61c44b4996822",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -6120,6 +6120,7 @@
|
||||
"app.startup.modules_initializer -> app.startup.managed_resources_initializer",
|
||||
"app.startup.modules_initializer -> app.startup.outbox",
|
||||
"app.startup.modules_initializer -> app.startup.subscription",
|
||||
"app.startup.modules_initializer -> app.startup.workflow",
|
||||
"app.startup.monitor_initializer -> app.monitor",
|
||||
"app.startup.outbox -> app.application",
|
||||
"app.startup.outbox -> app.application.outbox",
|
||||
@@ -6200,6 +6201,12 @@
|
||||
"app.startup.subscription -> app.startup.outbox",
|
||||
"app.startup.transfer_initializer -> app.chain",
|
||||
"app.startup.transfer_initializer -> app.chain.transfer",
|
||||
"app.startup.workflow -> app.application",
|
||||
"app.startup.workflow -> app.application.workflow",
|
||||
"app.startup.workflow -> app.db",
|
||||
"app.startup.workflow -> app.db.oper",
|
||||
"app.startup.workflow -> app.db.oper.workflow",
|
||||
"app.startup.workflow -> app.db.uow",
|
||||
"app.startup.workflow_initializer -> app.workflow",
|
||||
"app.testing -> app.testing.stub",
|
||||
"app.testing.bootstrap -> app.application",
|
||||
@@ -6377,7 +6384,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 789,
|
||||
"module_count": 790,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -7146,6 +7153,7 @@
|
||||
"app.startup.scheduler_initializer",
|
||||
"app.startup.subscription",
|
||||
"app.startup.transfer_initializer",
|
||||
"app.startup.workflow",
|
||||
"app.startup.workflow_initializer",
|
||||
"app.testing",
|
||||
"app.testing.bootstrap",
|
||||
|
||||
+2
-32
@@ -4,9 +4,9 @@
|
||||
"async_db_query": 49,
|
||||
"async_db_update": 12,
|
||||
"db_query": 74,
|
||||
"db_update": 39
|
||||
"db_update": 33
|
||||
},
|
||||
"count": 174,
|
||||
"count": 168,
|
||||
"methods": [
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
@@ -828,11 +828,6 @@
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_update_state"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.fail"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/workflow.py",
|
||||
@@ -852,31 +847,6 @@
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.get_timer_triggered_workflows"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.reset"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.start"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.success"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.update_current_action"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.update_state"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -125,8 +125,8 @@ def test_transaction_debt_baseline_is_a_model_and_oper_ratchet() -> None:
|
||||
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert baseline["schema_version"] == 1
|
||||
assert baseline["model_decorators"]["count"] == 174
|
||||
assert sum(baseline["model_decorators"]["by_kind"].values()) == 174
|
||||
assert baseline["model_decorators"]["count"] == 168
|
||||
assert sum(baseline["model_decorators"]["by_kind"].values()) == 168
|
||||
assert baseline["model_transaction_calls"] == {"count": 0, "calls": []}
|
||||
assert baseline["model_session_factories"] == {"count": 0, "calls": []}
|
||||
assert baseline["oper_transaction_calls"] == {"count": 0, "calls": []}
|
||||
|
||||
@@ -357,6 +357,16 @@ def test_workflow_oper_exposes_lists_and_lifecycle(db):
|
||||
assert (oper.get(flow.id).state, oper.get(flow.id).run_count) == ("W", 0)
|
||||
|
||||
|
||||
def test_workflow_oper_no_session_uses_configured_uow_writer(db):
|
||||
"""旧的无 Session Oper 写入口仍可用,但事务由组合根服务持有。"""
|
||||
flow = db.add(Workflow(**_workflow_kwargs("op-wf-legacy")))
|
||||
|
||||
assert WorkflowOper().start(flow.id) is True
|
||||
|
||||
db.session.expire_all()
|
||||
assert WorkflowOper(db=db.session).get(flow.id).state == "R"
|
||||
|
||||
|
||||
def test_workflow_oper_event_list_and_async_accessors(db):
|
||||
"""
|
||||
事件触发列表与异步访问器同样可用。
|
||||
|
||||
@@ -228,6 +228,8 @@ def test_update_current_action_matches_async_twin(db):
|
||||
|
||||
for action in ("a1", "a2", "a1"):
|
||||
Workflow.update_current_action(db.session, sync_flow.id, action, {})
|
||||
# 同步 Model 方法只暂存 SQL;由测试持有的事务边界先提交,避免与异步会话争锁。
|
||||
db.session.commit()
|
||||
asyncio.run(Workflow.async_update_current_action(
|
||||
wid=async_flow.id, action_id=action, context={}))
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@ def test_mypy_gate_has_explicit_strict_scope_without_global_ignore() -> None:
|
||||
assert "app/runtime/extensions/module/contracts.py" in governed_files
|
||||
assert "app/startup/context.py" in governed_files
|
||||
assert "app/startup/download_failure.py" in governed_files
|
||||
assert "app/startup/workflow.py" in governed_files
|
||||
assert "app/application/workflow.py" in governed_files
|
||||
assert "app/api/context.py" in governed_files
|
||||
assert len(governed_files) >= 20
|
||||
assert len(governed_files) >= 22
|
||||
assert any(path.startswith("app/domain/") for path in governed_files)
|
||||
assert "ignore_errors" not in MYPY_CONFIG.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
|
||||
from app.application.workflow import (
|
||||
WorkflowDefinitionCommand,
|
||||
WorkflowExecutionCommand,
|
||||
WorkflowMutationCommand,
|
||||
WorkflowQueryService,
|
||||
)
|
||||
@@ -44,6 +45,53 @@ def _command(workflow=None, commit_error=None):
|
||||
return WorkflowMutationCommand(**dependencies), dependencies
|
||||
|
||||
|
||||
def _execution_command(commit_error=None):
|
||||
"""构造可观察的工作流执行状态事务命令。"""
|
||||
repository = Mock()
|
||||
repository.stage_start = Mock(return_value=True)
|
||||
repository.stage_success = Mock(return_value=True)
|
||||
repository.stage_fail = Mock(return_value=True)
|
||||
repository.stage_step = Mock(return_value=True)
|
||||
repository.stage_execution_reset = Mock(return_value=True)
|
||||
unit_of_work = Mock()
|
||||
unit_of_work.commit = Mock(side_effect=commit_error)
|
||||
unit_of_work.rollback = Mock()
|
||||
return WorkflowExecutionCommand(
|
||||
repository=repository,
|
||||
unit_of_work=unit_of_work,
|
||||
), repository, unit_of_work
|
||||
|
||||
|
||||
def test_execution_step_is_staged_before_unit_of_work_commit():
|
||||
"""工作流进度写入必须由应用命令暂存后统一提交。"""
|
||||
command, repository, unit_of_work = _execution_command()
|
||||
|
||||
result = command.step(7, "action-1", {"value": 1}, {"runtime": {}})
|
||||
|
||||
assert result is True
|
||||
repository.stage_step.assert_called_once_with(
|
||||
7,
|
||||
"action-1",
|
||||
{"value": 1},
|
||||
{"runtime": {}},
|
||||
)
|
||||
unit_of_work.commit.assert_called_once_with()
|
||||
unit_of_work.rollback.assert_not_called()
|
||||
|
||||
|
||||
def test_execution_commit_failure_rolls_back():
|
||||
"""执行状态提交失败时必须回滚并保留原始异常。"""
|
||||
command, repository, unit_of_work = _execution_command(
|
||||
RuntimeError("commit failed")
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="commit failed"):
|
||||
command.fail(7, "failed")
|
||||
|
||||
repository.stage_fail.assert_called_once_with(7, "failed")
|
||||
unit_of_work.rollback.assert_called_once_with()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_query_service_delegates_list_and_get_to_repository():
|
||||
"""工作流查询服务只调用读取端口,不持有数据库会话或事务。"""
|
||||
|
||||
Reference in New Issue
Block a user