refactor: move workflow execution writes to uow

This commit is contained in:
jxxghp
2026-08-22 13:21:52 +08:00
parent fa2bf1fe4d
commit e61b5168ad
17 changed files with 389 additions and 52 deletions
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
]
},
+2 -2
View File
@@ -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": []}
+10
View File
@@ -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):
"""
事件触发列表与异步访问器同样可用。
+2
View File
@@ -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={}))
+3 -1
View File
@@ -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")
+48
View File
@@ -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():
"""工作流查询服务只调用读取端口,不持有数据库会话或事务。"""