Skip to content

Latest commit

 

History

History
307 lines (235 loc) · 16.7 KB

File metadata and controls

307 lines (235 loc) · 16.7 KB

Архитектура для разработчика

Этот документ отвечает на практический вопрос: в какой крейт положить новый код и через какие границы его провести.

Полная целевая архитектура зафиксирована в README.md. Здесь описаны действующие границы workspace и правила разработки.

Crate DAG

В workspace ровно шесть локальных крейтов:

                         ┌──────────────┐
                         │    server    │  CLI, HTTP
                         └──────┬───────┘
                                │
                         ┌──────▼───────┐
                         │     app      │  use cases, wiring
                         └──┬────┬────┬─┘
                            │    │    │
              ┌─────────────┘    │    └─────────────┐
              │                  │                  │
       ┌──────▼──────┐    ┌──────▼──────┐    ┌──────▼──────┐
       │    store    │    │   render    │    │   search    │
       └──────┬──────┘    └──────┬──────┘    └──────┬──────┘
              │                  │                  │
              └─────────────┬────┴────┬─────────────┘
                            │  core   │
                            └─────────┘

В терминах разрешённых локальных зависимостей:

server -> app
app    -> core + store + render + search
store  -> core
render -> core
search -> core
core   -> ни от одного локального крейта

Направление проверяет:

python3 scripts/check_architecture.py

Скрипт читает реальный cargo metadata, поэтому добавить запрещённую зависимость «только на время» незаметно не получится.

Ответственность крейтов

wikinext-core

Чистая доменная модель:

  • типизированные UserId, PageId, GroupId;
  • действия, входы и решения compatibility ACL;
  • в будущем — страницы, ревизии, команды и доменные invariants.

Здесь не должно быть Axum, SQLx, HTTP-клиентов, файловой системы, Tokio-задач или знания о формате таблиц. Доменное правило должно проверяться обычным детерминированным unit-тестом.

Текущий API разобран в core-development.md.

wikinext-store

Адаптеры устойчивого хранения:

  • bounded PostgreSQL pool;
  • встроенные forward-only migrations;
  • проверка PostgreSQL и версии схемы;
  • deadline для probes/migrations;
  • подготовка приватных локальных каталогов blobs/ и tmp/ без symlink ancestors и переходов через ...

Доменные repositories и транзакции страниц пока относятся к roadmap. store может переводить строки БД в типы core, но не принимает HTTP-решений и не вызывает render или search.

wikinext-render

Граница обработки Wikidot-разметки. Сейчас здесь есть только закреплённый профиль FTML 1.41.0 и исполняемые compatibility probes.

Production include resolver, module registry, hooks, sanitizer, cache и ограниченный blocking executor ещё не реализованы. Поэтому результат HtmlRender сейчас нельзя напрямую отдавать пользователю.

Подробнее: compat/ftml-1.41.md. Точную выбранную версию и Cargo feature surface дополнительно фиксирует python3 scripts/check_ftml_contract.py.

wikinext-search

Изолирует Meilisearch. Текущий SearchClient:

  • проверяет URL и разрешает только HTTP(S);
  • не принимает credentials/query/fragment в URL;
  • помечает ключ как sensitive header;
  • запрещает redirects и proxy для диагностических запросов;
  • ограничивает timeout и размер ответа;
  • проверяет health и точную версию Meilisearch.

Индексация, outbox consumer и reindex пока относятся к roadmap.

wikinext-app

Прикладной слой и composition root:

  • загрузка и проверка конфигурации;
  • env overrides;
  • отдельный command-scoped MigrationConfig, не загружающий search/storage секреты;
  • сборка AppServices;
  • orchestration для migrate, doctor, readiness и search status;
  • Argon2id PasswordService.

Будущие use cases должны жить здесь: слой координирует доменное правило и адаптеры, но не знает о JSON, cookies, HTTP status codes или Axum extractors.

PasswordService имеет синхронный CPU-intensive API. Пока auth flow не реализован, он не вызывается из async HTTP handler. При интеграции в M1 такие операции должны идти через ограниченный blocking executor, а не блокировать Tokio worker.

wikinext-server

Внешняя транспортная граница:

  • Clap CLI;
  • Axum router;
  • HTTP status/JSON;
  • request IDs, tracing, timeout и security headers;
  • graceful shutdown.

Handler должен быть тонким: разобрать и ограничить внешний ввод, вызвать app, затем перевести типизированный результат в безопасный HTTP-ответ. Доменное правило или SQL внутри handler — признак неверной границы.

Как выбрать крейт

Задайте вопросы по порядку:

  1. Это правило wiki, которое можно вычислить без I/O? Тогда core.
  2. Это чтение/запись PostgreSQL или локальных файлов? Тогда store.
  3. Это parsing/rendering Wikidot? Тогда render.
  4. Это общение с Meilisearch? Тогда search.
  5. Это законченная операция, координирующая правила и адаптеры? Тогда app.
  6. Это HTTP, CLI, JSON, headers или process lifecycle? Тогда server.

Если код «нужен сразу в двух соседних адаптерах», обычно координация принадлежит app. Не связывайте store, render и search друг с другом напрямую.

Вертикальный путь новой функции

Не каждая функция обязана менять все шесть крейтов. Правильный путь проходит только через нужные слои:

внешний запрос
    │
    ▼
server: validation transport-формата
    │
    ▼
app: use case и порядок шагов
    │
    ├──► core: доменные типы, переход состояния, проверка прав
    │
    ├──► store: транзакционная запись/чтение
    │
    ├──► render: безопасный render, если он нужен
    │
    └──► search: enqueue/index operation, если она нужна
    │
    ▼
