Files
ss-tools/specs/032-translate-requests-httpx/research.md
busya bc32dd65ef 032: mark all tasks [x] — feature complete
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.
2026-06-04 21:09:14 +03:00

292 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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` с authcache и 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` для мониторинга (postMVP).
### R4: Утечки ресурсов при `asyncio.to_thread`
**Вероятность**: Низкая | **Влияние**: Высокое
Поток в `to_thread` может зависнуть (сетевой вызов внутри). Приведёт к утечке потока из boundedexecutor.
**Митигация**: Boundedexecutor'ы с `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 |