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,80 @@
|
||||
"""Сервисы api и worker без браузера: Chrome — FakeSession (окружение — tests/harness.py)."""
|
||||
import pytest
|
||||
|
||||
from api import actions
|
||||
from tests.harness import WORKER_ID, run_env # noqa: F401
|
||||
from worker import windows
|
||||
|
||||
|
||||
class FakeContext:
|
||||
def __init__(self):
|
||||
self.ops = []
|
||||
|
||||
async def cookies(self):
|
||||
return [{"name": "cf_clearance", "value": "secret-cookie"}]
|
||||
|
||||
async def clear_cookies(self):
|
||||
self.ops.append("clear")
|
||||
|
||||
async def add_cookies(self, cookies):
|
||||
self.ops.append(("add", cookies))
|
||||
|
||||
|
||||
class FakePage:
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
||||
async def screenshot(self, type):
|
||||
return b"png"
|
||||
|
||||
async def inner_html(self, selector):
|
||||
return "<body>page</body>"
|
||||
|
||||
async def evaluate(self, js):
|
||||
return "UA/engine"
|
||||
|
||||
|
||||
class FakeSession:
|
||||
"""Вместо AsyncStealthySession: запоминает kwargs и вызовы, page_action выполняет на FakePage."""
|
||||
instances: list["FakeSession"] = []
|
||||
error: Exception | None = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
self.context = FakeContext()
|
||||
self.calls = []
|
||||
self.closed = False
|
||||
FakeSession.instances.append(self)
|
||||
|
||||
async def start(self):
|
||||
pass
|
||||
|
||||
async def close(self):
|
||||
self.closed = True
|
||||
|
||||
async def fetch(self, url, page_action, **kwargs):
|
||||
self.calls.append(("fetch", url))
|
||||
if FakeSession.error:
|
||||
raise FakeSession.error
|
||||
return await page_action(FakePage(self.context))
|
||||
|
||||
async def run(self, page_action, **kwargs):
|
||||
self.calls.append(("run", None))
|
||||
return await page_action(FakePage(self.context))
|
||||
|
||||
|
||||
def fake_execute(result=None, exc=None):
|
||||
async def execute(*args, **kwargs):
|
||||
if exc:
|
||||
raise exc
|
||||
return result if result is not None else []
|
||||
return execute
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def env(monkeypatch, tmp_path):
|
||||
FakeSession.instances, FakeSession.error = [], None
|
||||
monkeypatch.setattr(windows, "AsyncStealthySession", FakeSession)
|
||||
monkeypatch.setattr(actions, "execute", fake_execute())
|
||||
with run_env(monkeypatch, tmp_path) as harness:
|
||||
yield harness
|
||||
Reference in new issue
Block a user