- 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>
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
"""api и worker в одном процессе без RabbitMQ и Redis: Redis — fakeredis, отправка в очередь подменена
|
|
доставкой сообщения прямо в WindowPool воркера (tests/unit, tests/browser)."""
|
|
import asyncio
|
|
import json
|
|
from contextlib import contextmanager
|
|
|
|
import fakeredis
|
|
from fastapi.testclient import TestClient
|
|
|
|
import main
|
|
from db import clients
|
|
from db.engine import connect as connect_db
|
|
from db.journal import Journal
|
|
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)
|
|
|
|
|
|
def make_client_key(db, name="test") -> str:
|
|
async def create():
|
|
await db.init()
|
|
return (await clients.create_client(db, name))[1]
|
|
key = asyncio.run(create())
|
|
asyncio.run(db.close()) # соединения sqlite привязаны к циклу asyncio.run — закрываем, приложение откроет свои
|
|
return key
|
|
|
|
|
|
@contextmanager
|
|
def run_env(monkeypatch, tmp_path, slots=2):
|
|
server = fakeredis.FakeServer()
|
|
store = task_store.TaskStore(fakeredis.aioredis.FakeRedis(server=server, decode_responses=True))
|
|
db = connect_db(f"sqlite+aiosqlite:///{tmp_path / 'robotex.db'}")
|
|
key = make_client_key(db)
|
|
pool = windows.WindowPool(store, WORKER_ID, slots=slots, journal=Journal(db))
|
|
harness = Harness(store, pool, fakeredis.FakeRedis(server=server, decode_responses=True))
|
|
harness.db, harness.api_key = db, key
|
|
# профили — в отдельном каталоге: в tmp_path ещё и файл базы
|
|
profiles = tmp_path / "profiles"
|
|
profiles.mkdir()
|
|
harness.profiles = profiles
|
|
monkeypatch.setattr(windows, "USER_DATA_ROOT", profiles)
|
|
monkeypatch.setattr(main, "store", store)
|
|
monkeypatch.setattr(main, "database", db)
|
|
monkeypatch.setattr(main, "dispatch", harness.dispatch)
|
|
harness.heartbeat()
|
|
with TestClient(main.app, headers={"X-API-Key": key}) as client:
|
|
harness.client = client
|
|
yield harness
|
|
client.portal.call(pool.close_all)
|