156 lines
5.2 KiB
Python
156 lines
5.2 KiB
Python
|
|
# 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"
|