Перейти к содержанию

Наблюдение и границы источников

Оглавление серии «Агрегатор лент» · Урок 10 из 10.

В предыдущем уроке «Сигнал» экспортировал отобранные версии в отдельную папку с manifest. Завершим проект разбором ограничений и диагностики. Нам нужно понимать не только число записей, но и свежесть каждого источника, причину повторов, границу сетевого доступа и рост локального состояния. Наличие worker и базы не означает, что источники успешно обновляются или что экспорт отражает самое последнее редакторское решение.

Финальное состояние остаётся приватной лабораторией: Python работает локально, HTTP-сервер слушает loopback, сбор сети выключен без явного флага. Приложения, XML-примеры, SQLite и сеть при подготовке не выполнялись. Поэтому ниже рассматриваются ожидаемые признаки и действия по чтению исходников, без выдуманного журнала успешного запуска или фактических показателей нагрузки.

Свежесть источника

Добавим read-only команду status.py. Она показывает last_ok, next_due, failures, безопасное имя класса last_error и возраст последнего успешного прохода. Эти поля относятся к работе агрегатора. Дата published относится к записи издателя и не заменяет их. Старая статья может быть корректно прочитана сегодня, а новая дата в базе не доказывает, что источник всё ещё доступен сейчас.

Полный файл status.py десятого состояния:

import json, sqlite3, time
from config import DB
if __name__ == "__main__":
    con = sqlite3.connect(DB.as_uri() + "?mode=ro", uri=True)
    con.row_factory = sqlite3.Row
    try:
        now = int(time.time())
        sources = []
        for row in con.execute("SELECT id,last_ok,next_due,failures,last_error FROM sources ORDER BY id"):
            value = dict(row)
            value["age_seconds"] = None if row["last_ok"] is None else max(0, now - row["last_ok"])
            sources.append(value)
        counts = dict(con.execute("SELECT COUNT(*) AS entries,SUM(reviewed_hash=fingerprint) AS approved FROM entries").fetchone())
        print(json.dumps({"sources":sources,"counts":counts}, ensure_ascii=False, indent=2))
    finally:
        con.close()

age_seconds вычисляется от текущего времени до last_ok. Если успешного прохода ещё не было, значение остаётся null, а не нулём. Ноль означал бы только что состоявшийся успех и дал бы ложную картину здоровья. Также отрицательный возраст ограничен нулём при смещении часов: это упрощение представления, но не исправление несинхронизированных системных часов. Для обычного локального расписания время машины должно оставаться согласованным.

Условный ответ 304 обновляет last_ok, если есть сохранённое представление и validators. Поэтому свежесть обработки не зависит от появления новых элементов. Если источник месяц не меняется, но регулярно отвечает 304, агрегатор может работать корректно. Если источник отказал, прежние entries остаются в базе, однако растущие failures и старый last_ok показывают проблему доставки. Наличие старых карточек нельзя читать как признак успешного текущего сбора.

Количество и ограничения данных

Команда также выводит число entries и сумму текущих одобрений. SQL SUM(reviewed_hash=fingerprint) может вернуть null, если нет значений для суммирования, например все решения пусты. Это особенность представления, а не неизвестное успешное одобрение. Для интерфейса, которому обязательно нужен числовой ноль, можно заменить выражение фрагментом COALESCE(SUM(reviewed_hash=fingerprint), 0). Полный файл выше сохраняет исходное ожидаемое поведение.

В финальном store.py вводится учебный предел десяти тысяч entries. Проверка происходит в транзакции после обработки очередного batch. Если добавление превысило лимит, транзакция откатывается: новые карточки и validators не должны частично зафиксироваться как успешное представление. Старое согласованное состояние остаётся доступным, а worker назначает повтор по backoff. Бесконечное повторение не устранит превышение квоты само по себе.

Предел по количеству записей не равен пределу файла базы. Заголовки, идентификаторы, история и индексы имеют разный размер. В чтении ленты уже есть лимит одного мебибайта и ста элементов ответа, а нормализация ограничивает строки. Вместе эти рубежи удерживают лабораторию небольшой, но не заменяют наблюдение диска. Если файл базы растёт неожиданно, сравнивают число записей, число revisions и реальный размер хранения.

