032: T059 — ADR-0011 async-backend decision record
This commit is contained in:
55
docs/adr/ADR-0011-async-backend.md
Normal file
55
docs/adr/ADR-0011-async-backend.md
Normal file
@@ -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]
|
||||||
Reference in New Issue
Block a user