mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 11:54:50 +08:00
* fix: arbitrate subscription identity and restore workflow state * test: preserve legacy empty-owner subscription transactions * fix: align shared atomic configuration method with lifecycle prerequisite --------- Co-authored-by: jxxghp <jxxghp@gmail.com>
291 lines
9.8 KiB
Python
291 lines
9.8 KiB
Python
"""
|
|
订阅事务作用域及提交后回调的组合装配。
|
|
"""
|
|
|
|
import asyncio
|
|
import threading
|
|
from collections.abc import AsyncIterator, Callable, Iterator, Mapping
|
|
from contextlib import asynccontextmanager, contextmanager
|
|
from typing import Any
|
|
|
|
from app.adapters.external.server import MoviePilotServerHelper
|
|
from app.application.outbox import AsyncOutboxDispatchStore
|
|
from app.application.rules import (
|
|
AsyncRuleGroupMutationService,
|
|
SyncRuleGroupMutationService,
|
|
)
|
|
from app.application.site.mutation import SyncSiteReferenceMutationService
|
|
from app.application.subscription.complete import CompleteSubscriptionCommand
|
|
from app.application.subscription.contract import SubscriptionStagingPort
|
|
from app.application.subscription.delete import (
|
|
DeleteSubscribeCommand,
|
|
SyncDeleteSubscribeCommand,
|
|
)
|
|
from app.application.subscription.mutation import (
|
|
SubscriptionMutationService,
|
|
SyncSubscriptionMutationService,
|
|
)
|
|
from app.application.subscription.write import (
|
|
AsyncSubscriptionOutboxStager,
|
|
AsyncUnitOfWork,
|
|
SubscriptionBatchWritePort,
|
|
)
|
|
from app.db.adapters.configuration import SessionSystemConfigurationRepository
|
|
from app.db.adapters.outbox import (
|
|
SqlAlchemyAsyncOutboxDispatchStore,
|
|
SqlAlchemyAsyncOutboxStager,
|
|
SqlAlchemyOutboxDispatchStore,
|
|
SqlAlchemyOutboxStager,
|
|
)
|
|
from app.db.adapters.subscription import (
|
|
SessionSubscriptionBatchWriter,
|
|
SessionSubscriptionHistoryRepository,
|
|
SessionSubscriptionRepository,
|
|
)
|
|
from app.db.session import SessionFactory, async_session_scope
|
|
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
|
from app.runtime.events import eventmanager
|
|
from app.runtime.tasks import get_task_registry
|
|
from app.schemas.common import JsonData
|
|
from app.schemas.types import EventType, SystemConfigKey
|
|
|
|
_reference_mutation_lock = threading.Lock()
|
|
SystemConfigPublisher = Callable[[Mapping[SystemConfigKey, JsonData]], None]
|
|
|
|
|
|
def build_subscription_batch_writer(
|
|
*,
|
|
repository: SubscriptionStagingPort,
|
|
unit_of_work: AsyncUnitOfWork,
|
|
outbox: AsyncSubscriptionOutboxStager,
|
|
dispatch_store: AsyncOutboxDispatchStore,
|
|
) -> SubscriptionBatchWritePort:
|
|
"""
|
|
组装复用请求级事务且在提交后结算 durable intents 的批量写端口。
|
|
"""
|
|
return SessionSubscriptionBatchWriter(
|
|
repository=repository,
|
|
unit_of_work=unit_of_work,
|
|
outbox=outbox,
|
|
dispatch_store=dispatch_store,
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def rule_group_mutation_scope(
|
|
publish: SystemConfigPublisher,
|
|
) -> Iterator[SyncRuleGroupMutationService]:
|
|
"""
|
|
串行构造共享同步 Session 的规则定义与引用原子服务。
|
|
"""
|
|
_reference_mutation_lock.acquire()
|
|
session = SessionFactory()
|
|
try:
|
|
yield SyncRuleGroupMutationService(
|
|
configuration=SessionSystemConfigurationRepository(session),
|
|
subscriptions=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyUnitOfWork(session),
|
|
publish=publish,
|
|
)
|
|
finally:
|
|
session.close()
|
|
_reference_mutation_lock.release()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def async_rule_group_mutation_scope(
|
|
publish: SystemConfigPublisher,
|
|
) -> AsyncIterator[AsyncRuleGroupMutationService]:
|
|
"""
|
|
非阻塞等待跨入口锁,并构造共享 AsyncSession 的规则原子服务。
|
|
"""
|
|
lock = _reference_mutation_lock
|
|
acquisition = get_task_registry().create(
|
|
asyncio.to_thread(lock.acquire), owner="subscription.reference-lock", cancel_on_shutdown=False,
|
|
)
|
|
try:
|
|
await asyncio.shield(acquisition)
|
|
except asyncio.CancelledError:
|
|
# shield 保留线程的迟到结果,回调负责释放已经没有请求所有者的锁。
|
|
acquisition.add_done_callback(lambda result: lock.release() if result.result() else None)
|
|
raise
|
|
try:
|
|
async with async_session_scope() as session:
|
|
async def publish_async(
|
|
values: Mapping[SystemConfigKey, JsonData],
|
|
) -> None:
|
|
"""
|
|
适配同步快照发布器到异步命令提交后合同。
|
|
"""
|
|
publish(values)
|
|
|
|
yield AsyncRuleGroupMutationService(
|
|
configuration=SessionSystemConfigurationRepository(session),
|
|
subscriptions=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
|
publish=publish_async,
|
|
)
|
|
finally:
|
|
lock.release()
|
|
|
|
|
|
@contextmanager
|
|
def site_reference_mutation_scope(
|
|
publish: SystemConfigPublisher,
|
|
) -> Iterator[SyncSiteReferenceMutationService]:
|
|
"""
|
|
构造共享同步 Session 的 RSS 与订阅站点引用原子服务。
|
|
"""
|
|
_reference_mutation_lock.acquire()
|
|
session = SessionFactory()
|
|
try:
|
|
yield SyncSiteReferenceMutationService(
|
|
configuration=SessionSystemConfigurationRepository(session),
|
|
subscriptions=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyUnitOfWork(session),
|
|
publish=publish,
|
|
)
|
|
finally:
|
|
session.close()
|
|
_reference_mutation_lock.release()
|
|
|
|
|
|
async def _publish_modified(payload: dict[str, Any]) -> None:
|
|
"""
|
|
发布事务已提交的订阅修改事件。
|
|
"""
|
|
await eventmanager.async_send_event(EventType.SubscribeModified, payload)
|
|
|
|
|
|
def _publish_modified_sync(payload: dict[str, Any]) -> None:
|
|
"""
|
|
为同步 Chain 入口发布事务已提交的订阅修改事件。
|
|
"""
|
|
eventmanager.send_event(EventType.SubscribeModified, payload)
|
|
|
|
|
|
async def _publish_deleted(payload: dict[str, Any]) -> None:
|
|
"""
|
|
发布事务已提交的订阅删除事件。
|
|
"""
|
|
await eventmanager.async_send_event(EventType.SubscribeDeleted, payload)
|
|
|
|
|
|
def _publish_deleted_sync(payload: dict[str, Any]) -> None:
|
|
"""
|
|
为同步消息入口发布事务已提交的订阅删除事件。
|
|
"""
|
|
eventmanager.send_event(EventType.SubscribeDeleted, payload)
|
|
|
|
|
|
async def _report_deleted(payload: Mapping[str, JsonData]) -> object:
|
|
"""
|
|
把删除统计端口的只读映射转换为服务端兼容的普通字典。
|
|
"""
|
|
return await MoviePilotServerHelper.async_sub_done_durable(dict(payload))
|
|
|
|
|
|
def _report_deleted_sync(payload: Mapping[str, JsonData]) -> object:
|
|
"""
|
|
把同步删除统计端口的只读映射转换为服务端兼容的普通字典。
|
|
"""
|
|
return MoviePilotServerHelper.sub_done_durable(dict(payload))
|
|
|
|
|
|
def _publish_completed(payload: dict[str, Any]) -> None:
|
|
"""
|
|
发布已提交的订阅完成事件。
|
|
"""
|
|
eventmanager.send_event(EventType.SubscribeComplete, payload)
|
|
|
|
|
|
@contextmanager
|
|
def subscription_completion_scope() -> Iterator[CompleteSubscriptionCommand]:
|
|
"""
|
|
为同步完成链创建独占 Session、UoW 与 durable outbox。
|
|
"""
|
|
session = SessionFactory()
|
|
try:
|
|
yield CompleteSubscriptionCommand(
|
|
repository=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyUnitOfWork(session),
|
|
outbox=SqlAlchemyOutboxStager(session),
|
|
dispatch_store=SqlAlchemyOutboxDispatchStore(SessionFactory),
|
|
publish=_publish_completed,
|
|
)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def subscription_mutation_scope() -> AsyncIterator[SubscriptionMutationService]:
|
|
"""
|
|
为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。
|
|
"""
|
|
async with async_session_scope() as session:
|
|
yield SubscriptionMutationService(
|
|
repository=SessionSubscriptionRepository(session),
|
|
history_repository=SessionSubscriptionHistoryRepository(session),
|
|
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
|
outbox=SqlAlchemyAsyncOutboxStager(session),
|
|
dispatch_store=SqlAlchemyAsyncOutboxDispatchStore(
|
|
async_session_scope
|
|
),
|
|
publish_modified=_publish_modified,
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def sync_subscription_mutation_scope() -> Iterator[SyncSubscriptionMutationService]:
|
|
"""
|
|
为同步 Chain 入口创建独占 Session、UoW 与 durable outbox。
|
|
"""
|
|
session = SessionFactory()
|
|
try:
|
|
yield SyncSubscriptionMutationService(
|
|
repository=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyUnitOfWork(session),
|
|
outbox=SqlAlchemyOutboxStager(session),
|
|
dispatch_store=SqlAlchemyOutboxDispatchStore(SessionFactory),
|
|
publish_modified=_publish_modified_sync,
|
|
)
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
@asynccontextmanager
|
|
async def delete_subscribe_scope() -> AsyncIterator[DeleteSubscribeCommand]:
|
|
"""
|
|
为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。
|
|
"""
|
|
async with async_session_scope() as session:
|
|
yield DeleteSubscribeCommand(
|
|
repository=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
|
publish_deleted=_publish_deleted,
|
|
report_deleted=_report_deleted,
|
|
outbox=SqlAlchemyAsyncOutboxStager(session),
|
|
dispatch_store=SqlAlchemyAsyncOutboxDispatchStore(
|
|
async_session_scope
|
|
),
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def sync_delete_subscribe_scope() -> Iterator[SyncDeleteSubscribeCommand]:
|
|
"""
|
|
为同步消息入口创建独占 Session、UoW 与 durable outbox。
|
|
"""
|
|
session = SessionFactory()
|
|
try:
|
|
yield SyncDeleteSubscribeCommand(
|
|
repository=SessionSubscriptionRepository(session),
|
|
unit_of_work=SqlAlchemyUnitOfWork(session),
|
|
publish_deleted=_publish_deleted_sync,
|
|
report_deleted=_report_deleted_sync,
|
|
outbox=SqlAlchemyOutboxStager(session),
|
|
dispatch_store=SqlAlchemyOutboxDispatchStore(SessionFactory),
|
|
)
|
|
finally:
|
|
session.close()
|