fix(subscription): isolate statistic report failures

This commit is contained in:
jxxghp
2026-08-26 07:01:41 +08:00
parent 5493732d9f
commit df6f86ec3d
9 changed files with 234 additions and 82 deletions
+10 -3
View File
@@ -13,6 +13,7 @@ from app.application.outbox import (
SyncOutboxTransaction,
SyncUnitOfWork,
)
from app.runtime.log import logger
class SubscriptionCompletionRepository(Protocol):
@@ -116,9 +117,15 @@ class CompleteSubscriptionCommand:
self._publish(event_payload)
self._complete_sync_delivery(event_key)
if self._claim_sync_delivery(report_key):
if report(report_payload["subscribe_info"]) is False:
raise RuntimeError("订阅完成统计上报未确认")
self._complete_sync_delivery(report_key)
try:
report_delivered = report(report_payload["subscribe_info"])
except Exception as error:
logger.warning(f"订阅完成统计上报失败,将由后台重试:{error}")
else:
if report_delivered is False:
logger.warning("订阅完成统计上报未确认,将由后台重试")
else:
self._complete_sync_delivery(report_key)
def _claim_sync_delivery(self, event_key: str) -> bool:
"""在同步副作用前取得 lease,已由恢复投递接管时跳过直投。"""
+27 -17
View File
@@ -14,6 +14,7 @@ from app.application.outbox import (
SyncUnitOfWork,
SUBSCRIBE_DELETED_TOPIC,
)
from app.runtime.log import logger
from app.schemas.event import SubscribeDeletedEventData
@@ -153,16 +154,20 @@ class DeleteSubscribeCommand:
)
# 上报适配器会自行白名单过滤公开字段;传完整删除前快照可保留音乐实体维度,
# 避免 Agent 与 API 入口收敛后丢失 music_type / total_tracks。
report_result = self._report_deleted(effects.report_payload)
if inspect.isawaitable(report_result):
report_result = await report_result
if report_result is False:
raise RuntimeError("订阅删除统计上报未确认")
if self._outbox:
await self._outbox.complete_by_event_key(
effects.report_intent.event_key,
datetime.now(timezone.utc),
)
try:
report_result = self._report_deleted(effects.report_payload)
if inspect.isawaitable(report_result):
report_result = await report_result
except Exception as error:
logger.warning(f"订阅删除统计上报失败,将由后台重试:{error}")
else:
if report_result is False:
logger.warning("订阅删除统计上报未确认,将由后台重试")
elif self._outbox:
await self._outbox.complete_by_event_key(
effects.report_intent.event_key,
datetime.now(timezone.utc),
)
return True
@@ -216,13 +221,18 @@ class SyncDeleteSubscribeCommand:
effects.event_intent.event_key,
datetime.now(timezone.utc),
)
if self._report_deleted(effects.report_payload) is False:
raise RuntimeError("订阅删除统计上报未确认")
if self._outbox:
self._outbox.complete_by_event_key(
effects.report_intent.event_key,
datetime.now(timezone.utc),
)
try:
report_result = self._report_deleted(effects.report_payload)
except Exception as error:
logger.warning(f"订阅删除统计上报失败,将由后台重试:{error}")
else:
if report_result is False:
logger.warning("订阅删除统计上报未确认,将由后台重试")
elif self._outbox:
self._outbox.complete_by_event_key(
effects.report_intent.event_key,
datetime.now(timezone.utc),
)
return True
+6 -6
View File
@@ -28,8 +28,8 @@ from app.schemas.types import MUSIC_ENTITY_ALBUM, MediaType
# 而后续按身份去重也会失效,所以必须在查询与建模之前短路
INCOMPLETE_IDENTITY = (0, "媒体身份不完整")
AfterCommitEffect = Callable[[int], None]
AsyncAfterCommitEffect = Callable[[int], Awaitable[None]]
AfterCommitEffect = Callable[[int], bool | None]
AsyncAfterCommitEffect = Callable[[int], Awaitable[bool | None]]
class SubscriptionOutboxStager(Protocol):
@@ -57,7 +57,7 @@ class SubscribeWriter(Protocol):
after_commit: Optional[AfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
) -> Tuple[int, str]:
"""同步新增订阅,并在事务成功后执行外部副作用"""
"""同步新增订阅;提交后回调返回 False 时保留统计 intent 待重试"""
async def async_add(
self,
@@ -67,7 +67,7 @@ class SubscribeWriter(Protocol):
after_commit: Optional[AsyncAfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
) -> Tuple[int, str]:
"""异步新增订阅,并在事务成功后执行外部副作用"""
"""异步新增订阅;提交后回调返回 False 时保留统计 intent 待重试"""
class StagedSubscription(Protocol):
@@ -382,7 +382,7 @@ def add_subscribe(
:param mediainfo: 识别结果
:param subscribe_oper: 复用的订阅操作对象,未传时由启动组合根提供
:param after_commit: 数据提交后执行的消息、事件或上报编排
:param after_commit: 提交后副作用编排;返回 False 表示统计 intent 等待重试
:param kwargs: 订阅设置;owner_scope 为真时按用户名限定查重范围
:return: (订阅 ID, 结果说明)ID 为 0 表示未新增
"""
@@ -420,7 +420,7 @@ async def async_add_subscribe(
:param mediainfo: 识别结果
:param subscribe_oper: 复用的订阅操作对象,未传时由启动组合根提供
:param after_commit: 数据提交后执行的异步消息、事件或上报编排
:param after_commit: 异步提交后副作用编排;返回 False 表示统计 intent 等待重试
:param kwargs: 订阅设置;owner_scope 为真时按用户名限定查重范围
:return: (订阅 ID, 结果说明)ID 为 0 表示未新增
"""
+31 -16
View File
@@ -569,8 +569,8 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
self,
subscribe_id: int,
context: _SubscribePostCommitContext,
) -> None:
"""同步执行提交后消息事件统计,异常不再触碰数据库事务"""
) -> bool:
"""同步执行提交后消息事件统计失败留给 outbox 重试"""
if context.notification:
self.post_message(_SchemaMessage.model_validate(context.notification))
self.eventmanager.send_event(EventType.SubscribeAdded, {
@@ -582,17 +582,23 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
"username": context.username,
"mediainfo": context.mediainfo.to_dict(),
})
if not MoviePilotServerHelper.sub_reg_durable(
self.__subscribe_report_payload(context)
):
raise RuntimeError("订阅新增统计上报未确认")
try:
report_delivered = MoviePilotServerHelper.sub_reg_durable(
self.__subscribe_report_payload(context)
)
except Exception as error:
logger.warning(f"订阅新增统计上报失败,将由后台重试:{error}")
return False
if not report_delivered:
logger.warning("订阅新增统计上报未确认,将由后台重试")
return report_delivered
async def __async_post_subscribe_added(
self,
subscribe_id: int,
context: _SubscribePostCommitContext,
) -> None:
"""异步执行提交后消息事件统计,保持与同步入口相同顺序"""
) -> bool:
"""异步执行提交后消息事件统计失败留给 outbox 重试"""
if context.notification:
await self.async_post_message(
_SchemaMessage.model_validate(context.notification)
@@ -606,10 +612,16 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
"username": context.username,
"mediainfo": context.mediainfo.to_dict(),
})
if not await MoviePilotServerHelper.async_sub_reg_durable(
self.__subscribe_report_payload(context)
):
raise RuntimeError("订阅新增统计上报未确认")
try:
report_delivered = await MoviePilotServerHelper.async_sub_reg_durable(
self.__subscribe_report_payload(context)
)
except Exception as error:
logger.warning(f"订阅新增统计上报失败,将由后台重试:{error}")
return False
if not report_delivered:
logger.warning("订阅新增统计上报未确认,将由后台重试")
return report_delivered
@staticmethod
def __build_subscribe_create_context(
@@ -958,9 +970,9 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
self.__build_subscribe_notification(context),
)
def _after_commit(subscribe_id: int) -> None:
def _after_commit(subscribe_id: int) -> bool:
"""把同步提交后的副作用委托给单一顺序实现。"""
self.__post_subscribe_added(subscribe_id, post_commit_context)
return self.__post_subscribe_added(subscribe_id, post_commit_context)
sid, err_msg = add_subscribe(
mediainfo=context.mediainfo,
@@ -985,9 +997,12 @@ class SubscribeChain(MusicSubscribeMixin, InteractionChainMixin, ChainBase):
self.__build_subscribe_notification(context),
)
async def _after_commit(subscribe_id: int) -> None:
async def _after_commit(subscribe_id: int) -> bool:
"""把异步提交后的副作用委托给单一顺序实现。"""
await self.__async_post_subscribe_added(subscribe_id, post_commit_context)
return await self.__async_post_subscribe_added(
subscribe_id,
post_commit_context,
)
sid, err_msg = await async_add_subscribe(
mediainfo=context.mediainfo,
+14 -12
View File
@@ -58,9 +58,9 @@ class TransactionalSubscribeWriter:
)
def delivered(subscribe_id: int) -> None:
"""执行旧 post-commit 编排,全部成功后收口 durable intent。"""
"""执行提交后编排,分别收口已确认的 durable intent。"""
if after_commit:
after_commit(subscribe_id)
report_delivered = after_commit(subscribe_id)
outbox.complete_by_event_key(
subscription_added_event_key(subscribe_id, payload),
datetime.now(timezone.utc),
@@ -70,10 +70,11 @@ class TransactionalSubscribeWriter:
subscription_added_notification_key(subscribe_id, payload),
datetime.now(timezone.utc),
)
outbox.complete_by_event_key(
subscription_added_report_key(subscribe_id, payload),
datetime.now(timezone.utc),
)
if report_delivered is not False:
outbox.complete_by_event_key(
subscription_added_report_key(subscribe_id, payload),
datetime.now(timezone.utc),
)
return command.execute(
identity,
@@ -103,9 +104,9 @@ class TransactionalSubscribeWriter:
)
async def delivered(subscribe_id: int) -> None:
"""异步执行旧编排,全部成功后收口 durable intent。"""
"""异步执行提交后编排,分别收口已确认的 durable intent。"""
if after_commit:
await after_commit(subscribe_id)
report_delivered = await after_commit(subscribe_id)
await outbox.complete_by_event_key(
subscription_added_event_key(subscribe_id, payload),
datetime.now(timezone.utc),
@@ -115,10 +116,11 @@ class TransactionalSubscribeWriter:
subscription_added_notification_key(subscribe_id, payload),
datetime.now(timezone.utc),
)
await outbox.complete_by_event_key(
subscription_added_report_key(subscribe_id, payload),
datetime.now(timezone.utc),
)
if report_delivered is not False:
await outbox.complete_by_event_key(
subscription_added_report_key(subscribe_id, payload),
datetime.now(timezone.utc),
)
return await command.execute(
identity,