Indexium/docs/architecture.md

219 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Архитектура 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/<modid>/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`, сетевые вызовы в `<clinit>` / `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 в проде.