dataloader/tests/integrations/test_worker_protocol.py

156 lines
5.2 KiB
Python
Raw Normal View History

2025-11-05 11:31:19 +01:00
# tests/integrations/test_worker_protocol.py
import asyncio
from datetime import datetime, timedelta, timezone
from uuid import uuid4
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
from dataloader.storage.repositories import QueueRepository, CreateJobRequest
from dataloader.context import APP_CTX
@pytest.mark.anyio
async def test_e2e_worker_protocol_ok(db_session: AsyncSession):
"""
Проверяет полный E2E-сценарий жизненного цикла задачи:
1. Постановка (create)
2. Захват (claim)
3. Пульс (heartbeat)
4. Успешное завершение (finish_ok)
5. Проверка статуса
"""
repo = QueueRepository(db_session)
job_id = str(uuid4())
queue_name = "e2e_ok_queue"
lock_key = f"lock_{job_id}"
# 1. Постановка задачи
create_req = CreateJobRequest(
job_id=job_id,
queue=queue_name,
task="test_e2e_task",
args={},
idempotency_key=None,
lock_key=lock_key,
partition_key="",
priority=100,
available_at=datetime.now(timezone.utc),
max_attempts=3,
lease_ttl_sec=30,
producer=None,
consumer_group=None,
)
await repo.create_or_get(create_req)
# 2. Захват задачи
claimed_job = await repo.claim_one(queue_name, claim_backoff_sec=10)
assert claimed_job is not None
assert claimed_job["job_id"] == job_id
assert claimed_job["lock_key"] == lock_key
# 3. Пульс
success, cancel_requested = await repo.heartbeat(job_id, ttl_sec=60)
assert success
assert not cancel_requested
# 4. Успешное завершение
await repo.finish_ok(job_id)
# 5. Проверка статуса
status = await repo.get_status(job_id)
assert status is not None
assert status.status == "succeeded"
assert status.finished_at is not None
@pytest.mark.anyio
async def test_concurrency_claim_one_locks(db_session: AsyncSession):
"""
Проверяет, что при конкурентном доступе к задачам с одинаковым
lock_key только один воркер может захватить задачу.
"""
repo = QueueRepository(db_session)
queue_name = "concurrency_queue"
lock_key = "concurrent_lock_123"
job_ids = [str(uuid4()), str(uuid4())]
# 1. Создание двух задач с одинаковым lock_key
for i, job_id in enumerate(job_ids):
create_req = CreateJobRequest(
job_id=job_id,
queue=queue_name,
task=f"task_{i}",
args={},
idempotency_key=f"idem_con_{i}",
lock_key=lock_key,
partition_key="",
priority=100 + i,
available_at=datetime.now(timezone.utc),
max_attempts=1,
lease_ttl_sec=30,
producer="test",
consumer_group="test_group",
)
await repo.create_or_get(create_req)
# 2. Первый воркер захватывает задачу
claimed_job_1 = await repo.claim_one(queue_name, claim_backoff_sec=1)
assert claimed_job_1 is not None
assert claimed_job_1["job_id"] == job_ids[0]
# 3. Второй воркер пытается захватить задачу, но не может (из-за advisory lock)
claimed_job_2 = await repo.claim_one(queue_name, claim_backoff_sec=1)
assert claimed_job_2 is None
# 4. Первый воркер освобождает advisory lock (как будто завершил работу)
await repo._advisory_unlock(lock_key)
# 5. Второй воркер теперь может захватить вторую задачу
claimed_job_3 = await repo.claim_one(queue_name, claim_backoff_sec=1)
assert claimed_job_3 is not None
assert claimed_job_3["job_id"] == job_ids[1]
@pytest.mark.anyio
async def test_reaper_requeues_lost_jobs(db_session: AsyncSession):
"""
Проверяет, что reaper корректно возвращает "потерянные" задачи в очередь.
"""
repo = QueueRepository(db_session)
job_id = str(uuid4())
queue_name = "reaper_queue"
# 1. Создаем и захватываем задачу
create_req = CreateJobRequest(
job_id=job_id,
queue=queue_name,
task="reaper_test_task",
args={},
idempotency_key="idem_reaper_1",
lock_key="reaper_lock_1",
partition_key="",
priority=100,
available_at=datetime.now(timezone.utc),
max_attempts=3,
lease_ttl_sec=1, # Очень короткий lease
producer=None,
consumer_group=None,
)
await repo.create_or_get(create_req)
claimed_job = await repo.claim_one(queue_name, claim_backoff_sec=1)
assert claimed_job is not None
assert claimed_job["job_id"] == job_id
# 2. Ждем истечения lease
await asyncio.sleep(2)
# 3. Запускаем reaper
requeued_ids = await repo.requeue_lost()
assert requeued_ids == [job_id]
# 4. Проверяем статус
status = await repo.get_status(job_id)
assert status is not None
assert status.status == "queued"