Files
ss-tools/docs/adr/ADR-0011-async-backend.md

5.3 KiB
Raw Blame History

[DEF:ADR-0011:ADR]

@STATUS ACTIVE

@PURPOSE Документировать архитектурное решение о полном переходе backend на async-first архитектуру: замена синхронного requests.Session на httpx.AsyncClient, введение глобальных perenv семафоров, bounded executors и полный переход Task Manager на asyncio.

@RATIONALE ss-tools одновременно взаимодействует с несколькими внешними системами (Superset REST API, LLM API, SMTP, Telegram, Slack) и выполняет блокирующие операции (Git, файловый I/O, SQLAlchemy). При одном воркере uvicorn синхронные блокирующие вызовы останавливают все входящие HTTP-запросы. Единственный способ обеспечить конкурентность — сделать весь I/O неблокирующим.

@REJECTED aiohttp — отвергнут в пользу httpx, который уже был в зависимостях (0.28.1) и имеет лучший API для async HTTP.

@REJECTED Полный переход на async SQLAlchemy — потребовал бы переписывания всех моделей и репозиториев, что выходит за рамки этой фичи. run_blocking с bounded executor достаточен.

@REJECTED Dual-stack (sync + async) — потребовал бы поддержки двух транспортов, увеличивая surface для ошибок и усложняя код.

@REJECTED Wrapper-only (asyncio.to_thread на всё) — rejected, т.к. asyncio.to_thread не принимает executor и использует default пул без backpressure.

@REJECTED Multiprocessing / --workers N — rejected, т.к. увеличение воркеров не решает проблему блокировки внутри одного воркера и увеличивает потребление памяти.

Decision

Весь backend переводится на async-first архитектуру:

Transport Layer

  1. HTTP-клиент: requests.Sessionhttpx.AsyncClient через класс AsyncAPIClient в core/utils/async_network.py.
  2. SupersetClient: sync + AsyncSupersetClient объединены в единый асинхронный клиент. Все 13 миксинов — async def.
  3. Семафоры: Глобальный perenv asyncio.Semaphore через синглтон SupersetClientRegistry (core/utils/client_registry.py). При исчерпании — таймаут 30s → 503.
  4. LLM-клиент: Унифицированный LLMHttpClient в plugins/translate/_llm_async_http.py на httpx.AsyncClient. Rate-limit backoff через asyncio.sleep.

Blocking Operations

  1. Bounded executors: Именованные ThreadPoolExecutor (db, file, git) через helper run_blocking(kind, fn) в core/utils/executors.py. Вместо asyncio.to_thread.
  2. Graceful shutdown: Все клиенты закрываются при shutdown через SupersetClientRegistry.shutdown(). Незавершённые задачи помечаются CANCELLED.

Task Manager

  1. Запуск задач: ThreadPoolExecutorasyncio.create_task. Задачи разделяют основной event loop.
  2. EventBus: queue.Queue + call_soon_threadsafeasyncio.Queue(maxsize=10000) с fanout на подписчиков.
  3. Lifecycle: Thread-based → async context manager.

Конфигурация

  1. Perenv: connection_pool_size, connection_pool_timeout, request_timeout, llm_timeout в EnvironmentConfig.
  2. Applevel: db_executor_workers, file_executor_workers, git_executor_workers, graceful_shutdown_timeout в AppAsyncRuntimeConfig.

Тестирование

  1. requests_mockpytest-httpx для мокирования асинхронных HTTP-вызовов.
  2. Тесты конкурентности с asyncio.gather для всех C3+ контрактов.
  3. Rejected-path regression тесты для каждого запрещённого паттерна.

Модули, затронутые рефакторингом

  • core/utils/: async_network.py (+semaphore), client_registry.py (new), executors.py (new), fileio.py (+async wrappers), network.py → Tombstone, superset_compilation_adapter.py → async
  • core/superset_client/: _base.py (+13 mixins) → все async
  • core/task_manager/: manager.py, event_bus.py, lifecycle.py → все async
  • api/routes/: dashboards, assistant, migration, datasets, git, environments, profile → все async
  • plugins/translate/: _llm_async_http.py (new), все файлы → async
  • plugins/: backup.py, git_plugin.py, llm_analysis/plugin.py, storage/plugin.py → async
  • services/: notifications/providers.py, notifications/service.py, git/_base.py → async
  • services/: git/_base.py, git/_branch.py, git/_sync.py, git/_status.py, git/_merge.py → async

[/DEF:ADR-0011:ADR]