Skip to content

Модульная декомпозиция: план миграции (Strangler Fig) #489

@ShaerWare

Description

@ShaerWare

Контекст

План поэтапной миграции от текущей монолитной архитектуры к модульной декомпозиции, описанной в #481.

Стратегия: Strangler Fig — новая структура создаётся рядом со старой, импорты перенаправляются постепенно, старые файлы становятся тонкими фасадами-реэкспортами, удаляются только когда все потребители мигрированы.

Основано на аудите: #480, целевая архитектура: #481.


Текущее состояние (три монолита)

Монолит Размер Проблема
orchestrator.py 4140 строк Startup (365 строк), 8 глобальных сервисов, ~100 legacy endpoints, 5 background tasks, регистрация 28 роутеров
db/models.py 3667 строк, 54 модели Все домены в одном файле
db/integration.py 2688 строк, 29 менеджеров Все менеджеры — синглтоны на уровне модуля

Что уже хорошо (не нужно трогать)

  • db/repositories/ — 45 файлов, чистая изоляция, не импортируют ничего из orchestrator/routers/services
  • Бот-подпроцессы (telegram_bot/, whatsapp_bot/) — уже общаются через HTTP API, не импортируют db/
  • Роутеры — уже 28 отдельных файлов в app/routers/

Ключевые ограничения и риски

1. Alembic-миграции

Существующие миграции делают from db.models import User, ChatSession, .... Перенос моделей без фасада-реэкспорта ломает alembic upgrade head.

Решение: db/models.py остаётся как фасад — импортирует из доменных модулей и реэкспортирует всё. Старые миграции работают без изменений.

2. SQLAlchemy Base.metadata

Все модели должны быть зарегистрированы в одном Base для:

  • Base.metadata.create_all() (автосоздание таблиц)
  • Relationship-ы между моделями разных доменов
  • Alembic autogenerate

Решение: Base остаётся в db/database.py. Доменные models.py импортируют оттуда. При старте все модели "видны" через явные импорты в точке входа.

3. Cross-domain FK

14 таблиц имеют workspace_id FK, owner_id → User.id повсеместно. Убирание FK ради "чистоты модулей" = потеря referential integrity без выигрыша.

Решение: Принять, что доменные модели знают о core-моделях (User, Workspace) — это нормальная зависимость "все зависят от core". Не убирать FK из существующих таблиц.

4. Параллельная разработка (local + server)

Массовый рефакторинг файловой структуры = merge-ад. Переносы файлов + правки в старых путях = конфликты на каждом PR.

Решение: Каждую фазу делает одна машина. Мелкие PR с чёткими границами. Вторая машина в это время не трогает мигрируемые файлы.


Фазы миграции

✅ Фаза 0: Инфраструктура core (нулевой риск)

Чисто аддитивно — создаём новые модули, не трогая существующий код

Создать modules/core/:

  • events.py — EventBus (in-process pub/sub)
  • health.py — HealthRegistry (модули регистрируют свои health checks)
  • tasks.py — TaskRegistry (named background tasks, cancel_all)
  • base.py — BaseService (если нужен общий интерфейс)

Критерий готовности: Новые модули импортируемы и покрыты тестами. Существующий код не изменён.

Зависимости: Нет.


Фаза 1: Разделение db/models.py (низкий риск)

Модели переезжают в доменные файлы, db/models.py становится фасадом

Целевая структура:

modules/
├── core/models.py         ← User, Role, RolePermission, UserSession,
│                             Workspace, WorkspaceMember, WorkspaceInvite,
│                             UserIdentity, SystemConfig
├── chat/models.py         ← ChatSession, ChatMessage, ChatSessionShare, ResourceShare
├── channels/
│   ├── telegram/models.py ← BotInstance, TelegramSession + Bot* (12 sales-моделей)
│   ├── whatsapp/models.py ← WhatsAppInstance
│   └── widget/models.py   ← WidgetInstance
├── llm/models.py          ← CloudLLMProvider, LLMPreset
├── knowledge/models.py    ← KnowledgeCollection, KnowledgeDocument, FAQEntry
├── speech/models.py       ← TTSPreset
├── crm/models.py          ← AmoCRMConfig, AmoCRMSyncLog
├── ecommerce/models.py    ← WooCommerceConfig
├── kanban/models.py       ← KanbanProject, KanbanTask, KanbanTaskDependency,
│                             KanbanChecklistItem, KanbanTaskStatus
├── claude_code/models.py  ← ClaudeCodeSession, ClaudeCodeProject
├── monitoring/models.py   ← AuditLog, UsageLog, UsageLimits
├── sales/models.py        ← PaymentLog
├── admin/models.py        ← UserConsent
└── telephony/models.py    ← GSMCallLog, GSMSMSLog, GitHubRepoProject

