feat(admin): шкала загрузки источников, отладка и загрузка работ в корпус
All checks were successful
Deploy / test (push) Successful in 3m56s
Deploy / deploy (push) Successful in 31s

Заливка корпуса была чёрным ящиком: у источника только last_status
(idle/running/done/error), без «сколько из скольки», без причины падения и без
способа остановить начатое. Теперь каждый запуск создаёт строку parse_runs,
куда воркер раз в ~2с пишет стадию, счётчики и журнал событий.

Админка:
- шкала загрузки у каждого источника (0→50% выборка, 50→100% индексация),
  раскрытая строка — журнал прогона по шагам с таймингами;
- «Запустить всё» / «Остановить всё» и остановка по одному источнику
  (кооперативная отмена: воркер останавливается сам, не рвя запись в базу);
- пакетное добавление источников (тип + список тем), тип pmc в форме;
- загрузка PDF/DOCX/TXT прямо в базу сравнения (index.ingest_upload);
- страница «Отладка»: воркеры Celery и их текущие таски, очереди RabbitMQ,
  покрытие корпуса эмбеддингами, зависшие и упавшие прогоны, конфиг бэкендов.

Защита от краш-лупа по consumer_timeout RabbitMQ (docs/DR-HA.md §6), без неё
массовый запуск 170+ источников гарантированно ронял воркер:
- PARSER_TIME_BUDGET_S (1500с) — прогон закругляется сам и помечается partial;
- worker_prefetch_multiplier=1 — таймаут считается от ДОСТАВКИ сообщения, и с
  дефолтным префетчем очередь долгих run_parser убивала канал на задачах,
  которые ещё не начинались.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-08-27 17:40:23 +05:00
parent 95d2903766
commit 78e27806b9
29 changed files with 2118 additions and 168 deletions

View File

@@ -77,7 +77,11 @@
`faiss_id`.
- **fingerprints** — `doc_id`, `hash_value` (BIGINT, Winnowing), `position` — для L1.
- **usage_logs** — `user_id`, `action` — учёт лимитов по тарифу.
- **parse_sources** — задания парсеров (админка): тип, query, годы, лимит, статус.
- **parse_sources** — задания парсеров (админка): тип, query, годы, лимит, статус,
`last_run_id` — ссылка на последний прогон.
- **parse_runs** — прогоны заливки: стадия, счётчики (`target/fetched/processed/
added/duplicates/skipped/failed`), `cancel_requested`, `heartbeat_at`, журнал
событий (JSON). Из них админка рисует шкалу загрузки, см. §12.
- **staged_works** — пользовательские загрузки на модерацию перед добавлением в корпус.
- **admin_sessions** — одноразовые коды входа в админку.
@@ -87,7 +91,7 @@
| Очередь | Задачи | Воркер |
|---------|--------|--------|
| `queue.index` | `index.extract_and_check`, `index.add_document`, `index.run_parser`, `index.enrich_full_text` | worker-indexer |
| `queue.index` | `index.extract_and_check`, `index.add_document`, `index.run_parser`, `index.enrich_full_text`, `index.ingest_upload` | worker-indexer |
| `queue.gpu` | `gpu.check_plagiarism`, `gpu.embed_documents`, `gpu.search_semantic` | worker-gpu |
| `queue.gost` | `gost.format_bibliography` | worker-gost |
| `queue.notify` | `notify.send_task_done`, `notify.send_verification` | worker-notifier |
@@ -115,7 +119,11 @@
**Наполнение корпуса.** админка/CLI → `index.run_parser` (OpenAlex/arXiv/PMC/
КиберЛенинка, фильтр `is_oa`) → `index.add_document` (дедуп по `ext_id`,
fingerprints, MinHash, эмбеддинг) → `index.enrich_full_text` (скачать OA-PDF →
MinIO → переиндексация).
MinIO → переиндексация). Ход заливки пишется в `parse_runs` (§12).
Второй путь наполнения — ручная загрузка файлов админом: api сохраняет их в
MinIO (`corpus-upload/`) → `index.ingest_upload` (извлечь текст → `add_document`,
`source=manual_upload`). Это не проверка на плагиат: файл сразу становится
источником для сравнения, минуя отстойник.
## 7. Детекция плагиата — 4 уровня
@@ -203,6 +211,21 @@ Identity (Universal Auth) и генерирует `.env` заново (`infisica
статуса). Опционально — Prometheus+Grafana (профиль `observability`, метрики API + Flower).
- **Бэкапы**: `pg_dump→gzip→MinIO`, cron 03:00, ротация 14. Проверка восстановления —
`scripts/ops/pg_restore_verify.sh`. HA/DR — [DR-HA.md](DR-HA.md).
- **Шкала загрузки источников** (админка → «Источники»): каждый запуск создаёт строку
`parse_runs`, воркер пишет туда прогресс раз в ~2с. Шкала считается по формуле
`api/app/core/progress.py`: 0→50% — выборка из источника, 50→100% — индексация в
базу. Раскрытая строка показывает журнал прогона (по шагам, с таймингами).
Кнопки «Запустить всё» / «Остановить всё» — массовый старт и кооперативная отмена
(флаг `cancel_requested`, воркер останавливается сам на ближайшем тике; уже
начатый прогон не рвём посреди записи в базу).
- **Панель отладки** (админка → «Отладка», `GET /api/admin/debug`): один срез —
живые воркеры Celery и что именно они крутят, глубина очередей RabbitMQ
(в т.ч. `unacked` и число потребителей), покрытие корпуса эмбеддингами,
активные и проблемные прогоны, зависшие прогоны (нет heartbeat >10 мин),
упавшие проверки за сутки и текущие бэкенды (`EMBED_BACKEND`/`LLM_BACKEND`/
`VECTOR_BACKEND`).
- **Бюджет времени прогона** (`PARSER_TIME_BUDGET_S`, по умолчанию 1500с) —
защита от краш-лупа по `consumer_timeout` RabbitMQ, см. [DR-HA.md](DR-HA.md) §6.
## 13. Безопасность

View File

@@ -66,8 +66,22 @@ Redis у нас — кэш/rate-limits/LSH-индекс (префикс `antipla
(`restart: unless-stopped`) поднимает его заново, недоставленное сообщение
передоставляется — и цикл повторяется бесконечно, монополизируя весь пул воркера
(было и на `worker-indexer` из-за backoff `scripts/parsers/openalex.py`, и на
`worker-gpu` из-за зависшей Ollama на embedding-gpu). Текущая митигация — держать
retry-бэкоффы заведомо короче 1800с (пример: `MAX_RATE_LIMIT_RETRIES` в
`openalex.py`). Более глубокий фикс — поднять `consumer_timeout` на самом RabbitMQ
(доступа для этого пока не заводили) — актуально при добавлении любых новых
долгих retry-циклов в Celery-тасках.
`worker-gpu` из-за зависшей Ollama на embedding-gpu).
Митигации (2026-08-27, сделано для `worker-indexer`):
1. **Бюджет времени прогона** — `PARSER_TIME_BUDGET_S` (1500с) в
`worker-indexer/app/config.py`: `index.run_parser` сам останавливается раньше
дедлайна и помечает прогон `partial` вместо того, чтобы довести воркер до
падения. Остаток дозаливается повторным запуском источника.
2. **`worker_prefetch_multiplier=1`** (`worker-indexer/app/celery_app.py`) —
ключевое при массовой заливке. Таймаут отсчитывается от **доставки**
сообщения, а не от начала выполнения: с дефолтным префетчем (4×concurrency)
сотни поставленных в очередь долгих `run_parser` висят unacked и убивают
канал на задачах, которые ещё даже не начинались.
3. Retry-бэкоффы держим заведомо короче 1800с (`MAX_RATE_LIMIT_RETRIES` в
`openalex.py`).
`worker-gpu` этих защит пока не имеет (там нет своих долгих retry-циклов, но
есть зависание внешней Ollama — см. выше). Более глубокий общий фикс — поднять
`consumer_timeout` на самом RabbitMQ (доступа для этого пока не заводили).

