mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 00:16:57 +08:00
refactor: reorganize startup persistence boundaries
This commit is contained in:
@@ -1,152 +0,0 @@
|
||||
"""Chain durable 事件写入端口的 SQLAlchemy 启动适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.chain.durable_events import (
|
||||
ChainDurableEventWriter,
|
||||
TransferHistoryRef,
|
||||
download_added_event_key,
|
||||
snapshot_download_added,
|
||||
snapshot_transfer_result,
|
||||
transfer_result_event_key,
|
||||
)
|
||||
from app.application.history import TransferHistoryRecord, TransferHistoryWriter
|
||||
from app.application.outbox import DurableEventCommand, OutboxIntent
|
||||
from app.db.oper.downloadhistory import DownloadHistoryOper
|
||||
from app.db.oper.transferhistory import TransferHistoryOper
|
||||
from app.db.uow import SqlAlchemyUnitOfWork
|
||||
from app.startup.outbox import SqlAlchemyOutboxRepository
|
||||
|
||||
|
||||
class _StagingTransferHistoryWriter:
|
||||
"""让既有历史字段映射复用无提交的 replace 适配器。"""
|
||||
|
||||
def __init__(self, repository: TransferHistoryOper) -> None:
|
||||
"""保存绑定调用方 Session 的整理历史仓储。"""
|
||||
self._repository = repository
|
||||
|
||||
def get_by_src(
|
||||
self,
|
||||
src: str,
|
||||
storage: str | None = None,
|
||||
) -> TransferHistoryRecord | None:
|
||||
"""转发按源路径读取。"""
|
||||
return self._repository.get_by_src(src, storage)
|
||||
|
||||
def get_success_by_src(
|
||||
self,
|
||||
src: str,
|
||||
storage: str | None = None,
|
||||
) -> TransferHistoryRecord | None:
|
||||
"""转发按源路径读取成功记录。"""
|
||||
return self._repository.get_success_by_src(src, storage)
|
||||
|
||||
def add_force(self, **payload: Any) -> TransferHistoryRecord:
|
||||
"""保持应用层旧端口名,但只暂存替换而不自行提交。"""
|
||||
return self._repository.stage_replace_by_src(**payload)
|
||||
|
||||
|
||||
class TransactionalChainDurableEventWriter(ChainDurableEventWriter):
|
||||
"""为每次 Chain 结果事件创建独占同步 Session 和 UoW。"""
|
||||
|
||||
def __init__(self, session_factory: Callable[[], Session]) -> None:
|
||||
"""注入惰性同步 Session 工厂。"""
|
||||
self._session_factory = session_factory
|
||||
|
||||
def download_added(
|
||||
self,
|
||||
*,
|
||||
history_payload: dict[str, Any],
|
||||
file_payloads: list[dict[str, Any]],
|
||||
event_payload: dict[str, Any],
|
||||
after_commit: Callable[[], None],
|
||||
publish: Callable[[dict[str, Any]], None],
|
||||
) -> None:
|
||||
"""原子写下载历史、文件清单和 DownloadAdded intent。"""
|
||||
session = self._session_factory()
|
||||
try:
|
||||
repository = DownloadHistoryOper(session)
|
||||
outbox = SqlAlchemyOutboxRepository(session)
|
||||
command = DurableEventCommand(
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=outbox,
|
||||
)
|
||||
event_key = download_added_event_key(event_payload)
|
||||
event_payload["idempotency_key"] = event_key
|
||||
|
||||
def stage_business() -> None:
|
||||
"""在同一事务暂存下载历史和可选文件清单。"""
|
||||
repository.stage_add(history_payload)
|
||||
if file_payloads:
|
||||
repository.stage_add_files(file_payloads)
|
||||
|
||||
command.execute(
|
||||
intent=OutboxIntent(
|
||||
event_key=event_key,
|
||||
topic="download.added",
|
||||
payload=snapshot_download_added(event_payload),
|
||||
),
|
||||
stage_business=stage_business,
|
||||
after_commit=after_commit,
|
||||
publish=lambda: publish(event_payload),
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def transfer_result(
|
||||
self,
|
||||
*,
|
||||
topic: str,
|
||||
stage_history: Callable[[TransferHistoryWriter], TransferHistoryRecord | None],
|
||||
event_payload: dict[str, Any],
|
||||
publish: Callable[[dict[str, Any]], None],
|
||||
) -> TransferHistoryRecord | None:
|
||||
"""原子写整理历史与结果 intent,并返回脱离 Session 的最小投影。"""
|
||||
session = self._session_factory()
|
||||
try:
|
||||
staging = _StagingTransferHistoryWriter(TransferHistoryOper(session))
|
||||
command = DurableEventCommand(
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=SqlAlchemyOutboxRepository(session),
|
||||
)
|
||||
|
||||
def stage_business() -> TransferHistoryRef | None:
|
||||
"""复用历史字段映射,并在 flush 后冻结安全投影。"""
|
||||
history = stage_history(staging)
|
||||
if history is None:
|
||||
return None
|
||||
return TransferHistoryRef(
|
||||
id=history.id,
|
||||
status=bool(history.status),
|
||||
src=history.src,
|
||||
src_storage=history.src_storage,
|
||||
src_fileitem=history.src_fileitem,
|
||||
)
|
||||
|
||||
def build_intent(
|
||||
history: TransferHistoryRef | None,
|
||||
) -> OutboxIntent:
|
||||
"""历史 ID 确定后构造事件键与可恢复快照。"""
|
||||
if history is None:
|
||||
raise RuntimeError("整理历史暂存失败,无法登记 durable 结果事件")
|
||||
event_key = transfer_result_event_key(topic, history.id)
|
||||
event_payload["transfer_history_id"] = history.id
|
||||
event_payload["idempotency_key"] = event_key
|
||||
return OutboxIntent(
|
||||
event_key=event_key,
|
||||
topic=topic,
|
||||
payload=snapshot_transfer_result(event_payload),
|
||||
)
|
||||
|
||||
return command.execute(
|
||||
intent=build_intent,
|
||||
stage_business=stage_business,
|
||||
publish=lambda: publish(event_payload),
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
@@ -0,0 +1 @@
|
||||
"""宿主运行时对象、配置快照与跨层依赖的组合构建。"""
|
||||
@@ -0,0 +1,91 @@
|
||||
"""订阅事务作用域及提交后回调的组合装配。"""
|
||||
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
from typing import Any
|
||||
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.application.subscription.complete import (
|
||||
CompleteSubscriptionCommand,
|
||||
configure_subscription_completion_scope,
|
||||
)
|
||||
from app.application.subscription.delete import (
|
||||
DeleteSubscribeCommand,
|
||||
configure_delete_subscribe_scope,
|
||||
)
|
||||
from app.application.subscription.mutation import (
|
||||
SubscriptionMutationService,
|
||||
configure_subscription_mutation_scope,
|
||||
)
|
||||
from app.db.adapters.outbox import (
|
||||
SqlAlchemyAsyncOutboxStager,
|
||||
SqlAlchemyOutboxRepository,
|
||||
)
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.subscribehistory import SubscribeHistoryOper
|
||||
from app.db.session import SessionFactory, async_session_scope
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
from app.runtime.events import EventManager
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
async def _publish_modified(payload: dict[str, Any]) -> None:
|
||||
"""发布事务已提交的订阅修改事件。"""
|
||||
await EventManager().async_send_event(EventType.SubscribeModified, payload)
|
||||
|
||||
|
||||
async def _publish_deleted(payload: dict[str, Any]) -> None:
|
||||
"""发布事务已提交的订阅删除事件。"""
|
||||
await EventManager().async_send_event(EventType.SubscribeDeleted, payload)
|
||||
|
||||
|
||||
def _publish_completed(payload: dict[str, Any]) -> None:
|
||||
"""发布已提交的订阅完成事件。"""
|
||||
EventManager().send_event(EventType.SubscribeComplete, payload)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def subscription_completion_scope():
|
||||
"""为同步完成链创建独占 Session、UoW 与 durable outbox。"""
|
||||
session = SessionFactory()
|
||||
try:
|
||||
yield CompleteSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=SqlAlchemyOutboxRepository(session),
|
||||
publish=_publish_completed,
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def subscription_mutation_scope():
|
||||
"""为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。"""
|
||||
async with async_session_scope() as session:
|
||||
yield SubscriptionMutationService(
|
||||
repository=SubscribeOper(session),
|
||||
history_repository=SubscribeHistoryOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
outbox=SqlAlchemyAsyncOutboxStager(session),
|
||||
publish_modified=_publish_modified,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def delete_subscribe_scope():
|
||||
"""为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。"""
|
||||
async with async_session_scope() as session:
|
||||
yield DeleteSubscribeCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
publish_deleted=_publish_deleted,
|
||||
report_deleted=MoviePilotServerHelper.async_sub_done_durable,
|
||||
outbox=SqlAlchemyAsyncOutboxStager(session),
|
||||
)
|
||||
|
||||
|
||||
def configure_transactional_subscription_scopes() -> None:
|
||||
"""登记 Agent 等非 HTTP 入口复用的订阅事务作用域。"""
|
||||
configure_subscription_mutation_scope(subscription_mutation_scope)
|
||||
configure_delete_subscribe_scope(delete_subscribe_scope)
|
||||
configure_subscription_completion_scope(subscription_completion_scope)
|
||||
@@ -1,55 +0,0 @@
|
||||
"""下载失败冷却切片的显式会话与事务适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
|
||||
from app.db.oper.downloadfailure import DownloadFailureOper
|
||||
from app.db.uow import SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
class TransactionalDownloadFailureRepository:
|
||||
"""为 Chain 下载失败读写创建短生命周期会话并显式收口事务。"""
|
||||
|
||||
def __init__(self, session_factory: Callable[[], Any]) -> None:
|
||||
"""保存由启动组合根提供的同步会话工厂。"""
|
||||
self._session_factory = session_factory
|
||||
|
||||
def get_active_by_fingerprints(
|
||||
self,
|
||||
fingerprints: list[str],
|
||||
now_time: str,
|
||||
) -> dict[str, Any]:
|
||||
"""在独立只读会话中查询仍处于冷却期的失败记录。"""
|
||||
with self._session_factory() as session:
|
||||
return cast(
|
||||
dict[str, Any],
|
||||
DownloadFailureOper(db=session).get_active_by_fingerprints(
|
||||
fingerprints=fingerprints,
|
||||
now_time=now_time,
|
||||
),
|
||||
)
|
||||
|
||||
def record_failure(
|
||||
self,
|
||||
fingerprint: str,
|
||||
now_time: str,
|
||||
next_retry_at: str,
|
||||
**kwargs: object,
|
||||
) -> Any:
|
||||
"""在一个显式 UoW 中新增或更新下载失败记录。"""
|
||||
with self._session_factory() as session:
|
||||
transaction = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
failure = DownloadFailureOper(db=session).record_failure(
|
||||
fingerprint=fingerprint,
|
||||
now_time=now_time,
|
||||
next_retry_at=next_retry_at,
|
||||
**kwargs,
|
||||
)
|
||||
transaction.commit()
|
||||
return failure
|
||||
except Exception:
|
||||
transaction.rollback()
|
||||
raise
|
||||
@@ -0,0 +1 @@
|
||||
"""按领域组织的宿主初始化与关闭入口。"""
|
||||
@@ -17,7 +17,7 @@ from app.db.base import Base
|
||||
from app.db.engine import get_engine
|
||||
from app.db.models import load_all_models
|
||||
from app.runtime.log import logger
|
||||
from app.startup.database import build_database_governance
|
||||
from app.startup.composition.database import build_database_governance
|
||||
|
||||
|
||||
def _build_alembic_config(engine: Engine | None = None) -> Config:
|
||||
@@ -53,7 +53,7 @@ from app.application.configuration import (
|
||||
configure_system_config,
|
||||
configure_transfer_retry_config,
|
||||
)
|
||||
from app.startup.configuration import (
|
||||
from app.startup.composition.configuration import (
|
||||
build_api_runtime_config,
|
||||
build_chain_runtime_config,
|
||||
build_scheduler_runtime_config,
|
||||
@@ -83,7 +83,7 @@ from app.application.security.userconfig import (
|
||||
)
|
||||
from app.application.history import configure_transfer_history_provider
|
||||
from app.application.outbox import OutboxDispatcher, configure_outbox_dispatcher
|
||||
from app.startup.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
|
||||
from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
|
||||
from app.application.site.query import SiteQueryService, configure_site_query_service
|
||||
from app.application.site.health import SiteHealthService, configure_site_health_service
|
||||
from app.application.workflow import WorkflowQueryService, configure_workflow_query
|
||||
@@ -129,22 +129,22 @@ from app.command import CommandChain
|
||||
from app.schemas.message import Message
|
||||
from app.schemas.message import MessageType
|
||||
from app.schemas.types import EventType, SystemConfigKey
|
||||
from app.startup.agent_initializer import init_agent
|
||||
from app.startup.database import build_database_governance
|
||||
from app.startup.managed_resources_initializer import (
|
||||
from app.startup.initializers.agent import init_agent
|
||||
from app.startup.composition.database import build_database_governance
|
||||
from app.startup.initializers.managed_resources import (
|
||||
init_managed_resources,
|
||||
stop_managed_resources,
|
||||
)
|
||||
from app.startup.subscription import (
|
||||
TransactionalSubscribeWriter,
|
||||
from app.db.adapters.subscription import TransactionalSubscribeWriter
|
||||
from app.startup.composition.subscription import (
|
||||
configure_transactional_subscription_scopes,
|
||||
)
|
||||
from app.startup.chain_events import TransactionalChainDurableEventWriter
|
||||
from app.startup.download_failure import TransactionalDownloadFailureRepository
|
||||
from app.startup.site import TransactionalSiteRepository
|
||||
from app.startup.workflow import TransactionalWorkflowExecutionService
|
||||
from app.startup.transaction import TransactionalWriteRunner
|
||||
from app.startup.context import (
|
||||
from app.db.adapters.chain import TransactionalChainDurableEventWriter
|
||||
from app.db.adapters.download import TransactionalDownloadFailureRepository
|
||||
from app.db.adapters.site import TransactionalSiteRepository
|
||||
from app.db.adapters.workflow import TransactionalWorkflowExecutionService
|
||||
from app.db.adapters.transaction import TransactionalWriteRunner
|
||||
from app.startup.composition.context import (
|
||||
AgentChatRuntime,
|
||||
AuthenticationRuntime,
|
||||
HistoryRuntime,
|
||||
@@ -8,7 +8,7 @@ from typing import Callable
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.startup.cache_initializer import configure_cache_dependencies
|
||||
from app.startup.initializers.cache import configure_cache_dependencies
|
||||
# 缓存装饰器会在业务模块导入时创建后端,必须先完成适配器装配。
|
||||
configure_cache_dependencies()
|
||||
# urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁
|
||||
@@ -37,17 +37,17 @@ from app.runtime.tasks import TaskRegistry, configure_task_registry
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.runtime.state import SystemHelper
|
||||
from app.runtime.log import logger, LoggerManager
|
||||
from app.startup.command_initializer import init_command, stop_command, restart_command
|
||||
from app.startup.agent_initializer import stop_agent
|
||||
from app.startup.domain_initializer import configure_domain_dependencies
|
||||
from app.startup.modules_initializer import (
|
||||
from app.startup.initializers.command import init_command, stop_command, restart_command
|
||||
from app.startup.initializers.agent import stop_agent
|
||||
from app.startup.initializers.domain import configure_domain_dependencies
|
||||
from app.startup.initializers.modules import (
|
||||
drain_events,
|
||||
init_modules,
|
||||
settle_events,
|
||||
stop_modules,
|
||||
)
|
||||
from app.startup.monitor_initializer import stop_monitor, init_monitor
|
||||
from app.startup.plugins_initializer import (
|
||||
from app.startup.initializers.monitor import stop_monitor, init_monitor
|
||||
from app.startup.initializers.plugins import (
|
||||
configure_plugin_services,
|
||||
execute_task,
|
||||
finalize_plugins,
|
||||
@@ -57,18 +57,18 @@ from app.startup.plugins_initializer import (
|
||||
stop_plugin_monitor,
|
||||
sync_plugins,
|
||||
)
|
||||
from app.startup.routers_initializer import init_routers
|
||||
from app.startup.scheduler_initializer import (
|
||||
from app.startup.initializers.routers import init_routers
|
||||
from app.startup.initializers.scheduler import (
|
||||
stop_scheduler,
|
||||
init_scheduler,
|
||||
init_plugin_scheduler,
|
||||
)
|
||||
from app.db.engine import check_connection_budget, get_engine, get_global_async_engine
|
||||
from app.startup.transfer_initializer import (
|
||||
from app.startup.initializers.transfer import (
|
||||
replay_pending_transfers,
|
||||
stop_transfer_runtime,
|
||||
)
|
||||
from app.startup.workflow_initializer import init_workflow, stop_workflow
|
||||
from app.startup.initializers.workflow import init_workflow, stop_workflow
|
||||
from app.startup.lifecycle.components import (
|
||||
LifecycleComponent,
|
||||
LifecycleFailurePolicy,
|
||||
@@ -236,7 +236,7 @@ async def initialize_modules_component(app: FastAPI) -> None:
|
||||
try:
|
||||
runtime = await init_modules()
|
||||
except BaseException:
|
||||
from app.startup.modules_initializer import stop_database_worker
|
||||
from app.startup.initializers.modules import stop_database_worker
|
||||
|
||||
try:
|
||||
await stop_database_worker()
|
||||
@@ -276,7 +276,7 @@ def prepare_database_component(app: FastAPI) -> None:
|
||||
"""完成数据库建表、迁移与 head 校验后发布数据库就绪状态。"""
|
||||
# Alembic 及全部 ORM 元数据只在 lifespan 真正启动时加载,create_app/import 阶段
|
||||
# 继续保持不建库、不加载迁移运行时的纯 ASGI 结构语义。
|
||||
from app.startup.database_initializer import (
|
||||
from app.startup.initializers.database import (
|
||||
prepare_database,
|
||||
verify_database_revision,
|
||||
)
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
"""启动组合层使用的 SQLAlchemy outbox 持久化适配器。"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import or_, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.outbox import ClaimedOutboxMessage, OutboxIntent
|
||||
from app.db.base import execute_dml
|
||||
from app.db.models.outbox import OutboxMessage
|
||||
|
||||
|
||||
def _iso(value: datetime) -> str:
|
||||
"""将带时区时间统一序列化为可排序 ISO 字符串。"""
|
||||
return value.isoformat()
|
||||
|
||||
|
||||
class SqlAlchemyOutboxRepository:
|
||||
"""使用调用方 Session 原子暂存并条件认领 outbox。"""
|
||||
|
||||
def __init__(self, session: Session) -> None:
|
||||
"""保存由调用方拥有的 SQLAlchemy Session。"""
|
||||
self._session = session
|
||||
|
||||
def stage(self, intent: OutboxIntent, now: datetime) -> None:
|
||||
"""加入当前事务并 flush,使唯一键冲突在业务 commit 前暴露。"""
|
||||
self._session.add(
|
||||
OutboxMessage(
|
||||
event_key=intent.event_key,
|
||||
topic=intent.topic,
|
||||
payload_version=intent.payload_version,
|
||||
payload=intent.payload,
|
||||
status="pending",
|
||||
attempt=0,
|
||||
next_retry_at=_iso(now),
|
||||
created_at=_iso(now),
|
||||
)
|
||||
)
|
||||
self._session.flush()
|
||||
|
||||
def claim(
|
||||
self,
|
||||
now: datetime,
|
||||
lease_until: datetime,
|
||||
) -> ClaimedOutboxMessage | None:
|
||||
"""条件更新候选行;并发丢失竞争时返回 None。"""
|
||||
now_text = _iso(now)
|
||||
candidate = self._session.execute(
|
||||
select(OutboxMessage)
|
||||
.where(
|
||||
OutboxMessage.status.in_(("pending", "processing")),
|
||||
OutboxMessage.next_retry_at <= now_text,
|
||||
or_(
|
||||
OutboxMessage.lease_until.is_(None),
|
||||
OutboxMessage.lease_until <= now_text,
|
||||
),
|
||||
)
|
||||
.order_by(OutboxMessage.id)
|
||||
.limit(1)
|
||||
).scalars().first()
|
||||
if candidate is None:
|
||||
return None
|
||||
next_attempt = candidate.attempt + 1
|
||||
claimed = execute_dml(
|
||||
self._session,
|
||||
update(OutboxMessage)
|
||||
.where(
|
||||
OutboxMessage.id == candidate.id,
|
||||
OutboxMessage.attempt == candidate.attempt,
|
||||
or_(
|
||||
OutboxMessage.lease_until.is_(None),
|
||||
OutboxMessage.lease_until <= now_text,
|
||||
),
|
||||
)
|
||||
.values(
|
||||
status="processing",
|
||||
attempt=next_attempt,
|
||||
lease_until=_iso(lease_until),
|
||||
),
|
||||
)
|
||||
self._session.commit()
|
||||
if not claimed:
|
||||
return None
|
||||
return ClaimedOutboxMessage(
|
||||
message_id=candidate.id,
|
||||
event_key=candidate.event_key,
|
||||
topic=candidate.topic,
|
||||
payload=dict(candidate.payload),
|
||||
payload_version=candidate.payload_version,
|
||||
attempt=next_attempt,
|
||||
)
|
||||
|
||||
def complete(self, message_id: int, completed_at: datetime) -> None:
|
||||
"""持久化完成终态并释放 lease。"""
|
||||
self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.id == message_id)
|
||||
.values(status="completed", completed_at=_iso(completed_at), lease_until=None)
|
||||
)
|
||||
self._session.commit()
|
||||
|
||||
def complete_by_event_key(self, event_key: str, completed_at: datetime) -> None:
|
||||
"""即时 post-commit 全部成功时按幂等键收口对应 intent。"""
|
||||
self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.event_key == event_key)
|
||||
.values(status="completed", completed_at=_iso(completed_at), lease_until=None)
|
||||
)
|
||||
self._session.commit()
|
||||
|
||||
def retry(
|
||||
self,
|
||||
message_id: int,
|
||||
*,
|
||||
next_retry_at: datetime,
|
||||
last_error: str,
|
||||
dead: bool,
|
||||
) -> None:
|
||||
"""持久化下一次退避或不可自动重试的 dead 终态。"""
|
||||
self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.id == message_id)
|
||||
.values(
|
||||
status="dead" if dead else "pending",
|
||||
next_retry_at=_iso(next_retry_at),
|
||||
lease_until=None,
|
||||
last_error=last_error,
|
||||
)
|
||||
)
|
||||
self._session.commit()
|
||||
|
||||
|
||||
class SqlAlchemyAsyncOutboxStager:
|
||||
"""只负责把 outbox 意图加入调用方异步事务。"""
|
||||
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
"""保存由异步订阅命令拥有的 Session。"""
|
||||
self._session = session
|
||||
|
||||
async def stage(self, intent: OutboxIntent, now: datetime) -> None:
|
||||
"""暂存并 flush,确保业务行与意图由同一次 commit 决定。"""
|
||||
self._session.add(
|
||||
OutboxMessage(
|
||||
event_key=intent.event_key,
|
||||
topic=intent.topic,
|
||||
payload_version=intent.payload_version,
|
||||
payload=intent.payload,
|
||||
status="pending",
|
||||
attempt=0,
|
||||
next_retry_at=_iso(now),
|
||||
created_at=_iso(now),
|
||||
)
|
||||
)
|
||||
await self._session.flush()
|
||||
|
||||
async def complete_by_event_key(
|
||||
self,
|
||||
event_key: str,
|
||||
completed_at: datetime,
|
||||
) -> None:
|
||||
"""异步 post-commit 全部成功时按幂等键收口 intent。"""
|
||||
await self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.event_key == event_key)
|
||||
.values(status="completed", completed_at=_iso(completed_at), lease_until=None)
|
||||
)
|
||||
await self._session.commit()
|
||||
@@ -1,225 +0,0 @@
|
||||
"""站点 Chain 端口的显式会话与事务适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class TransactionalSiteRepository:
|
||||
"""为同步 Chain 站点端口和异步健康统计提供短生命周期会话。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
||||
) -> None:
|
||||
"""保存同步会话工厂和异步会话上下文工厂。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def _read(self, operation: Callable[[SiteOper], T]) -> T:
|
||||
"""在独立同步会话中执行只读站点操作。"""
|
||||
with self._sync_session() as session:
|
||||
return operation(SiteOper(db=session))
|
||||
|
||||
def _write(self, operation: Callable[[SiteOper], T]) -> T:
|
||||
"""在独立同步 UoW 中执行站点写操作。"""
|
||||
with self._sync_session() as session:
|
||||
session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
result = operation(SiteOper(db=session))
|
||||
unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
async def _async_write(
|
||||
self,
|
||||
operation: Callable[[SiteOper], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在独立异步 UoW 中执行站点写操作。"""
|
||||
async with self._async_session() as session:
|
||||
session.sync_session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyAsyncUnitOfWork(session)
|
||||
try:
|
||||
result = await operation(SiteOper(db=session))
|
||||
await unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
await unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
async def _async_read(self, operation: Callable[[SiteOper], Awaitable[T]]) -> T:
|
||||
"""在独立异步会话中执行只读站点操作。"""
|
||||
async with self._async_session() as session:
|
||||
return await operation(SiteOper(db=session))
|
||||
|
||||
def add(self, **kwargs: Any) -> tuple[bool, str]:
|
||||
"""新增站点并提交事务。"""
|
||||
return self._write(lambda repository: repository.add(**kwargs))
|
||||
|
||||
def get(self, site_id: int) -> Any:
|
||||
"""按 ID 查询站点。"""
|
||||
return self._read(lambda repository: repository.get(site_id))
|
||||
|
||||
def get_by_domain(self, domain: str) -> Any:
|
||||
"""按域名查询站点。"""
|
||||
return self._read(lambda repository: repository.get_by_domain(domain))
|
||||
|
||||
def get_domains_by_ids(self, ids: list[int]) -> list[str | None]:
|
||||
"""查询一组站点 ID 对应的域名。"""
|
||||
return self._read(lambda repository: repository.get_domains_by_ids(ids))
|
||||
|
||||
def list(self) -> list[Any]:
|
||||
"""查询全部站点。"""
|
||||
return self._read(lambda repository: repository.list())
|
||||
|
||||
def list_order_by_pri(self) -> list[Any]:
|
||||
"""同步按优先级查询站点。"""
|
||||
return self._read(lambda repository: repository.list_order_by_pri())
|
||||
|
||||
def get_userdata_latest(self) -> list[Any]:
|
||||
"""同步查询各站点最新用户数据。"""
|
||||
return self._read(lambda repository: repository.get_userdata_latest())
|
||||
|
||||
async def async_get(self, site_id: int) -> Any:
|
||||
"""异步按 ID 查询站点。"""
|
||||
return await self._async_read(lambda repository: repository.async_get(site_id))
|
||||
|
||||
async def async_get_by_domain(self, domain: str) -> Any:
|
||||
"""异步按域名查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_by_domain(domain)
|
||||
)
|
||||
|
||||
async def async_get_by_name(self, name: str) -> Any:
|
||||
"""异步按名称查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_by_name(name)
|
||||
)
|
||||
|
||||
async def async_list(self) -> list[Any]:
|
||||
"""异步查询全部站点。"""
|
||||
return await self._async_read(lambda repository: repository.async_list())
|
||||
|
||||
async def async_list_order_by_pri(self) -> list[Any]:
|
||||
"""异步按优先级查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_list_order_by_pri()
|
||||
)
|
||||
|
||||
async def async_update(self, site_id: int, payload: dict[str, Any]) -> Any:
|
||||
"""异步更新站点并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_update(site_id, payload)
|
||||
)
|
||||
|
||||
async def async_get_userdata_by_domain(
|
||||
self,
|
||||
domain: str,
|
||||
workdate: str | None = None,
|
||||
) -> list[Any]:
|
||||
"""异步查询站点用户数据。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_userdata_by_domain(domain, workdate)
|
||||
)
|
||||
|
||||
async def async_get_userdata_latest(self) -> list[Any]:
|
||||
"""异步查询各站点最新用户数据。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_userdata_latest()
|
||||
)
|
||||
|
||||
async def async_get_icon_by_domain(self, domain: str) -> Any:
|
||||
"""异步按域名查询站点图标。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_icon_by_domain(domain)
|
||||
)
|
||||
|
||||
async def async_get_statistic_by_domain(self, domain: str) -> Any:
|
||||
"""异步按域名查询站点统计。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_statistic_by_domain(domain)
|
||||
)
|
||||
|
||||
async def async_list_statistics(self) -> list[Any]:
|
||||
"""异步查询全部站点统计。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_list_statistics()
|
||||
)
|
||||
|
||||
def update(self, site_id: int, payload: dict[str, Any]) -> Any:
|
||||
"""更新站点并提交事务。"""
|
||||
return self._write(lambda repository: repository.update(site_id, payload))
|
||||
|
||||
def update_cookie(self, domain: str, cookies: str) -> tuple[bool, str]:
|
||||
"""更新站点 Cookie 并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_cookie(domain, cookies)
|
||||
)
|
||||
|
||||
def update_rss(self, domain: str, rss: str) -> tuple[bool, str]:
|
||||
"""更新站点 RSS 地址并提交事务。"""
|
||||
return self._write(lambda repository: repository.update_rss(domain, rss))
|
||||
|
||||
def update_userdata(
|
||||
self,
|
||||
domain: str,
|
||||
name: str,
|
||||
payload: dict[str, Any],
|
||||
) -> tuple[bool, str]:
|
||||
"""更新站点用户数据并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_userdata(domain, name, payload)
|
||||
)
|
||||
|
||||
def update_icon(
|
||||
self,
|
||||
name: str,
|
||||
domain: str,
|
||||
icon_url: str,
|
||||
icon_base64: str,
|
||||
) -> bool:
|
||||
"""更新站点图标并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_icon(
|
||||
name,
|
||||
domain,
|
||||
icon_url,
|
||||
icon_base64,
|
||||
)
|
||||
)
|
||||
|
||||
def success(self, domain: str, seconds: int | None = None) -> Any:
|
||||
"""记录站点访问成功并提交事务。"""
|
||||
return self._write(lambda repository: repository.success(domain, seconds))
|
||||
|
||||
def fail(self, domain: str) -> Any:
|
||||
"""记录站点访问失败并提交事务。"""
|
||||
return self._write(lambda repository: repository.fail(domain))
|
||||
|
||||
async def async_success(self, domain: str, seconds: int | None = None) -> Any:
|
||||
"""异步记录站点访问成功并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_success(domain, seconds)
|
||||
)
|
||||
|
||||
async def async_fail(self, domain: str) -> Any:
|
||||
"""异步记录站点访问失败并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_fail(domain)
|
||||
)
|
||||
@@ -1,211 +0,0 @@
|
||||
"""订阅写入事务适配器的启动装配。"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractAsyncContextManager, asynccontextmanager, contextmanager
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.subscription.write import (
|
||||
AfterCommitEffect,
|
||||
AsyncAfterCommitEffect,
|
||||
AsyncCreateSubscriptionCommand,
|
||||
CreateSubscriptionCommand,
|
||||
subscription_added_event_key,
|
||||
subscription_added_notification_key,
|
||||
subscription_added_report_key,
|
||||
)
|
||||
from app.application.subscription.delete import (
|
||||
DeleteSubscribeCommand,
|
||||
configure_delete_subscribe_scope,
|
||||
)
|
||||
from app.application.subscription.complete import (
|
||||
CompleteSubscriptionCommand,
|
||||
configure_subscription_completion_scope,
|
||||
)
|
||||
from app.application.subscription.mutation import (
|
||||
SubscriptionMutationService,
|
||||
configure_subscription_mutation_scope,
|
||||
)
|
||||
from app.adapters.external.server import MoviePilotServerHelper
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.oper.subscribehistory import SubscribeHistoryOper
|
||||
from app.db.session import async_session_scope
|
||||
from app.db.session import SessionFactory
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
from app.startup.outbox import (
|
||||
SqlAlchemyAsyncOutboxStager,
|
||||
SqlAlchemyOutboxRepository,
|
||||
)
|
||||
from app.runtime.events import EventManager
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
class TransactionalSubscribeWriter:
|
||||
"""为每次订阅新增创建独占会话,并把提交权交给 Application Command。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[
|
||||
[],
|
||||
AbstractAsyncContextManager[AsyncSession],
|
||||
],
|
||||
) -> None:
|
||||
"""注入同步会话工厂和异步会话作用域。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def add(
|
||||
self,
|
||||
identity: dict,
|
||||
payload: dict,
|
||||
username: str | None = None,
|
||||
after_commit: AfterCommitEffect | None = None,
|
||||
notification: dict[str, object] | None = None,
|
||||
) -> tuple[int, str]:
|
||||
"""在独占同步会话内执行一次完整订阅新增事务。"""
|
||||
session = self._sync_session()
|
||||
try:
|
||||
outbox = SqlAlchemyOutboxRepository(session)
|
||||
command = CreateSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=outbox,
|
||||
)
|
||||
|
||||
def delivered(subscribe_id: int) -> None:
|
||||
"""执行旧 post-commit 编排,全部成功后收口 durable intent。"""
|
||||
if after_commit:
|
||||
after_commit(subscribe_id)
|
||||
outbox.complete_by_event_key(
|
||||
subscription_added_event_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
if notification:
|
||||
outbox.complete_by_event_key(
|
||||
subscription_added_notification_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
outbox.complete_by_event_key(
|
||||
subscription_added_report_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
return command.execute(
|
||||
identity,
|
||||
payload,
|
||||
username,
|
||||
delivered,
|
||||
notification,
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
async def async_add(
|
||||
self,
|
||||
identity: dict,
|
||||
payload: dict,
|
||||
username: str | None = None,
|
||||
after_commit: AsyncAfterCommitEffect | None = None,
|
||||
notification: dict[str, object] | None = None,
|
||||
) -> tuple[int, str]:
|
||||
"""在独占异步会话作用域内执行一次完整订阅新增事务。"""
|
||||
async with self._async_session() as session:
|
||||
outbox = SqlAlchemyAsyncOutboxStager(session)
|
||||
command = AsyncCreateSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
outbox=outbox,
|
||||
)
|
||||
|
||||
async def delivered(subscribe_id: int) -> None:
|
||||
"""异步执行旧编排,全部成功后收口 durable intent。"""
|
||||
if after_commit:
|
||||
await after_commit(subscribe_id)
|
||||
await outbox.complete_by_event_key(
|
||||
subscription_added_event_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
if notification:
|
||||
await outbox.complete_by_event_key(
|
||||
subscription_added_notification_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
await outbox.complete_by_event_key(
|
||||
subscription_added_report_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
return await command.execute(
|
||||
identity,
|
||||
payload,
|
||||
username,
|
||||
delivered,
|
||||
notification,
|
||||
)
|
||||
|
||||
|
||||
async def _publish_modified(payload: dict[str, Any]) -> None:
|
||||
"""发布事务已提交的订阅修改事件。"""
|
||||
await EventManager().async_send_event(EventType.SubscribeModified, payload)
|
||||
|
||||
|
||||
async def _publish_deleted(payload: dict[str, Any]) -> None:
|
||||
"""发布事务已提交的订阅删除事件。"""
|
||||
await EventManager().async_send_event(EventType.SubscribeDeleted, payload)
|
||||
|
||||
|
||||
def _publish_completed(payload: dict[str, Any]) -> None:
|
||||
"""发布已提交的订阅完成事件。"""
|
||||
EventManager().send_event(EventType.SubscribeComplete, payload)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def subscription_completion_scope():
|
||||
"""为同步完成链创建独占 Session、UoW 与 durable outbox。"""
|
||||
session = SessionFactory()
|
||||
try:
|
||||
yield CompleteSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=SqlAlchemyOutboxRepository(session),
|
||||
publish=_publish_completed,
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def subscription_mutation_scope():
|
||||
"""为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。"""
|
||||
async with async_session_scope() as session:
|
||||
yield SubscriptionMutationService(
|
||||
repository=SubscribeOper(session),
|
||||
history_repository=SubscribeHistoryOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
outbox=SqlAlchemyAsyncOutboxStager(session),
|
||||
publish_modified=_publish_modified,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def delete_subscribe_scope():
|
||||
"""为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。"""
|
||||
async with async_session_scope() as session:
|
||||
yield DeleteSubscribeCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
publish_deleted=_publish_deleted,
|
||||
report_deleted=MoviePilotServerHelper.async_sub_done_durable,
|
||||
outbox=SqlAlchemyAsyncOutboxStager(session),
|
||||
)
|
||||
|
||||
|
||||
def configure_transactional_subscription_scopes() -> None:
|
||||
"""登记 Agent 等非 HTTP 入口复用的订阅事务作用域。"""
|
||||
configure_subscription_mutation_scope(subscription_mutation_scope)
|
||||
configure_delete_subscribe_scope(delete_subscribe_scope)
|
||||
configure_subscription_completion_scope(subscription_completion_scope)
|
||||
@@ -1,61 +0,0 @@
|
||||
"""旧 Oper 写入口的 SQLAlchemy 事务执行适配器。"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from typing import TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class TransactionalWriteRunner:
|
||||
"""为兼容写入口创建独占会话,并用 UoW 明确提交或回滚。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
||||
) -> None:
|
||||
"""保存同步会话工厂和异步会话上下文工厂。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def sync(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在独占同步 Session 中执行操作并统一收口事务。"""
|
||||
session = self._sync_session()
|
||||
# 兼容 Oper 历史上会返回刚写入的 ORM 对象;提交后若过期,Session 关闭后连主键
|
||||
# 都无法读取。独占短会话没有后续一致性读取需求,因此保留已 flush 的字段快照。
|
||||
session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
result = operation(session)
|
||||
unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
unit_of_work.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
async def async_(
|
||||
self,
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在独占 AsyncSession 中执行操作并统一收口事务。"""
|
||||
async with self._async_session() as session:
|
||||
# 与同步兼容入口保持相同的返回对象生命周期。
|
||||
session.sync_session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyAsyncUnitOfWork(session)
|
||||
try:
|
||||
result = await operation(session)
|
||||
await unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
await unit_of_work.rollback()
|
||||
raise
|
||||
@@ -1,71 +0,0 @@
|
||||
"""工作流执行状态事务适配器。"""
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user