feat: make subscription notifications durable

This commit is contained in:
jxxghp
2026-08-23 02:35:16 +08:00
parent e99289a07b
commit ca66b39b7e
8 changed files with 234 additions and 54 deletions
+28 -1
View File
@@ -49,8 +49,9 @@ class CompleteSubscriptionCommand:
mediainfo: Mapping[str, Any],
notify: CompletionEffect,
report: CompletionReporter,
notification: Mapping[str, Any] | None = None,
) -> None:
"""在同一事务中写历史、删订阅并暂存完成事件与统计意图。"""
"""在同一事务中写历史、删订阅并暂存完成事件、通知与统计意图。"""
info = dict(subscribe_info)
event_payload = {
"subscribe_id": subscribe_id,
@@ -60,6 +61,7 @@ class CompleteSubscriptionCommand:
}
event_key = event_payload["idempotency_key"]
report_key = completion_report_key(subscribe_id, info)
notification_key = completion_notification_key(subscribe_id, info)
report_payload = {"subscribe_info": _completion_report_payload(info, report_key)}
try:
self._repository.add_history(**info)
@@ -74,6 +76,18 @@ class CompleteSubscriptionCommand:
),
now,
)
if notification:
self._outbox.stage(
OutboxIntent(
event_key=notification_key,
topic="subscribe.complete.notification",
payload={
"idempotency_key": notification_key,
"message": dict(notification),
},
),
now,
)
self._outbox.stage(
OutboxIntent(
event_key=report_key,
@@ -88,6 +102,11 @@ class CompleteSubscriptionCommand:
raise
notify()
if self._outbox and notification:
self._outbox.complete_by_event_key(
notification_key,
datetime.now(timezone.utc),
)
self._publish(event_payload)
if self._outbox:
self._outbox.complete_by_event_key(event_key, datetime.now(timezone.utc))
@@ -111,6 +130,14 @@ def completion_report_key(subscribe_id: int, subscribe_info: Mapping[str, Any])
return f"{completion_event_key(subscribe_id, subscribe_info)}:report"
def completion_notification_key(
subscribe_id: int,
subscribe_info: Mapping[str, Any],
) -> str:
"""构造订阅完成通知的稳定幂等键,避免恢复时重复生成不同消息。"""
return f"{completion_event_key(subscribe_id, subscribe_info)}:notification"
def _completion_report_payload(
subscribe_info: Mapping[str, Any],
report_key: str,
+52 -7
View File
@@ -17,7 +17,7 @@ app/application/history.py 里整理历史的写入路径同构。
from collections.abc import Awaitable, Callable
from datetime import datetime, timezone
from typing import Optional, Protocol, Tuple
from typing import Mapping, Optional, Protocol, Tuple
from app.application.outbox import OutboxIntent
from app.domain.context import MediaInfo, MusicInfo
@@ -55,6 +55,7 @@ class SubscribeWriter(Protocol):
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
) -> Tuple[int, str]:
"""同步新增订阅,并在事务成功后执行外部副作用。"""
@@ -64,6 +65,7 @@ class SubscribeWriter(Protocol):
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AsyncAfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
) -> Tuple[int, str]:
"""异步新增订阅,并在事务成功后执行外部副作用。"""
@@ -153,6 +155,7 @@ class CreateSubscriptionCommand:
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
) -> Tuple[int, str]:
"""执行同步新增;事务失败回滚,提交后副作用失败不反向回滚。"""
try:
@@ -164,6 +167,7 @@ class CreateSubscriptionCommand:
staged.subscribe_id,
payload,
username,
notification,
):
self._outbox.stage(intent, now)
self._unit_of_work.commit()
@@ -195,6 +199,7 @@ class AsyncCreateSubscriptionCommand:
payload: dict,
username: Optional[str] = None,
after_commit: Optional[AsyncAfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
) -> Tuple[int, str]:
"""执行异步新增;事务失败回滚,提交后副作用失败不反向回滚。"""
try:
@@ -210,6 +215,7 @@ class AsyncCreateSubscriptionCommand:
staged.subscribe_id,
payload,
username,
notification,
):
await self._outbox.stage(intent, now)
await self._unit_of_work.commit()
@@ -225,26 +231,44 @@ def _subscribe_added_intents(
subscribe_id: int,
payload: dict,
username: str | None,
) -> tuple[OutboxIntent, OutboxIntent]:
"""构造订阅新增事件与外部统计的同事务 durable intents。"""
notification: Mapping[str, object] | None = None,
) -> tuple[OutboxIntent, ...]:
"""构造订阅新增事件、通知与外部统计的同事务 durable intents。"""
event_key = subscription_added_event_key(subscribe_id, payload)
event_payload = {
"subscribe_id": subscribe_id,
"username": username,
"mediainfo": dict(payload),
}
return (
intents: list[OutboxIntent] = [
OutboxIntent(
event_key=event_key,
topic="subscribe.added",
payload=event_payload,
),
]
if notification:
intents.append(
OutboxIntent(
event_key=subscription_added_notification_key(subscribe_id, payload),
topic="subscribe.added.notification",
payload={
"idempotency_key": subscription_added_notification_key(
subscribe_id,
payload,
),
"message": dict(notification),
},
)
)
intents.append(
OutboxIntent(
event_key=subscription_added_report_key(subscribe_id, payload),
topic="subscribe.added.report",
payload={"subscribe_info": dict(payload)},
),
)
)
return tuple(intents)
def subscription_added_event_key(subscribe_id: int, payload: dict) -> str:
@@ -261,6 +285,11 @@ def subscription_added_report_key(subscribe_id: int, payload: dict) -> str:
return f"{subscription_added_event_key(subscribe_id, payload)}:report"
def subscription_added_notification_key(subscribe_id: int, payload: dict) -> str:
"""构造订阅新增通知的稳定幂等键。"""
return f"{subscription_added_event_key(subscribe_id, payload)}:notification"
_configured_subscribe_writer: Callable[[], SubscribeWriter] | None = None
@@ -345,6 +374,7 @@ def add_subscribe(
mediainfo: MediaInfo | MusicInfo,
subscribe_oper: Optional[SubscribeWriter] = None,
after_commit: Optional[AfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
**kwargs,
) -> Tuple[int, str]:
"""
@@ -361,13 +391,20 @@ def add_subscribe(
return INCOMPLETE_IDENTITY
identity, payload, username = translated
oper = _get_subscribe_writer(subscribe_oper)
extra = {"notification": notification} if notification is not None else {}
if after_commit is None:
return oper.add(identity=identity, payload=payload, username=username)
return oper.add(
identity=identity,
payload=payload,
username=username,
**extra,
)
return oper.add(
identity=identity,
payload=payload,
username=username,
after_commit=after_commit,
**extra,
)
@@ -375,6 +412,7 @@ async def async_add_subscribe(
mediainfo: MediaInfo | MusicInfo,
subscribe_oper: Optional[SubscribeWriter] = None,
after_commit: Optional[AsyncAfterCommitEffect] = None,
notification: Mapping[str, object] | None = None,
**kwargs,
) -> Tuple[int, str]:
"""
@@ -391,13 +429,20 @@ async def async_add_subscribe(
return INCOMPLETE_IDENTITY
identity, payload, username = translated
oper = _get_subscribe_writer(subscribe_oper)
extra = {"notification": notification} if notification is not None else {}
if after_commit is None:
return await oper.async_add(identity=identity, payload=payload, username=username)
return await oper.async_add(
identity=identity,
payload=payload,
username=username,
**extra,
)
return await oper.async_add(
identity=identity,
payload=payload,
username=username,
after_commit=after_commit,
**extra,
)