Files
vesti/openspec/specs/crawler-queue/spec.md
T

67 lines
4.5 KiB
Markdown
Raw 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.
# crawler-queue Specification
## Purpose
Двухфазный пайплайн сбора и обработки новостей: краулеры (фаза 1) собирают
кандидатов в очередь (`posts` со `status='new'` и неклассифицированные), воркеры
(фаза 3) разбирают её параллельно (ThreadPoolExecutor). Отдельная страница
`/crawlers` показывает статус источников и размер очереди.
## Requirements
### Requirement: Параллельная фаза 3 (воркеры)
Система MUST предоставлять `crawler/worker.py` с пулом `ThreadPoolExecutor(max_workers=N)`,
где каждый воркер атомарно забирает пост из очереди (`UPDATE posts SET classified=-1
WHERE id=? AND classified=0`), классифицирует его (`classify_text`: keywords → Ollama,
trafilatura для summary-only) и пишет результат (direction/relevance/interest/summary,
classified=1). При исключении посте MUST возвращаться в очередь (classified=0).
#### Scenario: Параллельная обработка очереди
- **GIVEN** 100 постов со `classified=0`
- **WHEN** `python -m crawler.worker --workers 8 --limit 200`
- **THEN** все 100 постов обработаны (classified=1), дублей нет (каждый обработан ровно 1 раз)
#### Scenario: Сбой воркера
- **GIVEN** пост, у которого `classify_text` бросает исключение (Ollama недоступна)
- **WHEN** воркер обрабатывает пост
- **THEN** пост возвращается в очередь (classified=0), воркер продолжает работу, запуск не падает
### Requirement: Очередь на основе posts (без новой таблицы)
«Размер очереди» MUST вычисляться из существующей таблицы `posts`:
`SELECT COUNT(*) FROM posts WHERE classified IS NULL OR classified=0` (pending),
`classified=-1` (processing), `classified=1 AND fetched_at >= date('now')` (done today).
Новая таблица для очереди НЕ создаётся — она дублировала бы `posts`.
#### Scenario: Размер очереди
- **GIVEN** в posts 10 новых (classified=0) и 2 в обработке (classified=-1)
- **WHEN** страница /crawlers запрашивает размер очереди
- **THEN** pending=10, processing=2, done today=0
### Requirement: Страница /crawlers
Веб MUST предоставлять `GET /crawlers` (за аутентификацией): таблица источников
(slug, name, crawler, status, last_fetch, last_error за 200 симв., error_count,
priority; для rss — etag/modified/last_build_date из rss_state) и карточки очереди
(pending/processing/done). Кнопка «Сбросить dead» (POST /crawlers/reset) MUST
устанавливать sources.status='alive', error_count=0, last_error=NULL для всех
источников со status='dead'.
#### Scenario: Просмотр статуса
- **GIVEN** источник lwn (rss, alive) и 15 новых постов в очереди
- **WHEN** GET /crawlers
- **THEN** страница показывает lwn с статусом alive, размер очереди 15
#### Scenario: Сброс dead-источников
- **GIVEN** источник со status='dead', error_count=7
- **WHEN** POST /crawlers/reset
- **THEN** источник становится alive, error_count=0, last_error=NULL
### Requirement: Фаза 1 (сбор) отдельно от фазы 3 (обработка)
Система MUST предоставлять `crawler/crawl_sources.py` — запуск сбора всех
включённых источников (telegram_crawler + rss_crawler) без классификации;
новые посты попадают в очередь (posts, classified=0). Обработка (фаза 3)
запускается отдельно (`crawler/worker.py`).
#### Scenario: Сбор без классификации
- **GIVEN** включённые источники telegram и rss
- **WHEN** `python -m crawler.crawl_sources --all`
- **THEN** новые посты добавлены в posts с classified=0, классификация НЕ запущена