# Архитектура Indexium — Асинхронный событийный индексатор > Цель: сделать сервис максимально лёгким, дешёвым в обслуживании и устойчивым к ограничениям GitHub. > Принцип: бэкенд **не хранит** тяжёлые файлы — артефакты отдаются с GitHub Releases CDN, мы валидируем, индексируем метаданные и выдаём быстрые JSON-ответы. --- ## 1. Обзор системных компонентов ``` ┌───────────────────────────────────────────────┐ │ GitHub Ecosystem │ └───────┬───────────────────────────────▲───────┘ │ │ │ 1. Webhook (release.published)│ 4. Read Assets / Metadata ▼ │ ┌─────────────────────────────────────────────────────────┴────────────────────────────────┐ │ Ваш Бэкенд (Event-Driven Indexer) │ │ │ │ ┌───────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │ │ │ Webhook Ingestion API │ ────> │ Async Job Queue │ ────> │ Worker / Indexer │ │ │ │ (Fast Signature Check)│ │ (Redis / NATS) │ │ (Jar Parser & AST) │ │ │ └───────────────────────┘ └──────────────────────┘ └──────────┬──────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ │ │ │ Edge Public REST/v1 │ <──── │ Read-Heavy Cache │ <──── │ Relational DB │ │ │ │ (High Throughput API) │ │ (Redis Key-Value) │ │ (PostgreSQL + FTS) │ │ │ └───────────────────────┘ └──────────────────────┘ └─────────────────────┘ │ └─────────────────────────────────────────────▲────────────────────────────────────────────┘ │ │ 5. Query Mods / Index │ ┌───────────┴───────────┐ │ Web UI / Launchers │ └───────────────────────┘ ``` ### Потоки данных 1. **Ingestion** — GitHub шлёт webhook → Ingestion API валидирует HMAC → кладёт job в очередь → отвечает `202`. 2. **Indexing** — Worker читает job → делает `Range Request` к `.jar` → парсит манифест → пишет в Postgres → инвалидирует кэш. 3. **Serving** — Лаунчер / Web UI дергает `GET /api/v1/mods` → читаем Redis → miss → Postgres FTS → кэшируем 60s → отдаём JSON. --- ## 2. Выбор технологического стека | Слой | Технология | Почему именно это? | | --- | --- | --- | | **Backend Core** | **Rust (Axum)** | Минимальный memory footprint, высокий throughput, быстрый async I/O при парсинге. Альтернатива Go (Fiber) — допустима на Phase 2. | | **Main Database** | **PostgreSQL** | Нативный FTS, `JSONB` для зависимостей, `pg_trgm` для fuzzy, `pgvector` опционально для семантики. | | **Cache & Queue** | **Redis / Valkey** | Одновременно брокер очередей (Streams/PubSub) и L2-кэш популярных эндпоинтов. | | **Worker Engine** | **Rust Background Worker (tokio)** | Потребляет webhook-события, скачивает только zip-header stream, валидирует байткод. | | **Auth & Security** | **GitHub App / OAuth 2.0** | Вход только через GitHub, без локальных паролей. | | **Frontend** | **SvelteKit + TypeScript** | SSR, маленький бандл, быстрый dev. | > Обоснование против Elasticsearch/Meilisearch на старте: `to_tsvector` + `pg_trgm` переваривают десятки тысяч модов с <5ms без отдельного кластера. Миграция на внешний поиск — только если FTS упрётся. --- ## 3. Жизненный цикл релиза (Release Lifecycle) ### 3.1 Регистрация мода (Onboarding) 1. Разработчик логинится через GitHub OAuth. 2. Жмёт «Импортировать репозиторий» → сервис проверяет наличие манифеста (`fabric.mod.json`, `neoforge.mods.toml`, `quilt.mod.json`) в default branch. 3. Устанавливается GitHub App + Webhook на события `release` и `push`. ### 3.2 Обработка webhook (Ingestion & Job Dispatch) 1. GitHub шлёт `release.published`. 2. **Ingestion API** валидирует `X-Hub-Signature-256` (HMAC SHA-256 с `WEBHOOK_SECRET`), проверяет идемпотентность по `X-GitHub-Delivery`. 3. Кладёт задачу в Redis Queue, отвечает `202 Accepted` за <50ms (чтобы не висеть по таймауту GitHub — 10s). ### 3.3 Работа воркера-индексатора (Worker Execution) — детальный алгоритм `jar_parser.rs` > Цель: не скачивать весь `.jar` (может быть 20–50MB), а прочитать только нужный манифест через 2–3 Range-запроса. ``` download_url = "https://github.com/owner/repo/releases/download/v1.2.3/mod-1.2.3.jar" │ Step 1: HEAD ─────┤ ▼ Content-Length: 12345678 Accept-Ranges: bytes (если нет Content-Length → GET Range: bytes=0-0 + парс Content-Range) │ Step 2: GET tail ─┤ Range: bytes=-65536 (последние 64KB) ▼ Найти EOCD (End of Central Directory) = 0x06054b50 Из EOCD: central_dir_offset, central_dir_size, num_entries │ Step 3: GET central dir ─┤ Range: bytes=central_dir_offset..central_dir_offset+size ▼ Парс Central Directory headers (0x02014b50) Найти entry: fabric.mod.json | quilt.mod.json | neoforge.mods.toml | mcmod.info + icon (assets//icon.png если указан в манифесте) → получить local_header_offset, compressed_size │ Step 4: GET manifest ─┤ Range: bytes=local_header_offset.. + compressed_size + header ▼ Распаковать (DEFLATE/STORE), парс JSON/TOML, валидация + SHA-256 сверка, file_size = Content-Length ``` **Детали реализации:** 1. `HEAD` — обязателен, чтобы получить `Content-Length` и убедиться `Accept-Ranges: bytes`. Таймаут 5s, retry 2. 2. Последние 64KB достаточно для EOCD даже для jar с 10k файлов (EOCD в конце). Если не найден — fallback к последним 128KB. 3. Central Directory читается одним запросом (обычно 5–30KB). Парсим `central_dir_offset/size` из EOCD. 4. Манифест — 4-й запрос только если нужен (часто 1–3KB). Иконка — опционально 5-й запрос, кэшируется и отдаётся через `GET /mods/:slug/icon`. 5. Все `GET` — `reqwest` с `header("Range", ...)`, проверка `206 Partial Content`, иначе fallback к полному скачиванию с лимитом 10MB. **Ошибки:** `412` если `Accept-Ranges` != bytes → full download; `404` на Range → retry без Range; повреждённый ZIP → помечаем `suspicious` и DLQ. Код: `indexium-backend/src/worker/jar_parser.rs` — чистые функции `find_eocd()`, `parse_central_dir()`, `fetch_manifest()` без I/O в тестах. ### 3.4 Агрегация и индексация (Storage & Cache Invalidation) 1. Сохраняет версию в `mod_versions` (см. `database-schema.md`). 2. `download_url` = прямая ссылка `https://github.com/.../releases/download/...` (CDN `objects.githubusercontent.com`). 3. Инвалидирует / обновляет Redis-кэш для поиска и карточки мода. --- ## 4. Обход ключевых ограничений (Edge Cases) ### GitHub Rate Limits - Запросы воркеров — от имени **GitHub App Installs** (5k–12.5k RPH на инсталл vs 60 RPH анонимных). - Скачивание не проксируем — выдаём клиентам прямые CDN-ссылки, трафик не идёт через нас. ### Безопасность (Malware Protection) - Сверка `SHA-256` ассета с `.sha256` если есть. - Сканирование байткода: флаг на `Runtime.getRuntime().exec()`, `ProcessBuilder`, `URLClassLoader`, сетевые вызовы в `` / `FabricModInitializer`. - Карантин: помечаем версию `suspicious = true`, не показываем в публичном поиске до ручной проверки. ### Поиск без Elasticsearch - Postgres `to_tsvector('russian'|'english', name || summary)` + `GIN`. - `pg_trgm` (`similarity()`, `%` оператор) для опечаток. - Материализованный `search_vector` + триггер на update. ### Надёжность очереди - Retry с exponential backoff (3 попытки), DLQ (dead-letter) для ручного разбора. - Идемпотентный воркер: `ON CONFLICT (mod_id, version_number) DO UPDATE`. ### Телеметрия (bStats аналог, см. docs/analytics.md) - **Ingestion:** `POST /api/v1/analytics/submit` (gzip JSON, без IP логов) → валидация allow-list → `server_hash = sha256(uuid + daily_salt)` → Redis rate limit 1/15мин → `INSERT mod_telemetry_pings`. - **Aggregation:** кроном раз в час `COUNT(DISTINCT server_hash)` + `breakdown_json` → `mod_daily_stats`, TTL 30 дней для сырых пингов (`DELETE WHERE pinged_at < NOW()-30d`). - **Serving:** `GET /mods/:slug/analytics?range=30d` (кэш 5 мин) + `GET /badges/:slug/servers.svg` + `sort=active_servers` для честной сортировки по реальным установкам, а не накрученным скачиваниям. - **Масштаб:** Postgres хватает до 10M пингов/мес, далее TimescaleDB hypertable без смены схемы. --- ## 5. Схема структуры БД (Core Entity Relation) См. детально в [`database-schema.md`](database-schema.md). Коротко: ```sql -- Таблица модов CREATE TABLE mods ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), github_repo_id BIGINT UNIQUE NOT NULL, slug VARCHAR(64) UNIQUE NOT NULL, name VARCHAR(128) NOT NULL, summary TEXT, author_github_id BIGINT NOT NULL, default_branch VARCHAR(32) DEFAULT 'main', created_at TIMESTAMPTZ DEFAULT now() ); -- Таблица версий (релизов) CREATE TABLE mod_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), mod_id UUID REFERENCES mods(id) ON DELETE CASCADE, version_number VARCHAR(32) NOT NULL, game_versions VARCHAR(32)[] NOT NULL, -- e.g. ['1.20.1', '1.20.2'] loaders VARCHAR(16)[] NOT NULL, -- e.g. ['fabric', 'quilt'] download_url TEXT NOT NULL, -- GitHub Release Direct Asset URL file_sha256 CHAR(64) NOT NULL, published_at TIMESTAMPTZ NOT NULL, UNIQUE(mod_id, version_number) ); CREATE INDEX idx_versions_lookup ON mod_versions USING GIN (game_versions, loaders); ``` Дополнительно: `authors`, `webhook_deliveries` (идемпотентность), `search_vector`, `mod_telemetry_pings`/`mod_daily_stats` (аналитика). --- ## 6. Масштабирование и эволюция | Этап | Нагрузка | Действие | |------|----------|----------| | MVP | <10k модов, <100 RPS | Один инстанс Axum + Postgres + Redis, воркер в том же бинаре (tokio spawn) | | Growth | 10k-100k модов, 1k RPS | Вынос воркера в отдельный деплой, реплика Postgres RO, Redis Cluster | | Scale | 100k+ модов, 10k RPS | NATS JetStream вместо Redis Streams, read-replica + шардирование по `slug`, CDN перед API (Cloudflare) | --- ## 7. Нефункциональные требования - **Latency**: `GET /mods` p95 < 80ms (cache hit), < 200ms (cache miss + FTS). - **Availability**: 99.9% (допустим 43 мин downtime/мес на MVP). - **Cost**: < $20/мес на MVP (1 VPS Hetzner + managed Postgres free tier). - **Security**: HMAC, GitHub App, no local passwords, malware scan. --- ## 8. Диаграмма деплоя (MVP) ``` [GitHub] --webhook--> [Axum Ingestion :3000] --> [Redis :6379] --> [Worker] | | v v [Postgres :5432] <--+ | [Browser/Launcher] --> [Axum Public API :3000] --> [Redis Cache] \ --> [SvelteKit :5173] (SSR, fetch API) ``` Все сервисы — `docker compose` локально, один VPS в проде.