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,52 @@
|
||||
"""
|
||||
Брокер dramatiq поверх RabbitMQ (#78) и сообщения, которыми api ставит работу воркерам.
|
||||
|
||||
Каждый воркер слушает общую очередь `robotex` и персональную `robotex.worker.<id>`. В персональную
|
||||
api отправляет задачи сессии, чьё окно открыто на этом воркере (#83), иначе — в общую. Сообщение
|
||||
адресуется actor'у по имени, поэтому api не импортирует код воркера (и Chrome ему не нужен).
|
||||
|
||||
`RABBITMQ_URL=stub://` — брокер в памяти для тестов.
|
||||
"""
|
||||
import os
|
||||
|
||||
import dramatiq
|
||||
from dramatiq.brokers.stub import StubBroker
|
||||
from dramatiq.middleware import AsyncIO
|
||||
|
||||
COMMON_QUEUE = "robotex"
|
||||
RUN_TASK = "run_task"
|
||||
CLOSE_WINDOW = "close_window"
|
||||
|
||||
|
||||
def personal_queue(worker_id: str) -> str:
|
||||
return f"{COMMON_QUEUE}.worker.{worker_id}"
|
||||
|
||||
|
||||
def make_broker(url: str) -> dramatiq.Broker:
|
||||
if url.startswith("stub://"):
|
||||
broker = StubBroker()
|
||||
else:
|
||||
from dramatiq.brokers.rabbitmq import RabbitmqBroker
|
||||
# confirm_delivery: enqueue возвращается, только когда RabbitMQ принял сообщение
|
||||
broker = RabbitmqBroker(url=url, confirm_delivery=True)
|
||||
broker.add_middleware(AsyncIO())
|
||||
return broker
|
||||
|
||||
|
||||
broker = make_broker(os.getenv("RABBITMQ_URL", "amqp://guest:guest@localhost:5672/"))
|
||||
dramatiq.set_broker(broker)
|
||||
|
||||
|
||||
def task_message(
|
||||
task_id: str, session_id: str, payload: dict, idle_timeout: int, queue: str = COMMON_QUEUE,
|
||||
) -> dramatiq.Message:
|
||||
"""payload — SolveRequest/TaskRequest.model_dump(): в нём прокси и ключ 2captcha, поэтому
|
||||
он живёт только в сообщении, а не в Redis."""
|
||||
return dramatiq.Message(
|
||||
queue_name=queue, actor_name=RUN_TASK,
|
||||
args=(task_id, session_id, payload, idle_timeout), kwargs={}, options={},
|
||||
)
|
||||
|
||||
|
||||
def close_message(session_id: str, queue: str) -> dramatiq.Message:
|
||||
return dramatiq.Message(queue_name=queue, actor_name=CLOSE_WINDOW, args=(session_id,), kwargs={}, options={})
|
||||
Reference in new issue
Block a user