Files

4.5 KiB
Raw Permalink Blame History

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, классификация НЕ запущена