Files
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

233 lines
9.2 KiB
Python

"""
持久化 Outbox handler 与 dispatcher 的宿主组合装配。
"""
from collections.abc import Callable
from app.adapters.external.server import MoviePilotServerHelper
from app.application.chain.events import (
restore_download_added,
restore_download_processing,
restore_transfer_result,
)
from app.application.outbox import (
DOWNLOAD_MODULE_TOPIC,
DOWNLOAD_NOTIFICATION_TOPIC,
DOWNLOAD_SUBTITLE_TOPIC,
ClaimedOutboxMessage,
OutboxDispatcher,
durable_event_topic,
reset_outbox_dispatcher,
validate_durable_event_handlers,
)
from app.command import CommandChain
from app.db.adapters.outbox import SqlAlchemyOutboxDispatchStore
from app.db.session import SessionFactory
from app.runtime.correlation import correlation_scope
from app.runtime.events import EventManager
from app.runtime.observability import record_metric
from app.schemas.message import Message
from app.schemas.types import EventType
def build_outbox_handlers() -> dict[
str,
Callable[[ClaimedOutboxMessage], None],
]:
"""
构造等待真实执行边界的 at-least-once 通知、事件和统计 handler。
"""
event_manager_factory: Callable[[], EventManager] = EventManager
def discard_event_receipt(_event: object) -> None:
"""
丢弃普通事件 API 的回执,使 outbox handler 仅表达结算成功。
"""
def dispatch_subscribe_deleted_report(message: ClaimedOutboxMessage) -> None:
"""
重放订阅删除统计;未确认时抛错以进入有限重试。
"""
if not MoviePilotServerHelper.sub_done_durable(message.payload.get("subscribe_info") or {}):
raise RuntimeError("订阅删除统计上报未确认")
def dispatch_subscribe_added_report(message: ClaimedOutboxMessage) -> None:
"""
重放订阅新增统计;未确认时抛错以进入有限重试。
"""
if not MoviePilotServerHelper.sub_reg_durable(message.payload.get("subscribe_info") or {}):
raise RuntimeError("订阅新增统计上报未确认")
def dispatch_subscribe_complete_report(message: ClaimedOutboxMessage) -> None:
"""
重放订阅完成统计;未确认时抛错以进入有限重试。
"""
if not MoviePilotServerHelper.sub_done_durable(message.payload.get("subscribe_info") or {}):
raise RuntimeError("订阅完成统计上报未确认")
def dispatch_subscribe_notification(message: ClaimedOutboxMessage) -> None:
"""
恢复订阅完成通知;消息快照无需重建领域对象。
"""
snapshot = message.payload.get("message") or {}
if not isinstance(snapshot, dict):
raise RuntimeError("订阅完成通知快照格式无效")
CommandChain().post_message_strict(
Message.model_validate(snapshot),
event_key=message.event_key,
)
def dispatch_subscribe_added_notification(message: ClaimedOutboxMessage) -> None:
"""
恢复订阅新增通知;恢复使用提交前冻结的渲染消息快照。
"""
snapshot = message.payload.get("message") or {}
if not isinstance(snapshot, dict):
raise RuntimeError("订阅新增通知快照格式无效")
CommandChain().post_message_strict(
Message.model_validate(snapshot),
event_key=message.event_key,
)
def dispatch_download_notification(message: ClaimedOutboxMessage) -> None:
"""
按稳定事件键同步恢复已渲染的下载通知。
"""
snapshot = message.payload.get("message") or {}
if not isinstance(snapshot, dict):
raise RuntimeError("下载通知快照格式无效")
CommandChain().post_message_strict(
Message.model_validate(snapshot),
event_key=message.event_key,
)
def dispatch_download_module(message: ClaimedOutboxMessage) -> None:
"""
恢复模块下载后处理,并向模块边界传播稳定幂等键。
"""
from app.chain.download import DownloadChain
snapshot = restore_download_processing(message.payload)
with correlation_scope(message.event_key):
DownloadChain().download_added(
context=snapshot.context,
download_dir=snapshot.download_dir,
torrent_content=snapshot.torrent_content,
)
def dispatch_download_subtitle(message: ClaimedOutboxMessage) -> None:
"""
恢复站点字幕处理,避免模块成功后因字幕失败重复执行模块。
"""
from app.chain.download import DownloadChain
snapshot = restore_download_processing(message.payload)
with correlation_scope(message.event_key):
DownloadChain().download_site_subtitles(
context=snapshot.context,
download_dir=snapshot.download_dir,
torrent_content=snapshot.torrent_content,
download_hash=snapshot.download_hash,
downloader=snapshot.downloader,
)
handlers: dict[str, Callable[[ClaimedOutboxMessage], None]] = {
durable_event_topic(EventType.SubscribeAdded): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.SubscribeAdded,
message.payload,
)
),
"subscribe.added.report": dispatch_subscribe_added_report,
"subscribe.added.notification": dispatch_subscribe_added_notification,
durable_event_topic(EventType.SubscribeModified): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.SubscribeModified,
message.payload,
)
),
durable_event_topic(EventType.SubscribeDeleted): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.SubscribeDeleted,
message.payload,
)
),
"subscribe.deleted.report": dispatch_subscribe_deleted_report,
durable_event_topic(EventType.SubscribeComplete): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.SubscribeComplete,
message.payload,
)
),
"subscribe.complete.report": dispatch_subscribe_complete_report,
"subscribe.complete.notification": dispatch_subscribe_notification,
durable_event_topic(EventType.DownloadAdded): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.DownloadAdded,
restore_download_added(message.payload),
)
),
DOWNLOAD_NOTIFICATION_TOPIC: dispatch_download_notification,
DOWNLOAD_MODULE_TOPIC: dispatch_download_module,
DOWNLOAD_SUBTITLE_TOPIC: dispatch_download_subtitle,
durable_event_topic(EventType.TransferComplete): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.TransferComplete,
restore_transfer_result(message.payload),
)
),
durable_event_topic(EventType.TransferFailed): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.TransferFailed,
restore_transfer_result(message.payload),
)
),
durable_event_topic(EventType.SubtitleTransferComplete): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.SubtitleTransferComplete,
restore_transfer_result(message.payload),
)
),
durable_event_topic(EventType.SubtitleTransferFailed): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.SubtitleTransferFailed,
restore_transfer_result(message.payload),
)
),
durable_event_topic(EventType.AudioTransferComplete): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.AudioTransferComplete,
restore_transfer_result(message.payload),
)
),
durable_event_topic(EventType.AudioTransferFailed): lambda message: discard_event_receipt(
event_manager_factory().send_event_strict(
EventType.AudioTransferFailed,
restore_transfer_result(message.payload),
)
),
}
validate_durable_event_handlers(handlers)
return handlers
def build_outbox_dispatcher() -> OutboxDispatcher:
"""
创建使用独立短事务和 attempt fencing 的恢复 dispatcher。
"""
return OutboxDispatcher(
repository=SqlAlchemyOutboxDispatchStore(SessionFactory),
handlers=build_outbox_handlers(),
failure_observer=lambda dead: record_metric(
"scheduler.job.dead_letter" if dead else "scheduler.job.retry",
owner="outbox",
),
)
def reset_outbox_services() -> None:
"""
撤销当前 lifespan 发布的 Outbox dispatcher 工厂。
"""
reset_outbox_dispatcher()