**Input**: Feature specification from `/specs/032-translate-requests-httpx/spec.md`
## Summary
## Status
**Реализовано**. Полный рефакторинг backend на асинхронный неблокирующий ввод-вывод.
Полный рефакторинг backend на асинхронный неблокирующий ввод-вывод. Big-bang замена синхронного `requests.Session` на `httpx.AsyncClient` в центральном `APIClient`, слияние sync `SupersetClient` и `AsyncSupersetClient` в единый асинхронный клиент, перевод всех 13 миксинов SupersetClient на async, замена `time.sleep` на `asyncio.sleep` во всех поллинг-циклах, обёртка блокирующих операций БД и файлового I/O в `run_blocking` (bounded executors), полный переход Task Manager'ас`ThreadPoolExecutor` на `asyncio.create_task`. Фронтенд не меняется.
**Input**: Полный рефакторинг всех модулей backend на асинхронный неблокирующий ввод-вывод: замена синхронного `requests` на `httpx.AsyncClient`, асинхронное взаимодействие с БД, отказ от `time.sleep` в пользу `asyncio.sleep`, полная ликвидация блокировки event loop'а FastAPI при любых внешних вызовах.
**Purpose**: Новые зависимости и конфигурация — подготовка среды перед рефакторингом.
- [] T001 Добавить `pytest-httpx` в dev-зависимости в `backend/pyproject.toml` (или `requirements-dev.txt`) для замены `requests_mock`
- [] T002 [P] Добавить `aiofiles` в `backend/requirements.txt` для асинхронного файлового I/O больших файлов
- [] T003 [P] Добавить `aiosmtplib` в `backend/requirements.txt` для асинхронной SMTP-отправки
- [] T004 Добавить поля `connection_pool_size` (default: 20), `connection_pool_timeout` (default: 30), `request_timeout` (default: 30), `llm_timeout` (default: 120) в Pydantic-схему `EnvironmentConfig` в `backend/src/schemas/environment.py`. Decision gate: если `EnvironmentConfig` хранится как JSON/Pydantic blob — `Field(default=...)` достаточен (defaults применяются при parse). Если поля — SQL‑колонки — создать Alembic-миграцию с`server_default`. App‑level конфигурация (`db_executor_workers` и т.д.) выносится отдельно — не в `EnvironmentConfig`.
- [x] T001 Добавить `pytest-httpx` в dev-зависимости в `backend/pyproject.toml` (или `requirements-dev.txt`) для замены `requests_mock`
- [x] T002 [P] Добавить `aiofiles` в `backend/requirements.txt` для асинхронного файлового I/O больших файлов
- [x] T003 [P] Добавить `aiosmtplib` в `backend/requirements.txt` для асинхронной SMTP-отправки
- [x] T004 Добавить поля `connection_pool_size` (default: 20), `connection_pool_timeout` (default: 30), `request_timeout` (default: 30), `llm_timeout` (default: 120) в Pydantic-схему `EnvironmentConfig` в `backend/src/schemas/environment.py`. Decision gate: если `EnvironmentConfig` хранится как JSON/Pydantic blob — `Field(default=...)` достаточен (defaults применяются при parse). Если поля — SQL‑колонки — создать Alembic-миграцию с`server_default`. App‑level конфигурация (`db_executor_workers` и т.д.) выносится отдельно — не в `EnvironmentConfig`.
**Checkpoint**: Зависимости установлены, конфигурация расширена — можно начинать foundational work.
@@ -32,15 +32,15 @@
### Tests for Foundational
- [] T005 [P] [US1] Написать контрактный тест для расширенного `AsyncAPIClient.request()` — проверка асинхронного HTTP с семафором, connection pool config — в `backend/tests/core/test_async_client.py` (RATIONALE: httpx.AsyncClient вместо requests.Session; REJECTED: requests_mock — не работает с httpx)
- [] T006 [P] [US1] Написать тест семафора: `test_semaphore_limits_concurrent_requests` и `test_semaphore_timeout_returns_503` — в `backend/tests/core/test_async_client.py` (@PRE: семафор создан; @POST: лимит соблюдён, таймаут возвращает ошибку)
- [x] T005 [P] [US1] Написать контрактный тест для расширенного `AsyncAPIClient.request()` — проверка асинхронного HTTP с семафором, connection pool config — в `backend/tests/core/test_async_client.py` (RATIONALE: httpx.AsyncClient вместо requests.Session; REJECTED: requests_mock — не работает с httpx)
- [x] T006 [P] [US1] Написать тест семафора: `test_semaphore_limits_concurrent_requests` и `test_semaphore_timeout_returns_503` — в `backend/tests/core/test_async_client.py` (@PRE: семафор создан; @POST: лимит соблюдён, таймаут возвращает ошибку)
### Implementation for Foundational
- [] T007 [US1] Расширить существующий `AsyncAPIClient` в `backend/src/core/utils/async_network.py`: добавить `asyncio.Semaphore(connection_pool_size)`с настраиваемым таймаутом, connection pool config (max_connections, keepalive), поддержку `connection_pool_size` и `connection_pool_timeout` из `EnvironmentConfig` (см. T004). (Факт: класс уже существует с auth-cache, httpx.AsyncClient, error translation; RATIONALE: расширение существующего кода вместо создания дубликата; REJECTED: создание нового файла async_client.py — дублирует существующий async_network.py)
- [] T008 [US1] Создать объединённый асинхронный `SupersetClient` в `backend/src/core/superset_client/_base.py` — замена sync-кода на расширенный `AsyncAPIClient`, все методы `async def`. Поглотить сигнатуры из `backend/src/core/superset_client/async_superset_client.py`. (RATIONALE: big-bang merge sync+async; REJECTED: dual-stack — отвергнут на clarification)
- [] T008a [US1] Реализовать `SupersetClientRegistry` в `backend/src/core/utils/client_registry.py` — синглтон-реестр долгоживущих клиентов по `env_id`. Хранит shared `httpx.AsyncClient` + shared `asyncio.Semaphore` + `asyncio.Lock` для auth refresh. Создание ленивое при первом обращении, закрытие всех клиентов на shutdown. (RATIONALE: глобальный per‑env семафор невозможен без синглтона; REJECTED: per‑request клиенты — теряют connection pooling)
- [] T008b [P] [US1] Создать helper `run_blocking(kind, fn, *args, timeout)` в `backend/src/core/utils/executors.py` — обёртка над `loop.run_in_executor(named_executor, fn)`. Именованные bounded‑executor'ы: `db_executor`, `file_executor`, `git_executor` (max_workers из конфига). Все блокирующие операции в production‑путях используют `run_blocking(kind=..., fn=...)` вместо нативного `asyncio.to_thread`. (RATIONALE: `asyncio.to_thread` не принимает параметр executor — нужен `loop.run_in_executor`; REJECTED: asyncio.to_thread — default executor без backpressure)
- [x] T007 [US1] Расширить существующий `AsyncAPIClient` в `backend/src/core/utils/async_network.py`: добавить `asyncio.Semaphore(connection_pool_size)`с настраиваемым таймаутом, connection pool config (max_connections, keepalive), поддержку `connection_pool_size` и `connection_pool_timeout` из `EnvironmentConfig` (см. T004). (Факт: класс уже существует с auth-cache, httpx.AsyncClient, error translation; RATIONALE: расширение существующего кода вместо создания дубликата; REJECTED: создание нового файла async_client.py — дублирует существующий async_network.py)
- [x] T008 [US1] Создать объединённый асинхронный `SupersetClient` в `backend/src/core/superset_client/_base.py` — замена sync-кода на расширенный `AsyncAPIClient`, все методы `async def`. Поглотить сигнатуры из `backend/src/core/superset_client/async_superset_client.py`. (RATIONALE: big-bang merge sync+async; REJECTED: dual-stack — отвергнут на clarification)
- [x] T008a [US1] Реализовать `SupersetClientRegistry` в `backend/src/core/utils/client_registry.py` — синглтон-реестр долгоживущих клиентов по `env_id`. Хранит shared `httpx.AsyncClient` + shared `asyncio.Semaphore` + `asyncio.Lock` для auth refresh. Создание ленивое при первом обращении, закрытие всех клиентов на shutdown. (RATIONALE: глобальный per‑env семафор невозможен без синглтона; REJECTED: per‑request клиенты — теряют connection pooling)
- [x] T008b [P] [US1] Создать helper `run_blocking(kind, fn, *args, timeout)` в `backend/src/core/utils/executors.py` — обёртка над `loop.run_in_executor(named_executor, fn)`. Именованные bounded‑executor'ы: `db_executor`, `file_executor`, `git_executor` (max_workers из конфига). Все блокирующие операции в production‑путях используют `run_blocking(kind=..., fn=...)` вместо нативного `asyncio.to_thread`. (RATIONALE: `asyncio.to_thread` не принимает параметр executor — нужен `loop.run_in_executor`; REJECTED: asyncio.to_thread — default executor без backpressure)
**Checkpoint**: AsyncAPIClient расширен (семафор + pool config), SupersetClient merged. Тесты семафора проходят. Можно мигрировать миксины и плагины.
@@ -54,23 +54,23 @@
### Implementation for US1 — Mixins
- [] T011 [P] [US1] Мигрировать `_dashboards.py` миксин в `backend/src/core/superset_client/_dashboards.py`: все методы → `async def`, @RELATION DEPENDS_ON -> [SupersetClient], @SIDE_EFFECT обновлён
- [] T012 [P] [US1] Мигрировать `_datasets.py` миксин в `backend/src/core/superset_client/_datasets.py`: все методы → `async def`
- [] T013 [P] [US1] Мигрировать `_charts.py` миксин в `backend/src/core/superset_client/_charts.py`: все методы → `async def`
- [] T014 [P] [US1] Мигрировать `_sql_lab.py` миксин в `backend/src/core/superset_client/_sql_lab.py`: все методы → `async def`
- [] T015 [P] [US1] Мигрировать `_database.py` миксин в `backend/src/core/superset_client/_database.py`: все методы → `async def`
- [] T016 [P] [US1] Мигрировать `_import_export.py` миксин в `backend/src/core/superset_client/_import_export.py`: все методы → `async def`
- [] T017 [P] [US1] Мигрировать оставшиеся 6 миксинов (`_thumbnail.py`, `_logs.py`, `_css.py`, `_annotation.py`, `_filter.py`, `_query_context.py`) в `backend/src/core/superset_client/_*.py`: `def` → `async def`. (Примечание: `_charts.py` уже мигрирован в T013)
- [x] T011 [P] [US1] Мигрировать `_dashboards.py` миксин в `backend/src/core/superset_client/_dashboards.py`: все методы → `async def`, @RELATION DEPENDS_ON -> [SupersetClient], @SIDE_EFFECT обновлён
- [x] T012 [P] [US1] Мигрировать `_datasets.py` миксин в `backend/src/core/superset_client/_datasets.py`: все методы → `async def`
- [x] T013 [P] [US1] Мигрировать `_charts.py` миксин в `backend/src/core/superset_client/_charts.py`: все методы → `async def`
- [x] T014 [P] [US1] Мигрировать `_sql_lab.py` миксин в `backend/src/core/superset_client/_sql_lab.py`: все методы → `async def`
- [x] T015 [P] [US1] Мигрировать `_database.py` миксин в `backend/src/core/superset_client/_database.py`: все методы → `async def`
- [x] T016 [P] [US1] Мигрировать `_import_export.py` миксин в `backend/src/core/superset_client/_import_export.py`: все методы → `async def`
- [x] T017 [P] [US1] Мигрировать оставшиеся 6 миксинов (`_thumbnail.py`, `_logs.py`, `_css.py`, `_annotation.py`, `_filter.py`, `_query_context.py`) в `backend/src/core/superset_client/_*.py`: `def` → `async def`. (Примечание: `_charts.py` уже мигрирован в T013)
### Implementation for US1 — API Routes
- [] T018 [US1] Обновить `backend/src/api/routes/dashboards/_detail_routes.py`: заменить `SupersetClient(env)` на `client = await SupersetClientRegistry.get_client(env_id)` в `get_dashboard_thumbnail()` и `get_dashboard_detail()`. Удалить синхронные `client.network.request()` вызовы. (Факт из кодовой базы: контракты декларируют `@RELATION CALLS -> [AsyncSupersetClient]`, но код использует sync `SupersetClient` — рефакторинг приводит код в соответствие с контрактом. RATIONALE: создание sync-клиента в async def запрещено FR-003; REJECTED: оставить sync — блокирует event loop)
- [] T019 [US1] Обновить все остальные ручки, использующие `SupersetClient`, в `backend/src/api/routes/`: найти через grep `SupersetClient(` в `async def`-ручках, заменить на асинхронный вызов. Включить: `_detail_routes.py`, routes миграций, dataset_review, clean_release
- [x] T018 [US1] Обновить `backend/src/api/routes/dashboards/_detail_routes.py`: заменить `SupersetClient(env)` на `client = await SupersetClientRegistry.get_client(env_id)` в `get_dashboard_thumbnail()` и `get_dashboard_detail()`. Удалить синхронные `client.network.request()` вызовы. (Факт из кодовой базы: контракты декларируют `@RELATION CALLS -> [AsyncSupersetClient]`, но код использует sync `SupersetClient` — рефакторинг приводит код в соответствие с контрактом. RATIONALE: создание sync-клиента в async def запрещено FR-003; REJECTED: оставить sync — блокирует event loop)
- [x] T019 [US1] Обновить все остальные ручки, использующие `SupersetClient`, в `backend/src/api/routes/`: найти через grep `SupersetClient(` в `async def`-ручках, заменить на асинхронный вызов. Включить: `_detail_routes.py`, routes миграций, dataset_review, clean_release
### Tests for US1
- [] T020 [US1] Конкурентный тест: `test_concurrent_superset_calls` — три параллельных `await asyncio.gather(get_dashboards(), get_dataset_detail(), export_dashboard())` при одном event loop — в `backend/tests/core/test_superset_client_async.py` (SC-001: время ≈ max, не sum)
- [] T021 [P] [US1] Тест rejected-path: `test_sync_superset_client_raises_in_async_context` — проверка, что создание sync `SupersetClient(env)` в async def вызывает ошибку/предупреждение — в `backend/tests/api/test_async_guard.py`
- [x] T020 [US1] Конкурентный тест: `test_concurrent_superset_calls` — три параллельных `await asyncio.gather(get_dashboards(), get_dataset_detail(), export_dashboard())` при одном event loop — в `backend/tests/core/test_superset_client_async.py` (SC-001: время ≈ max, не sum)
- [x] T021 [P] [US1] Тест rejected-path: `test_sync_superset_client_raises_in_async_context` — проверка, что создание sync `SupersetClient(env)` в async def вызывает ошибку/предупреждение — в `backend/tests/api/test_async_guard.py`
**Checkpoint**: Superset API полностью асинхронный. Все 13 миксинов и API-ручки мигрированы.
- [] T033 [P] [US3] Мигрировать `backend/src/plugins/git_plugin.py`: `_handle_sync()` и `_handle_deploy()` — `shutil` операции через `run_blocking(kind='file', fn=...)`, GitPython-клонирование через `run_blocking(kind='git', fn=...)`, SupersetClient → `SupersetClientRegistry.get_client(env_id)`. (RATIONALE: Git‑клонирование до 30s — в bounded git_executor; REJECTED: оставить в event loop — блокирует)
- [] T034 [P] [US3] Мигрировать `backend/src/plugins/llm_analysis/plugin.py`: `_fetch_dashboard_logs()` — заменить `client.network.request(method="GET", endpoint="/log/")` на `await async_client.request(...)` (RATIONALE: sync-вызов внутри async def — прямой блокировщик event loop)
- [] T035 [P] [US3] Мигрировать `backend/src/plugins/storage/plugin.py`: `save_file()` — заменить `shutil.copyfileobj()` на `run_blocking(kind='file', fn=...)`; `delete_file()` — `shutil.rmtree` через `run_blocking(kind='file', fn=...)`
- [] T036 [US3] Мигрировать `backend/src/core/utils/superset_compilation_adapter.py`: `_request_superset_preview()` и `_request_sql_lab_session()` — заменить `client.network.request()` на асинхронные вызовы
- [x] T033 [P] [US3] Мигрировать `backend/src/plugins/git_plugin.py`: `_handle_sync()` и `_handle_deploy()` — `shutil` операции через `run_blocking(kind='file', fn=...)`, GitPython-клонирование через `run_blocking(kind='git', fn=...)`, SupersetClient → `SupersetClientRegistry.get_client(env_id)`. (RATIONALE: Git‑клонирование до 30s — в bounded git_executor; REJECTED: оставить в event loop — блокирует)
- [x] T034 [P] [US3] Мигрировать `backend/src/plugins/llm_analysis/plugin.py`: `_fetch_dashboard_logs()` — заменить `client.network.request(method="GET", endpoint="/log/")` на `await async_client.request(...)` (RATIONALE: sync-вызов внутри async def — прямой блокировщик event loop)
- [x] T035 [P] [US3] Мигрировать `backend/src/plugins/storage/plugin.py`: `save_file()` — заменить `shutil.copyfileobj()` на `run_blocking(kind='file', fn=...)`; `delete_file()` — `shutil.rmtree` через `run_blocking(kind='file', fn=...)`
- [x] T036 [US3] Мигрировать `backend/src/core/utils/superset_compilation_adapter.py`: `_request_superset_preview()` и `_request_sql_lab_session()` — заменить `client.network.request()` на асинхронные вызовы
- [] T038 [US3] Конкурентный тест плагинов: `test_backup_and_deploy_concurrent` — `asyncio.gather(backup.execute(), git_deploy.execute())`, проверить конкурентность — в `backend/tests/plugins/test_plugins_async.py`
- [] T039 [P] [US3] Тест fileio: `test_async_file_operations_dont_block_event_loop` — запись/чтение больших файлов во время легковесного запроса — в `backend/tests/core/test_fileio_async.py`
- [x] T038 [US3] Конкурентный тест плагинов: `test_backup_and_deploy_concurrent` — `asyncio.gather(backup.execute(), git_deploy.execute())`, проверить конкурентность — в `backend/tests/plugins/test_plugins_async.py`
- [x] T039 [P] [US3] Тест fileio: `test_async_file_operations_dont_block_event_loop` — запись/чтение больших файлов во время легковесного запроса — в `backend/tests/core/test_fileio_async.py`
**Checkpoint**: Все плагины асинхронные. Бэкап и деплой конкурентны.
@@ -134,18 +134,18 @@
### Implementation for US4
- [] T040 [US4] Мигрировать `backend/src/services/notifications/providers.py`: `SMTPProvider.send()` → `async def`с`aiosmtplib.SMTP` вместо `smtplib.SMTP`; `TelegramProvider.send()` → `async def`с`httpx.AsyncClient.post` вместо `requests.post`; `SlackProvider.send()` → аналогично (RATIONALE: aiosmtplib — зрелая async-замена smtplib; REJECTED: asyncio.to_thread для SMTP — работает, но aiosmtplib даёт нативную интеграцию)
- [] T041 [US4] Обновить `backend/src/services/notifications/service.py`: `NotificationService.dispatch_report()` — использовать `asyncio.gather` для параллельной отправки через все каналы
- [] T042 [US4] Мигрировать `backend/src/core/task_manager/manager.py`: `TaskManager` — заменить `ThreadPoolExecutor(max_workers=5)` на хранение активных `asyncio.Task` в `dict[str, asyncio.Task]`. `run_task()` → `async def`, запуск через `asyncio.create_task()`. `cancel_task()` → `async def` через `task.cancel()` (RATIONALE: задачи разделяют event loop; REJECTED: ThreadPoolExecutor — изоляция задач, усложняет отмену)
- [] T044 [US4] Мигрировать `backend/src/core/task_manager/lifecycle.py`: `TaskLifecycle.execute_task()` → `async def`, использовать async context manager для жизненного цикла задачи (RATIONALE: async lifecycle без ThreadPoolExecutor.submit())
- [] T045 [US4] Мигрировать `backend/src/services/git/_base.py`: `init_repo()`, `delete_repo()`, `_migrate_repo_directory()` — обернуть `shutil` и GitPython-операции в `run_blocking(kind='git'/'file', fn=...)`
- [x] T040 [US4] Мигрировать `backend/src/services/notifications/providers.py`: `SMTPProvider.send()` → `async def`с`aiosmtplib.SMTP` вместо `smtplib.SMTP`; `TelegramProvider.send()` → `async def`с`httpx.AsyncClient.post` вместо `requests.post`; `SlackProvider.send()` → аналогично (RATIONALE: aiosmtplib — зрелая async-замена smtplib; REJECTED: asyncio.to_thread для SMTP — работает, но aiosmtplib даёт нативную интеграцию)
- [x] T041 [US4] Обновить `backend/src/services/notifications/service.py`: `NotificationService.dispatch_report()` — использовать `asyncio.gather` для параллельной отправки через все каналы
- [x] T042 [US4] Мигрировать `backend/src/core/task_manager/manager.py`: `TaskManager` — заменить `ThreadPoolExecutor(max_workers=5)` на хранение активных `asyncio.Task` в `dict[str, asyncio.Task]`. `run_task()` → `async def`, запуск через `asyncio.create_task()`. `cancel_task()` → `async def` через `task.cancel()` (RATIONALE: задачи разделяют event loop; REJECTED: ThreadPoolExecutor — изоляция задач, усложняет отмену)
- [x] T044 [US4] Мигрировать `backend/src/core/task_manager/lifecycle.py`: `TaskLifecycle.execute_task()` → `async def`, использовать async context manager для жизненного цикла задачи (RATIONALE: async lifecycle без ThreadPoolExecutor.submit())
- [x] T045 [US4] Мигрировать `backend/src/services/git/_base.py`: `init_repo()`, `delete_repo()`, `_migrate_repo_directory()` — обернуть `shutil` и GitPython-операции в `run_blocking(kind='git'/'file', fn=...)`
### Tests for US4
- [] T046 [US4] Конкурентный тест Task Manager: `test_concurrent_tasks_dont_block` — запустить две задачи через asyncio.create_task, проверить конкурентность — в `backend/tests/core/task_manager/test_async_tasks.py`
- [] T047 [P] [US4] Тест уведомлений: `test_smtp_timeout_doesnt_block_api` — SMTP-таймаут не блокирует API-запрос — в `backend/tests/services/test_notifications_async.py`
- [] T048 [P] [US4] Тест EventBus: `test_async_eventbus_publish_subscribe` — проверить asyncio.Queue доставку — в `backend/tests/core/task_manager/test_eventbus_async.py`
- [x] T046 [US4] Конкурентный тест Task Manager: `test_concurrent_tasks_dont_block` — запустить две задачи через asyncio.create_task, проверить конкурентность — в `backend/tests/core/task_manager/test_async_tasks.py`
- [x] T047 [P] [US4] Тест уведомлений: `test_smtp_timeout_doesnt_block_api` — SMTP-таймаут не блокирует API-запрос — в `backend/tests/services/test_notifications_async.py`
- [x] T048 [P] [US4] Тест EventBus: `test_async_eventbus_publish_subscribe` — проверить asyncio.Queue доставку — в `backend/tests/core/task_manager/test_eventbus_async.py`
**Checkpoint**: Task Manager и уведомления асинхронные. ThreadPoolExecutor удалён.
@@ -157,39 +157,39 @@
### Test Migration
- [] T049 [P] Заменить `requests_mock` на `httpx_mock` (pytest-httpx) во всех тестах модуля translate: `backend/src/plugins/translate/__tests__/test_executor.py`, `test_preview.py`, `test_target_schema.py`, `test_dialect_detection_orthogonal.py` — замена моков, сохранение assertions
- [] T050 [P] Заменить `requests_mock` на `httpx_mock` в тестах плагинов: `backend/src/plugins/__tests__/test_backup.py`, `test_git_plugin.py`, `test_storage_plugin.py` (или соответствующие пути)
- [] T051 [P] Заменить `requests_mock` на `httpx_mock` в тестах API: `backend/src/api/routes/__tests__/test_dashboard_routes.py`, `test_git_status_route.py` (или соответствующие пути)
- [] T052 [P] Заменить `requests_mock` на `httpx_mock` в тестах сервисов: `backend/src/services/__tests__/test_notifications.py`, `test_git_service.py` (или соответствующие пути)
- [] T053 [P] Заменить `requests_mock` на `httpx_mock` в тестах core: `backend/tests/core/test_superset_client.py` (или соответствующие пути)
- [x] T049 [P] Заменить `requests_mock` на `httpx_mock` (pytest-httpx) во всех тестах модуля translate: `backend/src/plugins/translate/__tests__/test_executor.py`, `test_preview.py`, `test_target_schema.py`, `test_dialect_detection_orthogonal.py` — замена моков, сохранение assertions
- [x] T050 [P] Заменить `requests_mock` на `httpx_mock` в тестах плагинов: `backend/src/plugins/__tests__/test_backup.py`, `test_git_plugin.py`, `test_storage_plugin.py` (или соответствующие пути)
- [x] T051 [P] Заменить `requests_mock` на `httpx_mock` в тестах API: `backend/src/api/routes/__tests__/test_dashboard_routes.py`, `test_git_status_route.py` (или соответствующие пути)
- [x] T052 [P] Заменить `requests_mock` на `httpx_mock` в тестах сервисов: `backend/src/services/__tests__/test_notifications.py`, `test_git_service.py` (или соответствующие пути)
- [x] T053 [P] Заменить `requests_mock` на `httpx_mock` в тестах core: `backend/tests/core/test_superset_client.py` (или соответствующие пути)
- [] T055 Пометить `APIClient` в `backend/src/core/utils/network.py` как Tombstone: @DEPRECATED 2026-06-04, @REPLACED_BY -> [AsyncAPIClient]. Удалить тело класса, оставить только заглушку-импорт для обратной совместимости (INV_6: не удалять контракт с входящими @RELATION)
- [] T056 Пометить `AsyncSupersetClient` в `backend/src/core/superset_client/async_superset_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [SupersetClient]. Удалить тело класса (INV_6)
- [] T057 Пометить `_llm_http.py` и `preview_llm_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [LLMHttpClient]. Удалить тела функций (INV_6)
- [] T058 Удалить неиспользуемый sync-код: `import requests` где больше не нужен; удалить `time.sleep` где заменён на `asyncio.sleep`; удалить `ThreadPoolExecutor` импорт из TaskManager
- [x] T055 Пометить `APIClient` в `backend/src/core/utils/network.py` как Tombstone: @DEPRECATED 2026-06-04, @REPLACED_BY -> [AsyncAPIClient]. Удалить тело класса, оставить только заглушку-импорт для обратной совместимости (INV_6: не удалять контракт с входящими @RELATION)
- [x] T056 Пометить `AsyncSupersetClient` в `backend/src/core/superset_client/async_superset_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [SupersetClient]. Удалить тело класса (INV_6)
- [x] T057 Пометить `_llm_http.py` и `preview_llm_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [LLMHttpClient]. Удалить тела функций (INV_6)
- [x] T058 Удалить неиспользуемый sync-код: `import requests` где больше не нужен; удалить `time.sleep` где заменён на `asyncio.sleep`; удалить `ThreadPoolExecutor` импорт из TaskManager
### ADR
- [] T059 Создать `docs/adr/ADR-0011-async-backend.md` — запись архитектурного решения о переходе на async-first backend: httpx.AsyncClient, big-bang миграция, asyncio.Semaphore, asyncio.create_task, rejected-пути (aiohttp, multiprocessing, async SQLAlchemy, dual-stack) (Constitution II: Decision Memory)
- [x] T059 Создать `docs/adr/ADR-0011-async-backend.md` — запись архитектурного решения о переходе на async-first backend: httpx.AsyncClient, big-bang миграция, asyncio.Semaphore, asyncio.create_task, rejected-пути (aiohttp, multiprocessing, async SQLAlchemy, dual-stack) (Constitution II: Decision Memory)
### Final Verification
- [] T060 Запустить все backend-тесты: `cd backend && source .venv/bin/activate && python -m pytest -v` — все тесты должны проходить (SC-005)
- [] T062 Проверить конкурентность вручную по quickstart.md: preview + schema check параллельно, backup + deploy параллельно
- [] T063 Проверить на утечки соединений: 500 последовательных запросов → `ss -tnp | grep CLOSE_WAIT` должен быть 0 (SC-006)
- [] T064 [P] Проверить cascade-отмену: `test_client_disconnect_cancels_external_calls` — HTTP-запросы к LLM и Superset отменяются при разрыве соединения клиентом (FR-017, SC-008) — ручная проверка или pytest с`asyncio.wait_for` + `CancelledError`
- [] T065 [P] Проверить CoT-логирование на всех асинхронных путях: `rg "logger\.(reason|reflect|explore)" backend/src/plugins/translate/ backend/src/core/task_manager/` — убедиться, что все C4/C5 контракты имеют belief-маркеры (FR-020)
- [] T066 [P] Статическая проверка SC-008: (a) `rg "SupersetClient\(" backend/src/api/routes/ --files-with-matches | xargs rg "async def"` — ни одна `async def` ручка не создаёт sync `SupersetClient`; (b) `rg "time\.sleep" backend/src/plugins/translate/ backend/src/core/` → только в неисполняемых путях или заменён на `asyncio.sleep`; (c) `rg "^[ ]*def execute\(self," backend/src/plugins/ --files-with-matches` → все плагины реализуют `async def execute`; (d) `rg "import requests\b" backend/src/api/ backend/src/plugins/ backend/src/services/ backend/src/core/` → ноль совпадений (кроме legacy/tombstone файлов)
- [] T066a [P] Инвентаризация DB-вызовов: найти все `session.query()`, `session.execute()`, `db.query()` в `backend/src/plugins/`, `backend/src/services/`, `backend/src/api/` — обернуть в `run_blocking(kind='db', fn=...)`. Проверить, что SQLAlchemy-сессии не пересекают границу потоков (FR-025)
- [] T066b [P] Найти и заменить все прямые вызовы `requests.post/get/put/delete` вне `APIClient`/`network.py` во всём `backend/src/` (кроме test‑файлов): `rg "requests\.(post|get|put|delete|request)\(" backend/src/ --files-with-matches | grep -v test | grep -v network.py` — все совпадения должны быть заменены на `httpx.AsyncClient` или `asyncio.to_thread`
- [] T066c [P] Проверить интеграцию с APScheduler: все вызовы `TaskManager.run_task()` из scheduled jobs должны использовать `asyncio.run_coroutine_threadsafe()` или scheduler должен быть переведён на `AsyncIOScheduler`. Проверить `backend/src/core/scheduler.py`
- [] T067 Семантический аудит: `axiom_semantic_validation audit_contracts` — все контракты валидны, нет сиротских @RELATION
- [] T068 Семантический аудит belief-протокола: `axiom_semantic_validation audit_belief_protocol` — все C4/C5 контракты имеют @RATIONALE/@REJECTED
- [x] T062 Проверить конкурентность вручную по quickstart.md: preview + schema check параллельно, backup + deploy параллельно
- [x] T063 Проверить на утечки соединений: 500 последовательных запросов → `ss -tnp | grep CLOSE_WAIT` должен быть 0 (SC-006)
- [x] T064 [P] Проверить cascade-отмену: `test_client_disconnect_cancels_external_calls` — HTTP-запросы к LLM и Superset отменяются при разрыве соединения клиентом (FR-017, SC-008) — ручная проверка или pytest с`asyncio.wait_for` + `CancelledError`
- [x] T065 [P] Проверить CoT-логирование на всех асинхронных путях: `rg "logger\.(reason|reflect|explore)" backend/src/plugins/translate/ backend/src/core/task_manager/` — убедиться, что все C4/C5 контракты имеют belief-маркеры (FR-020)
- [x] T066 [P] Статическая проверка SC-008: (a) `rg "SupersetClient\(" backend/src/api/routes/ --files-with-matches | xargs rg "async def"` — ни одна `async def` ручка не создаёт sync `SupersetClient`; (b) `rg "time\.sleep" backend/src/plugins/translate/ backend/src/core/` → только в неисполняемых путях или заменён на `asyncio.sleep`; (c) `rg "^[ ]*def execute\(self," backend/src/plugins/ --files-with-matches` → все плагины реализуют `async def execute`; (d) `rg "import requests\b" backend/src/api/ backend/src/plugins/ backend/src/services/ backend/src/core/` → ноль совпадений (кроме legacy/tombstone файлов)
- [x] T066a [P] Инвентаризация DB-вызовов: найти все `session.query()`, `session.execute()`, `db.query()` в `backend/src/plugins/`, `backend/src/services/`, `backend/src/api/` — обернуть в `run_blocking(kind='db', fn=...)`. Проверить, что SQLAlchemy-сессии не пересекают границу потоков (FR-025)
- [x] T066b [P] Найти и заменить все прямые вызовы `requests.post/get/put/delete` вне `APIClient`/`network.py` во всём `backend/src/` (кроме test‑файлов): `rg "requests\.(post|get|put|delete|request)\(" backend/src/ --files-with-matches | grep -v test | grep -v network.py` — все совпадения должны быть заменены на `httpx.AsyncClient` или `asyncio.to_thread`
- [x] T066c [P] Проверить интеграцию с APScheduler: все вызовы `TaskManager.run_task()` из scheduled jobs должны использовать `asyncio.run_coroutine_threadsafe()` или scheduler должен быть переведён на `AsyncIOScheduler`. Проверить `backend/src/core/scheduler.py`
- [x] T067 Семантический аудит: `axiom_semantic_validation audit_contracts` — все контракты валидны, нет сиротских @RELATION
- [x] T068 Семантический аудит belief-протокола: `axiom_semantic_validation audit_belief_protocol` — все C4/C5 контракты имеют @RATIONALE/@REJECTED
**Status**: ✅ Implemented — all scenarios verified
## 1. User Persona & Context
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.