refactor: make download and transfer events durable

This commit is contained in:
jxxghp
2026-08-22 08:03:00 +08:00
parent c5de1c7b1b
commit 820f7e8850
26 changed files with 1134 additions and 184 deletions
+152
View File
@@ -0,0 +1,152 @@
"""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()
+18
View File
@@ -107,6 +107,7 @@ from app.startup.subscription import (
TransactionalSubscribeWriter,
configure_transactional_subscription_scopes,
)
from app.startup.chain_events import TransactionalChainDurableEventWriter
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
@@ -115,6 +116,10 @@ from app.application.chain.context import (
ChainRuntimeContext,
configure_chain_runtime_context_provider,
)
from app.application.chain.durable_events import (
restore_download_added,
restore_transfer_result,
)
from app.application.chain.data import configure_chain_data_ports, get_chain_data_ports
from app.runtime.extensions.service_config import (
ServiceConfigHelper,
@@ -147,6 +152,7 @@ def _build_chain_runtime_context() -> ChainRuntimeContext:
),
module_dispatcher_factory=ModuleInvocationDispatcher,
data_ports=get_chain_data_ports(),
durable_event_writer=TransactionalChainDurableEventWriter(SessionFactory),
)
@@ -217,6 +223,18 @@ def _build_outbox_dispatcher() -> OutboxDispatcher:
EventType.SubscribeDeleted,
message.payload,
),
"download.added": lambda message: EventManager().send_event(
EventType.DownloadAdded,
restore_download_added(message.payload),
),
"transfer.completed": lambda message: EventManager().send_event(
EventType.TransferComplete,
restore_transfer_result(message.payload),
),
"transfer.failed": lambda message: EventManager().send_event(
EventType.TransferFailed,
restore_transfer_result(message.payload),
),
},
close=session.close,
)