032: T059 — ADR-0011 async-backend decision record

This commit is contained in:
2026-06-04 20:36:53 +03:00
parent 27bbf057ec
commit 5d2813f173

View File

@@ -0,0 +1,55 @@
# [DEF:ADR-0011:ADR]
# @STATUS ACTIVE
# @PURPOSE Документировать архитектурное решение о полном переходе backend на async-first архитектуру: замена синхронного `requests.Session` на `httpx.AsyncClient`, введение глобальных perenv семафоров, 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. **Семафоры**: Глобальный perenv `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)` с fanout на подписчиков.
9. **Lifecycle**: Thread-based → async context manager.
### Конфигурация
10. **Perenv**: `connection_pool_size`, `connection_pool_timeout`, `request_timeout`, `llm_timeout` в `EnvironmentConfig`.
11. **Applevel**: `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]