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.
20 KiB
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()+CancelledErrorcascade. - 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 |