Collector daemon, FastAPI server (addresses, diff, collect, sources), SQLite storage with change journal, Docker Compose deployment, tests, documentation and project rules. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11 KiB
План: хранение собранных адресов в SQLite
Context
Собранные адреса хранятся в data.json и fqdn_data.json: каждая запись целиком читается и переписывается, доступ между процессами (демон, API, CLI) защищён файловыми блокировками, а повреждение файла ведёт к потере накопленной истории. SQLite даёт транзакции, конкурентное чтение во время записи, запросы по адресам и основу для будущих diff и истории (first_seen/last_seen уже есть). API, форматы вывода и семантика TTL не меняются.
Границы: в SQLite переезжают только собранные адреса. config.json (источники, расписание, ttl_days) и status.json (heartbeat) остаются JSON: их редактируют вручную, а демон опрашивает конфиг. Резервное копирование, diff и уведомления в этот шаг не входят.
Артефакты по правилам проекта (создаются при реализации)
docs/plan-sqlite-storage.md- копия этого плана (первым шагом)docs/summary-sqlite-storage.md- итоги (в конце)- обновить
README.md(схема хранения, миграция, откат)
Схема (ripe.db в DATA_DIR)
CREATE TABLE addresses (
kind TEXT NOT NULL CHECK (kind IN ('asn', 'fqdn')),
source TEXT NOT NULL, -- '62041' или 'example.com'
value TEXT NOT NULL, -- префикс или IP
first_seen TEXT NOT NULL, -- ISO-время
last_seen TEXT NOT NULL,
PRIMARY KEY (kind, source, value)
);
CREATE INDEX addresses_value ON addresses (value);
PRAGMA user_version = 1; -- версия схемы для будущих миграций
Режим journal_mode=WAL, busy_timeout=5000, synchronous=NORMAL, temp_store=MEMORY. Время хранится строками ISO (секундная точность для новых записей), сравнение last_seen < cutoff корректно лексикографически.
Изменения
1. Новый модуль db.py
connect(path=None): открывает базу (cc.DB_FILE), применяет PRAGMA, при необходимости создаёт схему и однократно импортирует старые JSON (см. ниже). Ошибки уровняsqlite3.DatabaseError, кромеOperationalError(например, «database is locked»), считаются порчей: файл переименовывается вripe.db.corrupt-<ts>(вместе с-wal/-shm), поднимаетсяStorageError(API -> 503, демон записываетlast_error) - то же поведение, что было для битого JSON.merge_source(conn, kind, source, values, now, ttl_days): в одной транзакции upsert найденных значений (ON CONFLICT DO UPDATE SET last_seen), затемDELETE ... WHERE kind=? AND source=? AND last_seen < cutoff(просроченные, которых сегодня не видели; приttl_days = 0не удаляется ничего). Возвращает добавленные и удалённые значения для лога. Заменяетmerge_entryи правило «обновлять last_seen раз в сутки» (запись в SQLite дешёвая, обновляем всегда).sweep_unconfigured(conn, kind, configured, now, ttl_days): удаляет просроченные значения источников, которых нет в конфигурации (однимDELETE ... source NOT IN (...)); пустые записи как сущность больше не нужны.purge_source(conn, kind, source),get_values(conn, kind=None)(SELECT DISTINCT value),count_values(conn, kind).
2. Миграция старых данных (внутри connect)
- Условие:
user_version = 0и таблицы нет. ПодBEGIN IMMEDIATE(API и демон могут стартовать одновременно; второй ждёт и видит уже выполненную миграцию). - Импорт
data.json(kindasn) иfqdn_data.json(kindfqdn): значения с блокомseenсохраняютfirst_seen/last_seen; записи старого формата безseen(реальные данные сейчас именно такие) получаютfirst_seen = last_updated,last_seen = сейчас(как в прежней миграции: TTL идёт с момента перехода). Сразу пишетсяuser_version = 1. - После успешной транзакции JSON-файлы переименовываются в
data.json.migrated-<ts>иfqdn_data.json.migrated-<ts>(не удаляются - это и есть резервная копия и путь отката). - Импорт идемпотентен: повторный запуск (
user_version = 1) ничего не читает.
3. cidr_collector.py (упрощается)
- Константа
DB_FILE = os.path.join(DATA_DIR, "ripe.db");DATA_FILEиFQDN_DATA_FILEостаются только как пути для импорта старых данных. CIDRCollector/FQDNCollector.run_collection: сетевая часть без изменений; затемwith db.connect() as conn(одна транзакция): читает конфиг, для найденных источников вызываетdb.merge_source, затемdb.sweep_unconfigured. Источник, удалённый во время сбора, по-прежнему пропускается (проверка по конфигу внутри транзакции).- Удаляются
load_data/save_data,merge_entry,sweep_unconfigured(JSON-версия),purge_entry,LAST_SEEN_REFRESH, блокировкиfile_lock(DATA_FILE). Блокировка конфига (update_config) остаётся.
4. api_server.py
get_cidrs()/get_fqdn_ips()и счётчики/healthчитают черезdb.get_values/db.count_values(соединение на запрос).DELETE /asns|/fqdns ?purge=trueвызываетdb.purge_source.StorageErrorотdb.connectобрабатывается существующим обработчиком (503).
5. Развёртывание
Dockerfile: добавитьdb.pyв список копируемых файлов (иначе образ не соберёт рабочее приложение)..gitignore/.dockerignore:*.db,*.db-wal,*.db-shm,*.migrated-*.- Docker: оба сервиса используют одну базу в томе
/data(WAL работает между контейнерами на одном хосте с локальным томом; не рекомендуется на сетевых ФС - отметить в README).requirements.txtне меняется (sqlite3из стандартной библиотеки; нужна SQLite >= 3.24 для upsert, вpython:3.11-slimи на текущем хосте выполняется).
6. Документация (README.md)
Раздел о хранении: таблица addresses, что хранится в JSON, а что в БД, автоматическая миграция, откат (остановить сервисы, вернуть *.migrated-* в data.json/fqdn_data.json, запустить прежнюю версию; адреса, собранные после миграции, при откате будут потеряны), просмотр данных (sqlite3 ripe.db "SELECT ..."), примечание про сетевые ФС; обновить описание «Collector Logic» и переносимость данных в раздел Docker (миграция теперь копирует и ripe.db).
7. Тесты (минимум)
Существующие тесты, использовавшие data.json, переводятся на БД (подмена cc.DB_FILE на временный файл, заполнение через db.merge_source): TTL и безопасность при сбое (test_core), форматы API (test_formats), purge и старение удалённого источника (test_sources_api); test_daemon не затрагивается. Добавляется 1 тест миграции: JSON в старом формате (без seen) и в новом (с seen) импортируются с ожидаемыми first_seen/last_seen, файлы переименованы, повторное открытие ничего не меняет. Итого 14 тестов.
Критичные файлы
Новый db.py; правки cidr_collector.py, api_server.py, Dockerfile, README.md, .gitignore, .dockerignore, tests/test_core.py, tests/test_formats.py, tests/test_sources_api.py. Переиспользуем: StorageError (storage.py), update_config, load_full_config, formatters.build_output (без изменений: принимает множество значений).
Проверка
docker build -f Dockerfile.test -t ripe-collector-test . && docker run --rm ripe-collector-test- 14 тестов.- Эталонное сравнение: на копии реальных данных (
scratchpad/backup) запустить API после миграции; ответы/addresses?type=all|cidr|fqdnпобайтно совпадают с эталонамиbefore_*.txt, снятыми старой версией; JSON-файлы переименованы, повторный запуск не импортирует заново. - Сбор на копии: демон/CLI
runсоздаёт и обновляет строки; повторный запуск не плодит дубликатов; уменьшенныйttl_daysудаляет просроченное;purge=trueудаляет адреса источника. - Конкурентность: во время идущего сбора цикл запросов
curl /addressesне даёт ошибок (WAL: чтение не блокируется записью); одновременный старт API и демона на пустой базе выполняет миграцию один раз. - Порча: записать мусор в
ripe.db- API отвечает 503, файл переименован в*.corrupt-*, демон фиксирует ошибку. - Docker Compose на копии данных (как в прошлый раз, отдельный проект): оба контейнера работают с одной базой в томе (
healthy, API видит адреса, собранные демоном, послеdown/upданные на месте); по окончании убрать тестовые контейнеры, тома и образы.