feat: add memory sync outbox worker
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""画像投影 Outbox 消费器。
|
||||
|
||||
外部存储通过 handler 注入;本模块只负责领取、重试、死信和 MySQL 状态更新。
|
||||
"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.infrastructure.db import SessionFactory
|
||||
from app.model.memory import MemorySyncOutbox
|
||||
|
||||
ProjectionHandler = Callable[[dict[str, Any]], Awaitable[Any]]
|
||||
MAX_RETRY_COUNT = 5
|
||||
|
||||
|
||||
class MemorySyncOutboxWorker:
|
||||
"""按目标存储独立消费画像投影事件。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
handlers: dict[str, ProjectionHandler],
|
||||
*,
|
||||
session_factory: Callable[[], AsyncSession] = SessionFactory,
|
||||
) -> None:
|
||||
self.handlers = handlers
|
||||
self.session_factory = session_factory
|
||||
|
||||
async def run_once(self, *, target_store: str | None = None) -> bool:
|
||||
"""领取并处理一条到期事件;没有可处理事件时返回 False。"""
|
||||
if not self.handlers:
|
||||
return False
|
||||
async with self.session_factory() as session:
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
conditions: list[Any] = [
|
||||
MemorySyncOutbox.status.in_({"pending", "failed"}),
|
||||
MemorySyncOutbox.next_retry_at.is_(None)
|
||||
| (MemorySyncOutbox.next_retry_at <= now),
|
||||
]
|
||||
if target_store is not None:
|
||||
conditions.append(MemorySyncOutbox.target_store == target_store)
|
||||
event = await session.scalar(
|
||||
select(MemorySyncOutbox)
|
||||
.where(*conditions)
|
||||
.order_by(MemorySyncOutbox.id)
|
||||
.limit(1)
|
||||
.with_for_update(skip_locked=True)
|
||||
)
|
||||
if event is None:
|
||||
await session.rollback()
|
||||
return False
|
||||
handler = self.handlers.get(event.target_store)
|
||||
if handler is None:
|
||||
self._fail(event, "target_handler_not_configured", now, dead=True)
|
||||
await session.commit()
|
||||
return True
|
||||
try:
|
||||
await handler(event.payload)
|
||||
except Exception as exc:
|
||||
self._fail(event, type(exc).__name__, now)
|
||||
else:
|
||||
event.status = "processed"
|
||||
event.processed_at = datetime.now(UTC).replace(tzinfo=None)
|
||||
event.last_error = None
|
||||
event.next_retry_at = None
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _fail(
|
||||
event: MemorySyncOutbox,
|
||||
reason: str,
|
||||
now: datetime,
|
||||
*,
|
||||
dead: bool = False,
|
||||
) -> None:
|
||||
"""写入可重试失败或死信状态,不吞掉失败事实。"""
|
||||
event.retry_count = int(event.retry_count) + 1
|
||||
event.last_error = reason[:500]
|
||||
event.status = "dead" if dead or event.retry_count >= MAX_RETRY_COUNT else "failed"
|
||||
event.next_retry_at = (
|
||||
None
|
||||
if event.status == "dead"
|
||||
else now + timedelta(seconds=min(300, 2 ** int(event.retry_count)))
|
||||
)
|
||||
Reference in New Issue
Block a user