server: безопасный HTTP/CLI результат

Порядок реализации

  1. Зафиксируйте контракт. Для совместимости опишите наблюдаемое поведение и truth table в docs/compat/; для нового поведения сформулируйте invariants.
  2. Добавьте домен. Типы и чистые правила идут в core вместе с unit- и property-тестами.
  3. Добавьте нужные адаптеры. SQL и migration — в store; FTML — в render; Meilisearch — в search. Пропустите ненужные адаптеры.
  4. Соберите use case. app определяет transaction boundary, порядок проверок, deadlines и компенсацию производных операций.
  5. Откройте transport. server добавляет request/response DTO, ограничения ввода, route/CLI и отображение ошибок.
  6. Закройте вертикаль тестами. Unit-тесты домена, adapter/integration-тесты, handler-тест и негативные security cases.
  7. Обновите документацию. Уберите пометку roadmap только после появления рабочего API и проверок.

Пример будущей функции: создание страницы

Это roadmap blueprint, не существующий API:

  • core задаст валидированную команду создания, идентификаторы и invariants;
  • store атомарно запишет страницу, первую ревизию, hash chain и outbox row;
  • app проверит ACL и вызовет транзакцию;
  • server ограничит размер тела, распарсит запрос и вернёт корректный status;
  • render не обязан быть частью write transaction;
  • search не вызывается синхронно из транзакции: будущий worker прочитает outbox и обновит производный индекс.

Этот пример показывает границы, но не обещает имена будущих Rust-типов.

Общие инженерные правила

Ошибки

  • На границе крейта используйте типизированный thiserror enum.
  • Сохраняйте исходную ошибку через #[source]/#[from], но давайте ей безопасный стабильный контекст.
  • Не возвращайте наружу сырой текст SQLx, Reqwest, Argon2 или файловой системы.
  • Невалидное или неполное состояние безопасности обрабатывайте fail-closed.
  • Не превращайте ожидаемую ошибку пользователя в panic.
  • Transport отдельно решает, что логировать, а что показывать клиенту.

Текущие примеры: StoreError, StorageError, SearchError, ConfigError, PasswordError, StartupError, ServerError.

Секреты

  • Никогда не помещайте пароли, DSN, API keys, session tokens или исходный password в tracing fields и тексты ошибок.
  • Для содержащих секрет структур пишите редактированный Debug. Текущие DatabaseOptions и конфигурационные secrets уже следуют этому правилу.
  • Помечайте auth headers как sensitive.
  • Не передавайте секретный header через redirects или недоверенный proxy.
  • В тестах используйте только заведомо фиктивные значения.

Async и blocking

  • PostgreSQL, HTTP и файловые операции выполняются асинхронно.
  • CPU-heavy parsing, sanitizing и Argon2 нельзя выполнять прямо на Tokio worker.
  • Для них нужен bounded blocking executor: ограниченная очередь, ограниченное число задач, deadline и понятная ошибка перегрузки.
  • Не используйте неограниченный spawn_blocking на каждый пользовательский запрос.
  • На внешнем I/O всегда должны быть timeout и предел размера ответа/ввода.

Ограниченный executor для auth/render — roadmap; это обязательное условие перед подключением соответствующего синхронного API к HTTP.

unwrap, expect, panic и unsafe

  • В runtime-коде не используйте unwrap() и expect() на данных, конфигурации, I/O и состоянии, которое может быть некорректным.
  • expect() допустим в тестах для создания fixture и улучшения сообщения о поломке самого теста.
  • Panic не является обработкой ошибки пользователя. HTTP panic catcher — последняя защита процесса, а не штатный control flow.
  • Workspace запрещает unsafe через unsafe_code = "forbid".

Ресурсы и производительность

  • Каждая коллекция, тело запроса, recursion/include depth и очередь должны иметь обоснованный предел.
  • Используйте bounded pool и backpressure, а не бесконечное накопление задач.
  • Не держите SQL transaction открытой во время render или сетевого вызова к Meilisearch.
  • Перед добавлением реальных blob-операций открывайте файлы относительно заранее открытого dirfd с openat2/O_NOFOLLOW (или безопасным платформенным эквивалентом). Текущая проверка пути нужна для bootstrap, но сама по себе не является защитой от TOCTOU при конкурентной замене path.
  • PostgreSQL остаётся источником истины; индекс и render cache производны.
  • Сначала измеряйте hot path, затем оптимизируйте без нарушения границ.

Публичные API и внутренняя структура

Экспортируйте из lib.rs только устойчивую границу крейта. Внутренние модули можно оставлять private и переэкспортировать нужные типы:

mod implementation;

pub use implementation::{PublicError, PublicService};

Это позволяет менять раскладку файлов без переписывания потребителей. Не экспортируйте конкретный SQLx pool, Reqwest client или Axum type из нижнего слоя, если потребителю достаточно доменного интерфейса.

Архитектурный checklist

Перед реализацией проверьте:

  • правило находится в самом нижнем подходящем слое;
  • новая локальная зависимость разрешена DAG;
  • server не содержит SQL и доменных решений;
  • адаптеры не вызывают друг друга;
  • внешний ввод ограничен и валидирован;
  • секреты не попадают в Debug, ошибки и tracing;
  • blocking work не выполняется на async worker;
  • ошибочное состояние закрывает доступ, а не открывает его;
  • roadmap API не выдан за уже реализованный.

Полный критерий готовности: testing.md.