feat: add memory sync outbox worker

This commit is contained in:
张胜宇
2026-09-11 20:07:55 +08:00
parent f167390a8a
commit 6c605ec3c1
2 changed files with 188 additions and 0 deletions
@@ -0,0 +1,100 @@
from datetime import datetime
import pytest
from app.model.memory import MemorySyncOutbox
from app.worker.memory_sync_outbox_worker import MemorySyncOutboxWorker
class FakeSession:
def __init__(self, event: MemorySyncOutbox | None) -> None:
self.event = event
self.commits = 0
self.rollbacks = 0
async def __aenter__(self) -> "FakeSession":
return self
async def __aexit__(self, *args: object) -> None:
return None
async def scalar(self, statement: object) -> MemorySyncOutbox | None:
del statement
return self.event
async def commit(self) -> None:
self.commits += 1
async def rollback(self) -> None:
self.rollbacks += 1
def event(*, target: str = "neo4j", retry_count: int = 0) -> MemorySyncOutbox:
return MemorySyncOutbox(
id=1, event_uuid="event-1", aggregate_type="profile_snapshot",
aggregate_uuid="profile-1", aggregate_version=1, target_store=target,
operation="upsert", payload={"customer_id": 7}, status="pending",
retry_count=retry_count, next_retry_at=None, last_error=None,
created_at=datetime(2026, 1, 1), processed_at=None,
)
@pytest.mark.asyncio
async def test_success_marks_event_processed() -> None:
item = event()
session = FakeSession(item)
seen: list[dict[str, object]] = []
async def handler(payload: dict[str, object]) -> None:
seen.append(payload)
worker = MemorySyncOutboxWorker({"neo4j": handler}, session_factory=lambda: session)
assert await worker.run_once() is True
assert seen == [{"customer_id": 7}]
assert item.status == "processed"
assert item.processed_at is not None
assert session.commits == 1
@pytest.mark.asyncio
async def test_handler_failure_uses_backoff_and_keeps_event() -> None:
item = event()
session = FakeSession(item)
async def handler(payload: dict[str, object]) -> None:
del payload
raise TimeoutError
worker = MemorySyncOutboxWorker({"neo4j": handler}, session_factory=lambda: session)
assert await worker.run_once() is True
assert item.status == "failed"
assert item.retry_count == 1
assert item.next_retry_at is not None
assert item.last_error == "TimeoutError"
@pytest.mark.asyncio
async def test_fifth_failure_enters_dead_state() -> None:
item = event(retry_count=4)
session = FakeSession(item)
async def handler(payload: dict[str, object]) -> None:
del payload
raise RuntimeError
worker = MemorySyncOutboxWorker({"neo4j": handler}, session_factory=lambda: session)
await worker.run_once()
assert item.status == "dead"
assert item.retry_count == 5
assert item.next_retry_at is None
@pytest.mark.asyncio
async def test_missing_handler_enters_dead_state_without_external_call() -> None:
item = event(target="milvus")
session = FakeSession(item)
worker = MemorySyncOutboxWorker({"neo4j": lambda _: None}, session_factory=lambda: session)
assert await worker.run_once() is True
assert item.status == "dead"
assert item.last_error == "target_handler_not_configured"