View File

@@ -14,11 +14,9 @@
(русскоязычные, CyberLeninka/OpenAlex `lang=ru`) и **`scripts/seed_broad_corpus.py`**
(англоязычные — OpenAlex/arXiv/PMC по широкому списку дисциплин, добавлен позже
первой волны).
- Известный операционный риск при массовой докачке: retry-бэкофф на 429 от OpenAlex
не должен по сумме ожиданий превышать `consumer_timeout` RabbitMQ (по умолчанию
1800с) — иначе брокер рвёт канал до того, как таск успевает сдаться, и
`worker-indexer` уходит в бесконечный краш-луп на редоставленном сообщении
(см. `scripts/parsers/openalex.py`, `MAX_RATE_LIMIT_RETRIES`).
- Операционный риск массовой докачки (`consumer_timeout` RabbitMQ vs долгие таски)
закрыт бюджетом времени прогона и `worker_prefetch_multiplier=1` — подробности
и почему префетч тут главный, см. [DR-HA.md](DR-HA.md) §6.
## Что подготовлено
@@ -42,10 +40,31 @@ python scripts/seed_ru_sources.py
python scripts/seed_ru_sources.py --apply --limit 500
# 2. Проверить пару источников на темпе/качестве, затем запустить заливку:
# • Админ-панель → «Источники» → «Запустить», ЛИБО
# • Админ-панель → «Источники» → «Запустить» (или «Запустить всё»), ЛИБО
# • Celery: index.run_parser.delay(source_id) по каждому id
```
## Управление заливкой из админки
Всё, что ниже, доступно на странице «Источники» — CLI для этого больше не нужен:
- **Шкала загрузки** у каждого источника: стадия (выборка → индексация), сколько
получено из скольки, сколько добавлено/дублей/ошибок. Раскрытая строка —
журнал прогона по шагам с таймингами (таблица `parse_runs`).
- **«Запустить всё»** — прогон по всем включённым источникам; уже идущие
пропускаются. **«Остановить всё»** и остановка по одному — кооперативная
отмена: воркер останавливается сам на ближайшем тике, не обрывая запись в базу.
- **Пакетное добавление** — один тип источника + список тем (по строке),
опционально с немедленным запуском. Заменяет `seed_*.py` для разовых расширений.
- **Загрузка работ в базу** — PDF/DOCX/TXT прямо в корпус сравнения
(`index.ingest_upload`, `source=manual_upload`), минуя проверку и отстойник.
- **Страница «Отладка»** — очереди, воркеры, покрытие эмбеддингами, зависшие и
упавшие прогоны (см. ARCHITECTURE.md §12).
Прогон со статусом `partial` — это не ошибка: сработал бюджет времени
(`PARSER_TIME_BUDGET_S`, 1500с), заливка остановилась раньше `consumer_timeout`
RabbitMQ. Остаток добирается повторным запуском источника.
Заливка сама: fetch (rate-limit 1 req/s) → `add_document` (дедуп по `ext_id`,
fingerprints L1, MinHash L2) → батч-эмбеддинги `gpu.embed_documents` (L3). ~30 тем ×
500 ≈ 15K русских документов на первый заход.