# Data Model: Async Backend Refactoring

**Feature**: `032-translate-requests-httpx`
**Date**: 2026-06-04
**Status**: ✅ Implemented

## Overview

Рефакторинг **не добавляет новых бизнес-таблиц** (SQLAlchemy models) и **не меняет публичные request/response Pydantic-схемы**. Добавляются транспортные/runtime-сущности (`SupersetClientRegistry`, `run_blocking`, bounded executors) и расширяется конфигурация окружений (`EnvironmentConfig`) и приложения (`AppAsyncRuntimeConfig`). Требование миграции БД зависит от формата хранения `EnvironmentConfig`: если JSON/Pydantic blob — defaults через `Field(default=...)` достаточны; если SQL‑колонки — нужна Alembic-миграция.

## 1. Changed Entities (Transport Layer)

### AsyncAPIClient (replaces APIClient)

| Attribute | Type | Change |
|-----------|------|--------|
| `session` | `requests.Session` → `httpx.AsyncClient` | Замена sync клиента на async |
| `base_url` | `str` | Без изменений |
| `csrf_token` | `str` | Без изменений (кешируется) |
| `semaphore` | **NEW** `asyncio.Semaphore` | Ограничение конкурентных соединений |
| `timeout` | **NEW** `httpx.Timeout` | Настраиваемый таймаут (default: 30s) |

**Lifecycle**: Клиент создаётся через `async with AsyncAPIClient(env_config) as client:` — контекстный менеджер управляет жизненным циклом `httpx.AsyncClient` (открытие/закрытие connection pool).

### SupersetClient (merged sync + async)

| Attribute | Type | Change |
|-----------|------|--------|
| `network` | `APIClient` → `AsyncAPIClient` | Замена sync на async |
| All mixin methods | `def` → `async def` | Все 13 миксинов становятся async |

**Methods (typical signature change)**:
```python
# Before (sync)
def get_dashboards(self, query=None) -> tuple[int, list[dict]]: ...

# After (async)
async def get_dashboards(self, query=None) -> tuple[int, list[dict]]: ...
```

### SupersetSqlLabExecutor

| Attribute | Type | Change |
|-----------|------|--------|
| `_client` | `SupersetClient` (sync) | Замена на async |
| `execute_sql()` | `def` → `async def` | Асинхронный HTTP + поллинг |
| `poll_execution_status()` | `def` → `async def` | `time.sleep` → `asyncio.sleep` |
| `max_polls` | `int` (60) | Без изменений |
| `poll_interval_seconds` | `float` (2.0) | Без изменений |

### TaskManager

| Attribute | Type | Change |
|-----------|------|--------|
| `executor` | `ThreadPoolExecutor` | **Удалён** |
| `_tasks` | `dict[str, asyncio.Task]` | **NEW** — словарь активных asyncio.Task |
| `run_task()` | `def` → `async def` | Запуск через `asyncio.create_task` |
| `cancel_task()` | `def` → `async def` | Отмена через `task.cancel()` |

### EventBus

| Attribute | Type | Change |
|-----------|------|--------|
| `_queue` | `queue.Queue` → `asyncio.Queue` | Замена sync очереди на async |
| `publish()` | `def` → `async def` | `put_nowait` → `await put` |
| `subscribe()` | `def` → `async def` | `get` → `await get` |

### LLM Client (unified)

| Attribute | Type | Change |
|-----------|------|--------|
| `call_openai_compatible()` | `def` → `async def` | Асинхронный HTTP |
| `_do_http_request()` | `requests.post` + `time.sleep` | `httpx.AsyncClient.post` + `asyncio.sleep` |
| `timeout` | `int` (180) → `httpx.Timeout(180)` | Настраиваемый таймаут |

### Notification Providers

| Provider | Method | Change |
|----------|--------|--------|
| SMTPProvider | `send()` | `smtplib.SMTP` → `aiosmtplib.SMTP` |
| TelegramProvider | `send()` | `requests.post` → `httpx.AsyncClient.post` |
| SlackProvider | `send()` | `requests.post` → `httpx.AsyncClient.post` |

## 2. New Entities

### SupersetClientRegistry [C:4]

Синглтон-реестр долгоживущих асинхронных клиентов по `env_id`:

| Attribute | Type | Description |
|-----------|------|-------------|
| `_clients` | `dict[str, _ClientSlot]` | env_id → slot с shared ресурсами |
| `_ClientSlot.client` | `httpx.AsyncClient` | Долгоживущий HTTP-клиент (connection pooling) |
| `_ClientSlot.semaphore` | `asyncio.Semaphore` | Глобальный per‑env семафор (connection_pool_size) |
| `_ClientSlot.auth_lock` | `asyncio.Lock` | Защита конкурентного обновления CSRF/auth‑токенов |

**Lifecycle**: Клиенты создаются при первом обращении к `env_id`, живут до shutdown. `async with` — только для тестов.

### DB Threading Contract

SQLAlchemy‑сессии создаются, используются, коммитятся/откатываются и закрываются **внутри одного worker‑потока** (через `asyncio.to_thread(db_executor, ...)`). Никакие ORM‑объекты не пересекают границу потока без явного detach/serialization. DB‑адаптер владеет жизненным циклом сессии.

## 3. Configuration Parameters

### Environment Config (per Superset instance — `EnvironmentConfig`)

```json
{
  "connection_pool_size": 20,
  "connection_pool_timeout": 30,
  "request_timeout": 30,
  "llm_timeout": 120
}
```

### App-Level Async Runtime Config (global — отдельная секция конфигурации приложения, не per‑env)

```json
{
  "db_executor_workers": 10,
  "file_executor_workers": 10,
  "git_executor_workers": 5,
  "graceful_shutdown_timeout": 30
}
```

Эти параметры добавляются в модель `EnvironmentConfig` (Pydantic) и хранятся в БД как часть конфигурации окружения.

## 3. Unchanged Entities

Следующие сущности **не меняют** свою структуру, только способ взаимодействия с внешними сервисами:

| Entity | Type | Notes |
|--------|------|-------|
| `TranslationJob` | SQLAlchemy Model | Без изменений |
| `TranslationPreviewSession` | SQLAlchemy Model | Без изменений |
| `TranslationPreviewRecord` | SQLAlchemy Model | Без изменений |
| `TranslationPreviewLanguage` | SQLAlchemy Model | Без изменений |
| `Dictionary` | SQLAlchemy Model | Без изменений |
| `LLMProvider` | SQLAlchemy Model | Без изменений |
| `EnvironmentConfig` | SQLAlchemy Model | Добавлены pool-параметры (см. выше) |
| Все Pydantic request/response schemas | Pydantic | Без изменений (FR-018) |
| Все API routes (URL, method, status codes) | FastAPI Router | Без изменений |
| `PluginBase.execute()` contract | ABC | Уже `async def`, без изменений |

## 4. Task State Transitions (Unchanged)

```
PENDING → RUNNING → COMPLETED
                  → FAILED
                  → CANCELLED
```

Переходы состояний не меняются. Меняется только механизм запуска: `ThreadPoolExecutor.submit()` → `asyncio.create_task()`.

## 5. WebSocket Message Format (Unchanged)

```json
{
  "type": "log",
  "task_id": "uuid",
  "timestamp": "ISO-8601",
  "level": "INFO|WARNING|ERROR",
  "message": "...",
  "trace_id": "uuid"
}
```

Формат сообщений WebSocket не меняется. Меняется только транспорт отправки: `queue.Queue` + `call_soon_threadsafe` → `asyncio.Queue` + `await`.