db/models.py остаётся — реэкспорт:

# db/models.py — фасад (backward compat для Alembic и старого кода)
from modules.core.models import User, Role, RolePermission, ...
from modules.chat.models import ChatSession, ChatMessage, ...
from modules.kanban.models import KanbanProject, KanbanTask, ...
# ... все 54 модели

Порядок: По одному домену за коммит. Начать с самых изолированных (kanban, claude_code, monitoring), заканчивать core (от которого все зависят).

Критерий готовности: alembic upgrade head работает. alembic revision --autogenerate видит все модели. Все тесты проходят. Все from db.models import X продолжают работать.

Зависимости: Нет (можно параллельно с Фазой 0).


Фаза 2: Разделение db/integration.py (низкий-средний риск)

Менеджеры переезжают в доменные service.py, db/integration.py становится фасадом

Паттерн:

# modules/chat/service.py
class ChatService:  # бывший AsyncChatManager
    ...

# db/integration.py — фасад
from modules.chat.service import ChatService as AsyncChatManager
async_chat_manager = AsyncChatManager()

29 менеджеров → ~14 доменных service.py (некоторые домены объединяют несколько менеджеров):

Домен Менеджеры → service.py
core AsyncUserManager, AsyncUserSessionManager, AsyncRoleManager, AsyncWorkspaceManager, AsyncUserIdentityManager, AsyncConfigManager
chat AsyncChatManager, AsyncChatShareManager
channels/telegram AsyncBotInstanceManager, AsyncTelegramSessionManager
channels/whatsapp AsyncWhatsAppInstanceManager
channels/widget AsyncWidgetInstanceManager
llm AsyncCloudProviderManager
knowledge AsyncKnowledgeDocManager, AsyncKnowledgeCollectionManager, AsyncFAQManager
speech AsyncPresetManager
crm AsyncAmoCRMManager
ecommerce AsyncWooCommerceManager
kanban AsyncKanbanManager, AsyncKanbanProjectManager
claude_code AsyncClaudeCodeManager, AsyncClaudeCodeProjectManager
monitoring AsyncAuditLogger, AsyncPaymentManager, DatabaseManager
admin AsyncResourceShareManager

Порядок: Начать с листовых доменов (ecommerce, claude_code, kanban), заканчивать core.

Критерий готовности: Все роутеры работают без изменений (импорт из db.integration по-прежнему валиден). Тесты проходят.

Зависимости: Фаза 1 (модели должны быть в доменах, чтобы service.py импортировал из своего домена).


Фаза 3: Перенос роутеров (средний риск)

