feature: архитектура v2 — api + очередь + worker, удержание окна браузера (#78)
- Сервис api (FastAPI, без Chrome): POST /v2/tasks, GET /v2/tasks/{id} (доступ по cookie
сессии), DELETE /v2/sessions/current; /v1/solve — прежний контракт поверх очереди.
- Сервис worker (dramatiq + RabbitMQ): слоты браузера, окна сессий между задачами
(browser_idle_timeout 0–120 мин, простой — TTL ключа в Redis), heartbeat.
- Маршрутизация задач сессии в персональную очередь воркера с открытым окном.
- Redis: задачи, результаты (RESULT_TTL_HOURS), окна, воркеры. Прокси и ключ 2captcha
в Redis не сохраняются.
- Движок: AsyncStealthySession.run() — действия на текущей странице без навигации.
- Два образа (robotex-api, robotex-worker), compose с rabbitmq и redis,
make up/down/logs/smoke, релиз собирает все образы.
- Тесты: api + worker в одном процессе (fakeredis), настоящий воркер dramatiq на
StubBroker, браузерные тесты на новой схеме, smoke для запущенного стенда.
- Документация: README, API (v2), Deployment, Usage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fe509c25c3
commit
7b511d5d38
38 files changed
+2142
-545
No files matched your search
@@ -0,0 +1,74 @@
|
||||
import fakeredis
|
||||
import pytest
|
||||
|
||||
from tasks import store as task_store
|
||||
from tasks.store import Status, TaskStore
|
||||
|
||||
pytestmark = pytest.mark.anyio
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store():
|
||||
return TaskStore(fakeredis.aioredis.FakeRedis(decode_responses=True))
|
||||
|
||||
|
||||
async def test_claim_once(store):
|
||||
await store.create("t", "s", "https://example.com")
|
||||
await store.heartbeat("w1", {})
|
||||
assert (await store.get("t"))["status"] == Status.QUEUED
|
||||
assert await store.claim("t", "w1")
|
||||
assert not await store.claim("t", "w2")
|
||||
assert not await store.cancel("t")
|
||||
|
||||
|
||||
async def test_cancelled_task_is_not_claimed(store):
|
||||
await store.create("t", "s", None)
|
||||
assert await store.cancel("t")
|
||||
assert not await store.claim("t", "w1")
|
||||
assert (await store.get("t"))["status"] == Status.CANCELLED
|
||||
|
||||
|
||||
async def test_missing_task_is_not_claimed(store):
|
||||
assert not await store.claim("nope", "w1")
|
||||
|
||||
|
||||
async def test_redelivery_while_running_here_is_skipped(store):
|
||||
await store.create("t", "s", None)
|
||||
await store.heartbeat("w1", {})
|
||||
assert await store.claim("t", "w1")
|
||||
assert not await store.claim("t", "w1", running_here=True)
|
||||
|
||||
|
||||
async def test_task_of_dead_worker_is_reclaimed_limited_times(store):
|
||||
await store.create("t", "s", None)
|
||||
await store.heartbeat("w2", {})
|
||||
assert await store.claim("t", "w1") # w1 умер — heartbeat нет
|
||||
assert await store.claim("t", "w2") # attempts = 2
|
||||
await store.remove_worker("w2")
|
||||
assert not await store.claim("t", "w3")
|
||||
task = await store.get("t")
|
||||
assert task["status"] == Status.FAILED and "worker died" in task["error"]
|
||||
|
||||
|
||||
async def test_task_of_live_worker_is_not_reclaimed(store):
|
||||
await store.create("t", "s", None)
|
||||
await store.heartbeat("w1", {})
|
||||
assert await store.claim("t", "w1")
|
||||
assert not await store.claim("t", "w2")
|
||||
|
||||
|
||||
async def test_finish_sets_result_and_ttl(store):
|
||||
await store.create("t", "s", None)
|
||||
assert await store.redis.ttl("robotex:task:t") == task_store.PENDING_TTL
|
||||
await store.finish("t", {"status": "ok", "actions": []})
|
||||
task = await store.get("t")
|
||||
assert task["status"] == Status.DONE and task["result"] == {"status": "ok", "actions": []}
|
||||
assert await store.redis.ttl("robotex:task:t") == task_store.RESULT_TTL
|
||||
|
||||
|
||||
async def test_live_window_requires_live_worker(store):
|
||||
await store.set_session("s", "w1", 5, 300)
|
||||
assert await store.live_window("s") is None
|
||||
await store.heartbeat("w1", {"browser_slots": 1, "free_slots": 0})
|
||||
assert await store.live_window("s") == {"worker_id": "w1", "idle_timeout": "5"}
|
||||
assert await store.workers() == [{"worker_id": "w1", "browser_slots": 1, "free_slots": 0}]
|
||||
Reference in new issue
Block a user