"""F6 — ReliableQueue: atomic dequeue + recovery + retry.""" import json import sys from pathlib import Path import fakeredis.aioredis import pytest # Make `_shared` importable when tests run from services/_shared sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent)) from _shared.reliable_queue import ReliableQueue @pytest.fixture async def redis(): r = fakeredis.aioredis.FakeRedis(decode_responses=False) yield r await r.flushall() await r.aclose() async def test_dequeue_atomically_moves_to_processing(redis): """BLMOVE: queue → processing 원자적 이동.""" q = ReliableQueue(redis, queue_key="queue:test", worker_id="w1") await redis.lpush("queue:test", json.dumps({"task_id": "t1"}).encode()) result = await q.dequeue(timeout=1) assert result is not None payload, raw = result assert payload["task_id"] == "t1" assert await redis.llen("queue:test") == 0 assert await redis.llen("processing:queue:test:w1") == 1 async def test_dequeue_returns_none_on_timeout(redis): q = ReliableQueue(redis, queue_key="queue:test", worker_id="w1") result = await q.dequeue(timeout=1) assert result is None async def test_ack_removes_from_processing(redis): q = ReliableQueue(redis, queue_key="queue:test", worker_id="w1") await redis.lpush("queue:test", json.dumps({"task_id": "t1"}).encode()) _, raw = await q.dequeue(timeout=1) await q.ack(raw) assert await redis.llen("processing:queue:test:w1") == 0 async def test_recover_returns_orphaned_to_main_queue(redis): """startup recovery: 잔존 processing list 항목을 main queue로 되돌림.""" orphan = json.dumps({"task_id": "t1", "attempts": 0}).encode() await redis.lpush("processing:queue:test:w1", orphan) q = ReliableQueue(redis, queue_key="queue:test", worker_id="w1") recovered = await q.recover() assert recovered == 1 assert await redis.llen("processing:queue:test:w1") == 0 payload, _ = await q.dequeue(timeout=1) assert payload["task_id"] == "t1" assert payload["attempts"] == 1 # incremented on recover async def test_fail_below_max_attempts_returns_to_main_queue(redis): q = ReliableQueue(redis, queue_key="queue:test", worker_id="w1", max_attempts=3) await redis.lpush("queue:test", json.dumps({"task_id": "t1", "attempts": 0}).encode()) payload, raw = await q.dequeue(timeout=1) await q.fail(raw, payload) assert await redis.llen("processing:queue:test:w1") == 0 assert await redis.llen("queue:test") == 1 requeued_raw = await redis.lindex("queue:test", 0) requeued = json.loads(requeued_raw) assert requeued["attempts"] == 1 async def test_fail_at_max_attempts_moves_to_dead_letter(redis): q = ReliableQueue(redis, queue_key="queue:test", worker_id="w1", max_attempts=3) await redis.lpush( "queue:test", json.dumps({"task_id": "t1", "attempts": 2}).encode() ) payload, raw = await q.dequeue(timeout=1) await q.fail(raw, payload) # attempts 2 → 3 (== max) → dead-letter assert await redis.llen("queue:test") == 0 assert await redis.llen("processing:queue:test:w1") == 0 assert await redis.llen("dead_letter:queue:test") == 1