15 KiB
Архитектура 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 │
└───────────────────────┘
Потоки данных
- Ingestion - GitHub шлёт webhook → Ingestion API валидирует HMAC → кладёт job в очередь → отвечает
202. - Indexing - Worker читает job → делает
Range Requestк.jar→ парсит манифест → пишет в Postgres → инвалидирует кэш. - 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)
- Разработчик логинится через GitHub OAuth.
- Жмёт «Импортировать репозиторий» → сервис проверяет наличие манифеста (
fabric.mod.json,neoforge.mods.toml,quilt.mod.json) в default branch. - Устанавливается GitHub App + Webhook на события
releaseиpush.
3.2 Обработка webhook (Ingestion & Job Dispatch)
- GitHub шлёт
release.published. - Ingestion API валидирует
X-Hub-Signature-256(HMAC SHA-256 сWEBHOOK_SECRET), проверяет идемпотентность поX-GitHub-Delivery. - Кладёт задачу в 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
Детали реализации:
HEAD- обязателен, чтобы получитьContent-Lengthи убедитьсяAccept-Ranges: bytes. Таймаут 5s, retry 2.- Последние 64KB достаточно для EOCD даже для jar с 10k файлов (EOCD в конце). Если не найден - fallback к последним 128KB.
- Central Directory читается одним запросом (обычно 5–30KB). Парсим
central_dir_offset/sizeиз EOCD. - Манифест - 4-й запрос только если нужен (часто 1–3KB). Иконка - опционально 5-й запрос, кэшируется и отдаётся через
GET /mods/:slug/icon. - Все
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)
- Сохраняет версию в
mod_versions(см.database-schema.md). download_url= прямая ссылкаhttps://github.com/.../releases/download/...(CDNobjects.githubusercontent.com).- Инвалидирует / обновляет 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. Коротко:
-- Таблица модов
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 /modsp95 < 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 в проде.