Для затронутых batch записей сохраняется не более двадцати fingerprints в revisions. Это ограничение истории версий карточки, а не ограничение числа всех обнаруженных материалов. Старые незатронутые записи из перенесённой базы могут потребовать отдельного прохода политики хранения. Схема не выдаёт историю revisions за полные копии издательских статей: в ней лежат нормализованные метаданные, которые агрегатор действительно получил.

Граница сетевого адреса

Сборщик принимает source ID из заданного словаря, а не URL из параметра HTTP. В конфигурации реальный адрес указан редактором проекта. Redirects выключены, proxy из окружения не используется, разрешено только ожидаемое представление ответа. Такая граница предотвращает превращение read-only страницы в endpoint, который посетитель заставляет обращаться к внутренним адресам. Политика исходящих запросов принадлежит сборщику, а не полю link каждой найденной записи.

safe_link() обрабатывает ссылку для отображения, но сборщик не загружает её содержимое. Проверка HTTPS, userinfo, порта и явных локальных адресов не является полной проверкой доверия издателю или DNS назначения. Поэтому слово «допустимая» относится к нашей синтаксической policy, а не к обещанию безопасности всей внешней страницы. Ссылка не становится доказательством права перепубликовать чужой текст и не запускает обход сайта в фоне.

Если позже понадобится пользовательское добавление источников, прежней фиксированной конфигурации уже недостаточно. Потребуется отдельная модель SSRF: допустимые направления, DNS и IP, redirect, egress и повторная проверка на каждом сетевом переходе. Текущий урок не изображает такой общий сервис. Сохраняйте исходную границу: локальный оператор выбирает ограниченный список, браузер только читает отобранные метаданные.

Для сетевого чтения заданы timeout, лимит байтов и контроль прошедшего времени между блоками. Это не точный wall-clock deadline всего обращения: отдельная блокирующая операция чтения ещё может ждать свой timeout. Документация urllib.request объясняет передаваемый timeout и HTTP-обработчики. Не нужно обещать завершение ровно на двадцатой секунде по проверке, которая выполняется лишь после возврата очередного read.

Разбор отказа по слоям

Если last_ok устарел, сначала проверьте режим сети и next_due. HTTP-источник без --network ожидаемо пропускается. Если срок ещё не наступил после успешного чтения или backoff, отсутствие нового запроса также ожидаемо. Затем смотрят failures и класс ошибки. Этот порядок предотвращает попытки исправить parser, когда worker вообще не получил разрешение на сеть или сознательно ждёт назначенного времени.

Если чтение произошло, но batch не сохранился, рассматривают лимит размера, Content-Type, корректность RSS/Atom, противоречивые ID и SQLite. Сохранённые validators должны соответствовать последнему успешно принятому представлению; ошибка не должна продвигать их к отклонённому ответу. Иначе следующий 304 оставил бы агрегатор без данных той версии, которую он якобы уже сохранил. Транзакционная граница пятого урока защищает именно эту согласованность.

Отдельно проверяйте текущий отбор и экспорт. manifest.json описывает выбор на момент последнего успешного экспорта, а не непрерывное представление SQLite. После изменения fingerprint или нового approve exporter нужно вызвать снова. Старые immutable файлы могут оставаться рядом, но не входят в новый manifest. Переносить все *.md из каталога по маске означало бы игнорировать эту границу и вернуть устаревшие версии в публикацию.

Повреждение или ручная правка существующего generated-файла должна быть видимой ошибкой, а не молчаливой перезаписью авторского текста. Подготовленный exporter сравнивает содержимое версии и прекращает работу при различии. Собственное редакторское описание лучше хранить отдельно от машинного immutable представления. Экспорт и публикация остаются разными действиями: никакой файл автоматически не переносится в контент сайта.

Финальный «Сигнал» умеет собрать ограниченные метаданные, сохранить версии, повторить отказ, связать выбор с fingerprint и показать или экспортировать актуальный отбор. Он не содержит публичной авторизации, распределённой очереди или автоматического создания авторских статей. Диагностика делает эти границы видимыми и помогает объяснить каждый пропущенный проход, вместо подмены ожидаемого поведения вымышленным успешным запуском.

Полное состояние шага — папка lesson-10 архива «Сигнал». Код, сеть, база данных и учебный сервер при подготовке не запускались; описанное поведение ожидается из разбора исходников.