app/routers/*.pymodules/{domain}/router.py, фасад в app/routers/__init__.py

Паттерн:

# modules/chat/router.py — реальный код
router = APIRouter(prefix="/admin/chat", tags=["chat"])
...

# app/routers/chat.py — фасад
from modules.chat.router import router  # noqa: F401

28 роутеров → ~14 доменов:

Домен Роутеры
core auth.py, roles.py, workspace.py
chat chat.py
channels/telegram telegram.py
channels/whatsapp whatsapp.py
channels/widget widget.py
llm llm.py
knowledge wiki_rag.py
speech tts.py, stt.py, services.py
crm amocrm.py
ecommerce woocommerce.py
kanban kanban.py, github_repos.py
claude_code claude_code.py
monitoring audit.py, usage.py, monitor.py
sales bot_sales.py, yoomoney_webhook.py
admin backup.py, legal.py, faq.py, github_webhook.py
telephony gsm.py

На этом этапе каждый домен имеет: models.py, service.py, router.py — базовая структура из #481.

Параллельно: Обновить импорты в роутерах — вместо from db.integration import Xfrom modules.{domain}.service import X.

Критерий готовности: orchestrator.py регистрирует роутеры через ту же точку входа (app/routers/__init__.py). Все endpoints работают.

Зависимости: Фаза 2.


Фаза 4: Разборка orchestrator.py (высокий риск)

Самая сложная фаза — разбить 4140 строк на управляемые части

Подэтапы:

4a. Вынести legacy endpoints

~100 legacy endpoints (OpenAI-compatible /v1/*, widget endpoints, helper functions) → отдельные роутеры:

  • /v1/*modules/compat/router.py
  • Widget endpoints (из orchestrator.py) → уже должны быть в modules/channels/widget/router.py (после Фазы 3)
  • StreamingTTSManagermodules/speech/streaming.py

4b. Модульный startup

Заменить 365-строчный startup_event() на:

for module in enabled_modules:
    svc = module.create_service(db, **deps)
    app.include_router(module.router)

Каждый модуль получает create_service() → возвращает инициализированный сервис.

4c. Background tasks → TaskRegistry

5 create_task() без сохранения references → TaskRegistry из Фазы 0:

tasks.register("session_cleanup", chat_svc.periodic_cleanup, interval=3600)
tasks.register("vacuum", db.periodic_vacuum, interval=7*24*3600)
tasks.register("kanban_sync", kanban_svc.periodic_sync, interval=900)

4d. Graceful shutdown

shutdown_event()tasks.cancel_all() → channels.stop_all_bots() → bridge.stop() → db.close()

4e. Deployment modes через модульную загрузку

MODULES = {
    "full":  [..., speech, telephony],
    "cloud": [...],  # speech и telephony не загружаются
}

Целевой orchestrator.py (или app.py): ~100-150 строк.

Критерий готовности: Health check работает. Все endpoints доступны. Боты стартуют. Graceful shutdown корректен.

Зависимости: Фазы 0-3.


Фаза 5: EventBus для cross-module коммуникации (низкий-средний риск)

Заменить прямые импорты между доменами на события

Приоритетные события:

Событие Публикует Подписчик Что заменяет
KnowledgeUpdated knowledge llm (reload FAQ cache) Прямой вызов из wiki_rag роутера
WidgetSessionCreated channels/widget crm (create lead) create_task(_widget_create_amocrm_lead) в orchestrator.py
WidgetMessageSent channels/widget crm (append note) Прямой вызов в widget endpoint
UserRoleChanged core/auth core/cache (invalidation) Ручной cache.invalidate()
DatasetSynced crm, ecommerce knowledge (reindex) Прямой вызов reindex в amocrm/woocommerce роутерах
ConfigChanged core/config affected modules Нет (сейчас требует рестарт)

По одному событию за PR. Каждое — отдельная дочерняя issue.

Критерий готовности: Прямые импорты между несвязанными доменами заменены на подписки. Граф зависимостей соответствует #481.

Зависимости: Фаза 4 (EventBus должен быть интегрирован в startup).


Фаза 6: Protocol interfaces (низкий риск, можно делать в любой момент после Фазы 2)

Типизация контрактов между модулями

Определить Protocol-классы для:

  • KnowledgeService (search, search_multi, get_collections)
  • LLMService (generate, stream, resolve_backend)
  • ChatService (create_session, send_message, stream_message)

Это не ломает код — просто добавляет типизацию для mypy и документации.


Граф зависимостей между фазами

Фаза 0 (core infra) ─────────────────────────────────┐
         │                                             │
Фаза 1 (split models) ─── можно параллельно ──────────┤
         │                                             │
Фаза 2 (split integration) ───────────────────────────┤
         │                                             │
Фаза 3 (move routers) ────────────────────────────────┤
         │                                             │
Фаза 4 (разборка orchestrator) ───────────────────────┤
         │                                             │
Фаза 5 (EventBus) ────────────────────────────────────┘
                                                       │
Фаза 6 (Protocols) ── можно в любой момент после Ф2 ──┘

Оценка объёма

Фаза Риск Оценка Кол-во PR
0: Core infra Нулевой S 1
1: Split models Низкий M 3-5 (по группам доменов)
2: Split integration Низкий-средний L 5-7 (по доменам)
3: Move routers Средний M 3-5
4: Разборка orchestrator Высокий XL 5-8 (по подэтапам)
5: EventBus Низкий-средний M 5-6 (по событиям)
6: Protocols Низкий S 1-2

Итого: ~25-35 PR, каждый — небольшой и ревьюабельный.


Правила миграции

  1. Никогда не ломать from db.models import X — фасад-реэкспорт обязателен
  2. Никогда не ломать from db.integration import X — фасад-реэкспорт обязателен
  3. Один домен за PR — не мигрировать несколько доменов в одном PR
  4. Тесты + lint на каждом PR — CI должен быть зелёным
  5. alembic upgrade head — обязательная проверка на каждом PR с моделями
  6. Новые миграции импортируют из modules/ — старые остаются с db.models
  7. Фасады удаляются только когда ВСЕ потребители мигрированы (отдельный PR, в конце)

Дочерние issues

Для каждой фазы и подэтапа будут созданы отдельные issues с конкретными задачами, чеклистами и acceptance criteria. Они будут привязаны к этому issue как parent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priorityphase:5-techdebtPhase 5: Technical DebtrefactoringArchitectural refactoring

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions