mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: move workflow execution writes to uow
This commit is contained in:
+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