diff --git a/docs/adr/ADR-0011-async-backend.md b/docs/adr/ADR-0011-async-backend.md new file mode 100644 index 00000000..ac5fd068 --- /dev/null +++ b/docs/adr/ADR-0011-async-backend.md @@ -0,0 +1,55 @@ +# [DEF:ADR-0011:ADR] +# @STATUS ACTIVE +# @PURPOSE Документировать архитектурное решение о полном переходе backend на async-first архитектуру: замена синхронного `requests.Session` на `httpx.AsyncClient`, введение глобальных per‑env семафоров, bounded executors и полный переход Task Manager на asyncio. +# @RATIONALE ss-tools одновременно взаимодействует с несколькими внешними системами (Superset REST API, LLM API, SMTP, Telegram, Slack) и выполняет блокирующие операции (Git, файловый I/O, SQLAlchemy). При одном воркере uvicorn синхронные блокирующие вызовы останавливают все входящие HTTP-запросы. Единственный способ обеспечить конкурентность — сделать весь I/O неблокирующим. +# @REJECTED aiohttp — отвергнут в пользу httpx, который уже был в зависимостях (0.28.1) и имеет лучший API для async HTTP. +# @REJECTED Полный переход на async SQLAlchemy — потребовал бы переписывания всех моделей и репозиториев, что выходит за рамки этой фичи. `run_blocking` с bounded executor достаточен. +# @REJECTED Dual-stack (sync + async) — потребовал бы поддержки двух транспортов, увеличивая surface для ошибок и усложняя код. +# @REJECTED Wrapper-only (`asyncio.to_thread` на всё) — rejected, т.к. `asyncio.to_thread` не принимает executor и использует default пул без backpressure. +# @REJECTED Multiprocessing / `--workers N` — rejected, т.к. увеличение воркеров не решает проблему блокировки внутри одного воркера и увеличивает потребление памяти. + +## Decision + +Весь backend переводится на async-first архитектуру: + +### Transport Layer + +1. **HTTP-клиент**: `requests.Session` → `httpx.AsyncClient` через класс `AsyncAPIClient` в `core/utils/async_network.py`. +2. **SupersetClient**: sync + AsyncSupersetClient объединены в единый асинхронный клиент. Все 13 миксинов — `async def`. +3. **Семафоры**: Глобальный per‑env `asyncio.Semaphore` через синглтон `SupersetClientRegistry` (`core/utils/client_registry.py`). При исчерпании — таймаут 30s → 503. +4. **LLM-клиент**: Унифицированный `LLMHttpClient` в `plugins/translate/_llm_async_http.py` на `httpx.AsyncClient`. Rate-limit backoff через `asyncio.sleep`. + +### Blocking Operations + +5. **Bounded executors**: Именованные `ThreadPoolExecutor` (db, file, git) через helper `run_blocking(kind, fn)` в `core/utils/executors.py`. Вместо `asyncio.to_thread`. +6. **Graceful shutdown**: Все клиенты закрываются при shutdown через `SupersetClientRegistry.shutdown()`. Незавершённые задачи помечаются CANCELLED. + +### Task Manager + +7. **Запуск задач**: `ThreadPoolExecutor` → `asyncio.create_task`. Задачи разделяют основной event loop. +8. **EventBus**: `queue.Queue` + `call_soon_threadsafe` → `asyncio.Queue(maxsize=10000)` с fan‑out на подписчиков. +9. **Lifecycle**: Thread-based → async context manager. + +### Конфигурация + +10. **Per‑env**: `connection_pool_size`, `connection_pool_timeout`, `request_timeout`, `llm_timeout` в `EnvironmentConfig`. +11. **App‑level**: `db_executor_workers`, `file_executor_workers`, `git_executor_workers`, `graceful_shutdown_timeout` в `AppAsyncRuntimeConfig`. + +### Тестирование + +12. `requests_mock` → `pytest-httpx` для мокирования асинхронных HTTP-вызовов. +13. Тесты конкурентности с `asyncio.gather` для всех C3+ контрактов. +14. Rejected-path regression тесты для каждого запрещённого паттерна. + +### Модули, затронутые рефакторингом + +- `core/utils/`: `async_network.py` (+semaphore), `client_registry.py` (new), `executors.py` (new), `fileio.py` (+async wrappers), `network.py` → Tombstone, `superset_compilation_adapter.py` → async +- `core/superset_client/`: `_base.py` (+13 mixins) → все async +- `core/task_manager/`: `manager.py`, `event_bus.py`, `lifecycle.py` → все async +- `api/routes/`: dashboards, assistant, migration, datasets, git, environments, profile → все async +- `plugins/translate/`: `_llm_async_http.py` (new), все файлы → async +- `plugins/`: `backup.py`, `git_plugin.py`, `llm_analysis/plugin.py`, `storage/plugin.py` → async +- `services/`: `notifications/providers.py`, `notifications/service.py`, `git/_base.py` → async +- `services/`: `git/_base.py`, `git/_branch.py`, `git/_sync.py`, `git/_status.py`, `git/_merge.py` → async + +# [/DEF:ADR-0011:ADR]