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

20 KiB
Raw Permalink Blame History

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.Queueasyncio.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.Queueasyncio.Queue.
  • Контракт TaskLifecycle [C:4] — асинхронный run_task().
  • WebSocket-ручки (/ws/tasks/{id}) — уже async def, изменений не требуют.

R10: Notification Providers Async Migration

Decision

  • SMTP: smtplib.SMTPaiosmtplib.SMTP (async SMTP-клиент).
  • Telegram/Slack: requests.posthttpx.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