mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 03:18:14 +08:00
233 lines
9.2 KiB
Python
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()
|