dataloader/tests/integrations/test_worker_protocol.py

156 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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"