refactor: make subscription lifecycle events durable

This commit is contained in:
jxxghp
2026-08-22 07:43:41 +08:00
parent 8f94fd620d
commit c5de1c7b1b
27 changed files with 1106 additions and 230 deletions
+50
View File
@@ -8,6 +8,13 @@ from app.application.messaging.chat import (
AsyncAgentChatRepository,
AsyncUnitOfWork,
)
from app.application.outbox import AsyncOutboxTransaction
from app.application.subscription.delete import SubscribeDeletionRepository
from app.application.subscription.identity import SubscribeIdentityDeletionRepository
from app.application.subscription.mutation import (
SubscriptionHistoryMutationRepository,
SubscriptionMutationRepository,
)
class AgentChatRepositoryFactory(Protocol):
@@ -26,6 +33,37 @@ class AsyncUnitOfWorkFactory(Protocol):
...
class AsyncOutboxFactory(Protocol):
"""由请求会话构造异步 outbox 事务端口的工厂。"""
def __call__(self, session: object) -> AsyncOutboxTransaction:
"""绑定请求会话并返回 outbox 暂存与收口端口。"""
...
class SubscriptionRepositoryFactory(Protocol):
"""由请求会话构造订阅写仓储的工厂。"""
def __call__(
self,
session: object,
) -> (
SubscriptionMutationRepository
| SubscribeDeletionRepository
| SubscribeIdentityDeletionRepository
):
"""绑定请求会话并返回订阅领域仓储。"""
...
class SubscriptionHistoryRepositoryFactory(Protocol):
"""由请求会话构造订阅历史写仓储的工厂。"""
def __call__(self, session: object) -> SubscriptionHistoryMutationRepository:
"""绑定请求会话并返回订阅历史仓储。"""
...
class AsyncSessionProvider(Protocol):
"""FastAPI 请求级异步会话提供器。"""
@@ -70,9 +108,21 @@ class AgentChatRuntime:
transaction: AsyncUnitOfWorkFactory
@dataclass(frozen=True, slots=True)
class SubscriptionRuntime:
"""订阅 API 可见的请求级写事务运行时。"""
async_session: AsyncSessionProvider
repository: SubscriptionRepositoryFactory
history_repository: SubscriptionHistoryRepositoryFactory
transaction: AsyncUnitOfWorkFactory
outbox: AsyncOutboxFactory
@dataclass(frozen=True, slots=True)
class HostRuntime:
"""宿主组合根构建且在一个 FastAPI lifespan 内共享的运行时对象。"""
agent_chat: AgentChatRuntime
subscription: SubscriptionRuntime
compatibility_api_data: CompatibilityApiData
+23 -4
View File
@@ -55,7 +55,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 SqlAlchemyOutboxRepository
from app.startup.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
@@ -103,8 +103,11 @@ from app.startup.managed_resources_initializer import (
init_managed_resources,
stop_managed_resources,
)
from app.startup.subscription import TransactionalSubscribeWriter
from app.startup.context import AgentChatRuntime, HostRuntime
from app.startup.subscription import (
TransactionalSubscribeWriter,
configure_transactional_subscription_scopes,
)
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
from app.application.image import configure_wallpaper_providers
@@ -205,7 +208,15 @@ def _build_outbox_dispatcher() -> OutboxDispatcher:
"subscribe.added": lambda message: EventManager().send_event(
EventType.SubscribeAdded,
message.payload,
)
),
"subscribe.modified": lambda message: EventManager().send_event(
EventType.SubscribeModified,
message.payload,
),
"subscribe.deleted": lambda message: EventManager().send_event(
EventType.SubscribeDeleted,
message.payload,
),
},
close=session.close,
)
@@ -455,6 +466,13 @@ async def init_modules() -> HostRuntime:
repository=AgentChatOper,
transaction=SqlAlchemyAsyncUnitOfWork,
),
subscription=SubscriptionRuntime(
async_session=get_async_db,
repository=SubscribeOper,
history_repository=SubscribeHistoryOper,
transaction=SqlAlchemyAsyncUnitOfWork,
outbox=SqlAlchemyAsyncOutboxStager,
),
compatibility_api_data=api_data,
)
configure_api_data_runtime(host_runtime.compatibility_api_data)
@@ -515,6 +533,7 @@ async def init_modules() -> HostRuntime:
async_session=async_session_scope,
)
)
configure_transactional_subscription_scopes()
# 托管资源只在这里装配声明与 adapter,具体资源仍由首个消费者显式激活。
init_managed_resources()
# 应用服务不反向依赖 Chain,由启动组合层注入壁纸来源。
+57 -1
View File
@@ -1,8 +1,9 @@
"""订阅写入事务适配器的启动装配。"""
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager
from contextlib import AbstractAsyncContextManager, asynccontextmanager
from datetime import datetime, timezone
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
@@ -14,12 +15,25 @@ from app.application.subscription.write import (
CreateSubscriptionCommand,
subscription_added_event_key,
)
from app.application.subscription.delete import (
DeleteSubscribeCommand,
configure_delete_subscribe_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.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:
@@ -98,3 +112,45 @@ class TransactionalSubscribeWriter:
username,
delivered,
)
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)
@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.sub_done_async,
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)