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,65 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user