# 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 |
