- Сервис 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>
66 lines
2.2 KiB
Python
66 lines
2.2 KiB
Python
"""api и worker в одном процессе без RabbitMQ и Redis: Redis — fakeredis, отправка в очередь подменена
|
|
доставкой сообщения прямо в WindowPool воркера (tests/unit, tests/browser)."""
|
|
import json
|
|
from contextlib import contextmanager
|
|
|
|
import fakeredis
|
|
from fastapi.testclient import TestClient
|
|
|
|
import main
|
|
from tasks import store as task_store
|
|
from tasks.broker import RUN_TASK
|
|
from worker import windows
|
|
|
|
WORKER_ID = "w1"
|
|
|
|
|
|
class Harness:
|
|
def __init__(self, store, pool, redis_sync):
|
|
self.store = store
|
|
self.pool = pool
|
|
self.redis = redis_sync
|
|
self.messages = []
|
|
self.outcomes = []
|
|
# True — сообщение выполняется сразу при отправке; False — копится в messages до deliver()
|
|
self.auto = True
|
|
self.client: TestClient | None = None
|
|
|
|
async def dispatch(self, message):
|
|
self.messages.append(message)
|
|
if self.auto:
|
|
await self.deliver(message)
|
|
|
|
async def deliver(self, message):
|
|
if message.actor_name == RUN_TASK:
|
|
self.outcomes.append(await self.pool.handle(*message.args))
|
|
else:
|
|
await self.pool.close(*message.args)
|
|
await self.store.heartbeat(WORKER_ID, self.pool.info())
|
|
|
|
def deliver_pending(self):
|
|
pending, self.messages = self.messages, []
|
|
for message in pending:
|
|
self.client.portal.call(self.deliver, message)
|
|
|
|
def heartbeat(self, **info):
|
|
self.redis.set(f"{task_store.PREFIX}:worker:{WORKER_ID}", json.dumps(self.pool.info() | info))
|
|
|
|
def call(self, fn, *args):
|
|
return self.client.portal.call(fn, *args)
|
|
|
|
|
|
@contextmanager
|
|
def run_env(monkeypatch, tmp_path, slots=2):
|
|
server = fakeredis.FakeServer()
|
|
store = task_store.TaskStore(fakeredis.aioredis.FakeRedis(server=server, decode_responses=True))
|
|
pool = windows.WindowPool(store, WORKER_ID, slots=slots)
|
|
harness = Harness(store, pool, fakeredis.FakeRedis(server=server, decode_responses=True))
|
|
monkeypatch.setattr(windows, "USER_DATA_ROOT", tmp_path)
|
|
monkeypatch.setattr(main, "store", store)
|
|
monkeypatch.setattr(main, "dispatch", harness.dispatch)
|
|
harness.heartbeat()
|
|
with TestClient(main.app) as client:
|
|
harness.client = client
|
|
yield harness
|
|
client.portal.call(pool.close_all)
|