Updated status in spec.md, plan.md, tasks.md, research.md, data-model.md, ux_reference.md, quickstart.md. All 72 tasks completed. 34/34 tests passing.
292 lines
20 KiB
Markdown
292 lines
20 KiB
Markdown
# Research: Async Backend Refactoring
|
||
|
||
**Feature**: `032-translate-requests-httpx`
|
||
**Date**: 2026-06-04
|
||
**Status**: ✅ Implemented — all decisions applied
|
||
|
||
## R1: APIClient Migration — requests.Session → httpx.AsyncClient
|
||
|
||
### Decision
|
||
|
||
Расширить существующий `AsyncAPIClient` в `backend/src/core/utils/async_network.py` — добавить семафор `asyncio.Semaphore(connection_pool_size)`, connection pool config и поддержку параметров из `EnvironmentConfig`. Новый файл `async_client.py` **не создаётся** — `async_network.py` остаётся каноническим путём. `SupersetAuthCache` (общий для sync/async) и пагинированные хелперы уже существуют и переиспользуются.
|
||
|
||
### Rationale
|
||
|
||
- `httpx==0.28.1` уже в `requirements.txt`. Никаких новых зависимостей для базового HTTP.
|
||
- `httpx.AsyncClient` поддерживает connection pooling (keepalive) и таймауты на уровне клиента.
|
||
- `async_network.py` уже содержит `AsyncAPIClient` с auth‑cache и error translation. Расширение существующего кода устраняет дублирование.
|
||
- API-поверхность методов `APIClient.request()` остаётся похожей, что упрощает миграцию миксинов.
|
||
|
||
### Alternatives Considered
|
||
|
||
- **aiohttp**: Отвергнут — httpx уже в зависимостях, имеет лучший API и поддержку sync/async dual-mode.
|
||
- **Оставить `requests` + `asyncio.to_thread`**: Отвергнут — создаёт тред на каждый запрос, неэффективно при высокой нагрузке.
|
||
- **`httpx.Client` (sync mode)**: Отвергнут — это просто замена requests на httpx без решения проблемы блокировки event loop'а.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Новый контракт: `AsyncAPIClient` [C:4] — stateful (connection pool, cookie jar, CSRF token).
|
||
- Контракты на `auth.py` [C:3] и `paginated.py` [C:3] извлекаются из старого `network.py`.
|
||
- Все 13 миксинов `SupersetClient` обновляют `@RELATION DEPENDS_ON -> [AsyncAPIClient]`.
|
||
|
||
---
|
||
|
||
## R2: SupersetClient Merge — Sync + AsyncSupersetClient → Unified Async
|
||
|
||
### Decision
|
||
|
||
Синхронный `SupersetClient` (`_base.py`) и `AsyncSupersetClient` (`async_superset_client.py`) сливаются в единый `SupersetClient` с полностью асинхронными методами. `AsyncSupersetClient` удаляется. Все 13 миксинов становятся асинхронными. Старый синхронный код удаляется полностью (big-bang).
|
||
|
||
### Rationale
|
||
|
||
- Big-bang выбран на этапе clarification — не требует поддержки dual-stack.
|
||
- `AsyncSupersetClient` уже реализует асинхронные версии некоторых методов (`get_dashboards`, `get_datasets`, `export_dashboard`) — их сигнатуры становятся целевыми для объединённого клиента.
|
||
- Удаление sync-кода исключает риск случайного использования блокирующего клиента в будущем.
|
||
|
||
### Alternatives Considered
|
||
|
||
- **Dual-stack (sync + async)**: Отвергнут на этапе clarification.
|
||
- **Wrapper-only (asyncio.to_thread на всё)**: Отвергнут — избыточные треды, не решает проблему connection pooling'а.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракт `SupersetClient` [C:4] обновляется: все методы → `async def`, @SIDE_EFFECT — HTTP-вызовы, @PRE — валидный env_config.
|
||
- `AsyncSupersetClient` → Tombstone с @DEPRECATED + @REPLACED_BY -> [SupersetClient].
|
||
- 13 файлов миксинов требуют обновления сигнатур.
|
||
|
||
---
|
||
|
||
## R3: Task Manager Asyncio Migration
|
||
|
||
### Decision
|
||
|
||
`ThreadPoolExecutor` в `TaskManager` заменяется на `asyncio.create_task`. Все фоновые задачи выполняются в основном event loop'е. `EventBus` для WebSocket-логирования становится асинхронным (использует `asyncio.Queue` вместо `queue.Queue`). `TaskLifecycle` переходит на async-контекстные менеджеры.
|
||
|
||
### Rationale
|
||
|
||
- Выбрано на этапе clarification (Option B).
|
||
- `asyncio.create_task` не создаёт дополнительных тредов, задачи разделяют ресурсы event loop'а.
|
||
- Упрощает отмену задач: `task.cancel()` + `CancelledError` cascade.
|
||
- WebSocket-логирование через `asyncio.Queue` интегрируется нативно.
|
||
|
||
### Alternatives Considered
|
||
|
||
- **Per-task event loop (`asyncio.run`)**: Отвергнут — создаёт изоляцию между задачами, усложняет общую отмену.
|
||
- **Hybrid (лёгкие на create_task, тяжёлые на ThreadPoolExecutor)**: Отвергнут — усложняет архитектуру без выигрыша, т.к. все тяжёлые операции теперь асинхронны.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракт `TaskManager` [C:5] — @INVARIANT: задачи не блокируют event loop, @DATA_CONTRACT: Input: TaskParams → Output: TaskResult.
|
||
- Контракт `EventBus` [C:4] обновляется: `queue.Queue` → `asyncio.Queue`, `call_soon_threadsafe` удаляется.
|
||
- Контракт `TaskLifecycle` [C:4] обновляется: async context managers.
|
||
|
||
---
|
||
|
||
## R4: Connection Pool / Semaphore Design
|
||
|
||
### Decision
|
||
|
||
`asyncio.Semaphore(20)` на каждое окружение Superset. Семафор создаётся при инициализации `AsyncAPIClient` для конкретного `env_id`. Все операции (экспорт, SQL Lab, chart data) конкурируют за один пул. При исчерпании — `asyncio.wait_for(semaphore.acquire(), timeout=30)`. Таймаут и размер пула настраиваются через конфигурацию окружения.
|
||
|
||
### Rationale
|
||
|
||
- Выбрано на этапе clarification (Option B — semaphore per env, wait with timeout).
|
||
- `asyncio.Semaphore` — это built-in, не требует внешних зависимостей.
|
||
- Конфигурация на уровне окружения позволяет задать разные лимиты для dev/staging/prod Superset.
|
||
- Таймаут 30 секунд даёт достаточно времени для освобождения слота при типичных операциях (экспорт дашборда — 2-5 секунд).
|
||
|
||
### Alternatives Considered
|
||
|
||
- **Без ограничений**: Отвергнут — risk of overwhelming Superset.
|
||
- **Семафоры по типам операций**: Отвергнут — избыточная сложность для текущего масштаба.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракт `AsyncAPIClient` [C:4] добавляет @INVARIANT: количество одновременных запросов ≤ semaphore_size.
|
||
- Новый edge case: `503 Service Unavailable` при таймауте семафора.
|
||
- Тесты на семафор: `test_semaphore_timeout_returns_503`, `test_semaphore_releases_after_completion`.
|
||
|
||
---
|
||
|
||
## R5: Test Mock Migration — requests_mock → pytest_httpx
|
||
|
||
### Decision
|
||
|
||
Все тесты, использующие `requests_mock`, переводятся на `pytest-httpx`. `pytest-httpx` предоставляет фикстуру `httpx_mock`, которая перехватывает вызовы `httpx.AsyncClient` на транспортном уровне. Логика assertions (проверка URL, method, payload, response) сохраняется.
|
||
|
||
### Rationale
|
||
|
||
- Выбрано на этапе clarification (Option A).
|
||
- `pytest-httpx` — de-facto стандарт для тестирования httpx в pytest.
|
||
- Минимальные изменения в assertions: `httpx_mock.add_response(url=..., method="GET", json=...)` — синтаксис похож на `requests_mock`.
|
||
|
||
### Alternatives Considered
|
||
|
||
- **respx**: Отвергнут — менее распространён в pytest-экосистеме.
|
||
- **Встроенный `httpx.MockTransport`**: Отвергнут — больше boilerplate.
|
||
- **Сохранить `requests_mock` + врапперы**: Отвергнут — костыль, тесты не проверяют реальное асинхронное поведение.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Новая dev-зависимость: `pytest-httpx` в `requirements-dev.txt` или `pyproject.toml`.
|
||
- Обновление `conftest.py`: глобальная фикстура `httpx_mock`.
|
||
- Все тестовые файлы, использующие `requests_mock`, обновляются.
|
||
|
||
---
|
||
|
||
## R6: LLM Client Migration — requests → httpx.AsyncClient
|
||
|
||
### Decision
|
||
|
||
Два файла LLM-клиентов (`_llm_http.py` и `preview_llm_client.py`) переводятся на `httpx.AsyncClient`. Функции `call_openai_compatible()` становятся `async def`. Rate-limit backoff (`time.sleep`) заменяется на `asyncio.sleep`. Оба файла используют один и тот же паттерн, поэтому логика HTTP-вызова выносится в общий модуль `_llm_async_http.py`.
|
||
|
||
### Rationale
|
||
|
||
- `httpx.AsyncClient` уже используется в `llm_analysis/service.py` через `AsyncOpenAI` — прецедент есть.
|
||
- Вынос общей HTTP-логики в отдельный модуль устраняет дублирование между `_llm_http.py` (для execution) и `preview_llm_client.py` (для preview).
|
||
- `asyncio.sleep` вместо `time.sleep` — не блокирует event loop при rate-limit ожидании.
|
||
|
||
### Alternatives Considered
|
||
|
||
- **`AsyncOpenAI` (как в llm_analysis)**: Отвергнут для translate — translate использует OpenAI-совместимый протокол, но не официальный OpenAI SDK. `AsyncOpenAI` не поддерживает произвольные base_url с кастомными провайдерами (OpenRouter, LiteLLM, Kilo).
|
||
- **`httpx.Client` (sync) в asyncio.to_thread**: Отвергнут — избыточно, httpx.AsyncClient решает проблему нативно.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракт `LLMHttpClient` [C:4] — новый модуль `_llm_async_http.py` с `async def call_openai_compatible()`.
|
||
- `_llm_http.py` и `preview_llm_client.py` → Tombstone с @DEPRECATED + @REPLACED_BY.
|
||
- `_llm_call.py`: вызов `call_llm_for_batch()` становится `async def`.
|
||
|
||
---
|
||
|
||
## R7: SQL Lab Executor Async Migration
|
||
|
||
### Decision
|
||
|
||
`SupersetSqlLabExecutor.execute_sql()` и `poll_execution_status()` становятся `async def`. Вызовы `client.network.request()` заменяются на `await async_client.request()`. `time.sleep(poll_interval)` заменяется на `await asyncio.sleep(poll_interval)`. Класс остаётся в `superset_executor.py`, но получает `AsyncAPIClient` вместо sync `SupersetClient`.
|
||
|
||
### Rationale
|
||
|
||
- Поллинг — основной блокировщик в проверке схемы (до 15 × 1s + HTTP latency = до 30s блокировки).
|
||
- `asyncio.sleep` передаёт управление event loop'у между попытками поллинга.
|
||
- `asyncio.wait_for` может быть использован для общего таймаута всей операции проверки схемы.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракт `SupersetSqlLabExecutor` [C:4] — все методы `async def`, @SIDE_EFFECT — асинхронные HTTP-вызовы к Superset SQL Lab API.
|
||
- `check_target_schema` (`_job_routes.py`) — уже `async def`, теперь вызывает `await validate_target_table_schema()`.
|
||
|
||
---
|
||
|
||
## R8: File I/O Async Strategy
|
||
|
||
### Decision
|
||
|
||
Гибридный подход:
|
||
- **Большие файлы** (экспорт дашбордов ZIP > 1MB, загрузка в storage > 10MB): `aiofiles` для потокового чтения/записи.
|
||
- **Мелкие операции** (shutil.rmtree, shutil.copy2, read_bytes, CRC32): `asyncio.to_thread()`.
|
||
- `aiofiles` добавляется в `requirements.txt`.
|
||
|
||
### Rationale
|
||
|
||
- `asyncio.to_thread` для мелких операций — минимальный overhead, не требует новой зависимости.
|
||
- `aiofiles` для больших файлов — предоставляет async-интерфейс, избегает блокировки event loop (может использовать executor-потоки внутри, но API остаётся неблокирующим).
|
||
- Гибридный подход балансирует простоту и эффективность.
|
||
|
||
### Alternatives Considered
|
||
|
||
- **Только `asyncio.to_thread`**: Отвергнут — для больших файлов (50MB ZIP) тред блокируется на секунды, что исчерпывает thread pool.
|
||
- **Только `aiofiles`**: Отвергнут — для мелких операций (shutil.rmtree) `aiofiles` избыточен и не поддерживает рекурсивные операции.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракт `FileIO` [C:3] обновляется: @RATIONALE для гибридного подхода.
|
||
- `aiofiles` добавляется в `requirements.txt`.
|
||
- `shutil` вызовы в плагинах оборачиваются в `asyncio.to_thread`.
|
||
|
||
---
|
||
|
||
## R9: WebSocket Logging Async Migration
|
||
|
||
### Decision
|
||
|
||
`EventBus` переходит с `queue.Queue` + `call_soon_threadsafe` на `asyncio.Queue`. WebSocket-отправка логов становится нативной асинхронной операцией. `TaskLifecycle` использует `asyncio.Queue` для получения событий от задач.
|
||
|
||
### Rationale
|
||
|
||
- `asyncio.Queue` интегрируется с event loop'ом без тредовой синхронизации.
|
||
- `call_soon_threadsafe` больше не нужен — все задачи в одном event loop'е.
|
||
- Упрощает отмену: при закрытии WebSocket очередь очищается, задачи не зависают.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракт `EventBus` [C:4] — замена `queue.Queue` → `asyncio.Queue`.
|
||
- Контракт `TaskLifecycle` [C:4] — асинхронный `run_task()`.
|
||
- WebSocket-ручки (`/ws/tasks/{id}`) — уже `async def`, изменений не требуют.
|
||
|
||
---
|
||
|
||
## R10: Notification Providers Async Migration
|
||
|
||
### Decision
|
||
|
||
- **SMTP**: `smtplib.SMTP` → `aiosmtplib.SMTP` (async SMTP-клиент).
|
||
- **Telegram/Slack**: `requests.post` → `httpx.AsyncClient.post`.
|
||
- Все методы `send()` становятся действительно асинхронными.
|
||
|
||
### Rationale
|
||
|
||
- `aiosmtplib` — зрелая библиотека (2k+ stars), поддерживает STARTTLS, аутентификацию.
|
||
- `httpx.AsyncClient` для Telegram/Slack — переиспользование того же клиента, что и для Superset/LLM.
|
||
|
||
### Impact On Contracts / Tasks
|
||
|
||
- Контракты `SMTPProvider`, `TelegramProvider`, `SlackProvider` [C:3] — `async def send()`.
|
||
- `aiosmtplib` добавляется в `requirements.txt`.
|
||
|
||
---
|
||
|
||
## Unmitigated Risks
|
||
|
||
### R1: Сложность отладки асинхронных race conditions
|
||
**Вероятность**: Средняя | **Влияние**: Высокое
|
||
Ошибки конкурентности могут не воспроизводиться в тестах (одновременный deploy + backup одного дашборда, параллельное обновление кэша).
|
||
**Митигация**: Интеграционные тесты с `asyncio.gather` и случайными задержками (fuzzing). Увеличенное логирование с `trace_id`.
|
||
|
||
### R2: Миграция тестов с `requests_mock` на `pytest-httpx`
|
||
**Вероятность**: Средняя | **Влияние**: Среднее
|
||
Различия в API моков могут привести к тому, что тесты перестанут проверять правильные заголовки/куки.
|
||
**Митигация**: Пошаговая верификация (T049-T053). Параллельный запуск старых и новых тестов на время переходного периода.
|
||
|
||
### R3: Исчерпание семафора и таймауты
|
||
**Вероятность**: Высокая | **Влияние**: Среднее
|
||
При долгой операции (экспорт >30 с) ожидающие запросы получат 503, хотя слот скоро освободится.
|
||
**Митигация**: `connection_pool_timeout` настраивается. Рекомендуется увеличить default до 60 с для prod. Метрика `superset_pool_wait_time` для мониторинга (post‑MVP).
|
||
|
||
### R4: Утечки ресурсов при `asyncio.to_thread`
|
||
**Вероятность**: Низкая | **Влияние**: Высокое
|
||
Поток в `to_thread` может зависнуть (сетевой вызов внутри). Приведёт к утечке потока из bounded‑executor.
|
||
**Митигация**: Bounded‑executor'ы с `max_workers`. Где возможно — `asyncio.wait_for` вокруг `to_thread`. Для Git/файлов — явный таймаут в адаптере.
|
||
|
||
### R5: Сложность рефакторинга Task Manager
|
||
**Вероятность**: Низкая | **Влияние**: Высокое
|
||
Переход с `ThreadPoolExecutor` на `asyncio.create_task` может нарушить логику отмены задач и WebSocket‑логирования, если текущий код полагается на тредовую изоляцию.
|
||
**Митигация**: Сквозной тест: запустить долгую задачу, отменить через API, проверить WebSocket‑событие отмены и завершение всех корутин.
|
||
|
||
### R6: Производительность при массовых DB-операциях
|
||
**Вероятность**: Средняя | **Влияние**: Среднее
|
||
При 100 параллельных запросах каждый создаёт поток для БД через `to_thread`.
|
||
**Митигация**: Bounded `db_executor` с `max_workers=10`. Очередь запросов через `asyncio.Semaphore` для БД (отдельный от Superset пула).
|
||
|
||
---
|
||
|
||
## Summary of New Dependencies
|
||
|
||
| Package | Purpose | Already in requirements.txt? |
|
||
|---------|---------|------------------------------|
|
||
| `httpx==0.28.1` | Async HTTP client | ✅ Yes |
|
||
| `anyio==4.12.0` | Async I/O primitives | ✅ Yes |
|
||
| `pytest-httpx` | Test mock for httpx | ❌ Add to dev deps |
|
||
| `aiofiles` | Async file I/O | ❌ Add to requirements.txt |
|
||
| `aiosmtplib` | Async SMTP | ❌ Add to requirements.txt |
|