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