- Postgres в compose; пакет db (SQLAlchemy async): clients, windows, requests. - Все эндпоинты, кроме /health, — только с ключом в X-API-Key, иначе 401; без выпущенных ключей доступа нет. В базе — только sha256 ключа. - Задача и окно сессии привязаны к клиенту: чужой ключ их не видит. - Воркер пишет журнал: окно (открыто, последнее обращение, закрыто) и задачу (в очереди, начало, конец, итог, данные без секретов). Сбой записи не роняет задачу. - python -m db.cli create|list|revoke|stats; make key-create/key-list/key-revoke/stats. - smoke и loadtest с ключом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
54 lines
2.2 KiB
Python
54 lines
2.2 KiB
Python
"""
|
||
Брокер 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,
|
||
meta: dict | None = None,
|
||
) -> dramatiq.Message:
|
||
"""payload — SolveRequest/TaskRequest.model_dump(): в нём прокси и ключ 2captcha, поэтому
|
||
он живёт только в сообщении, а не в Redis. meta — для журнала: client_id, api (v1/v2)."""
|
||
return dramatiq.Message(
|
||
queue_name=queue, actor_name=RUN_TASK,
|
||
args=(task_id, session_id, payload, idle_timeout, meta or {}), 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={})
|