Files
ripe-cidr-collector/docs/plan-sqlite-storage.md
T
ayurishchevandClaude Sonnet 5 bcf8156085 Initial commit: RIPE CIDR/FQDN collector
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>
2026-09-21 07:29:38 +03:00

73 lines
11 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.
# План: хранение собранных адресов в 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`)
```sql
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` (kind `asn`) и `fqdn_data.json` (kind `fqdn`): значения с блоком `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` (без изменений: принимает множество значений).
## Проверка
1. `docker build -f Dockerfile.test -t ripe-collector-test . && docker run --rm ripe-collector-test` - 14 тестов.
2. **Эталонное сравнение**: на копии реальных данных (`scratchpad/backup`) запустить API после миграции; ответы `/addresses?type=all|cidr|fqdn` побайтно совпадают с эталонами `before_*.txt`, снятыми старой версией; JSON-файлы переименованы, повторный запуск не импортирует заново.
3. Сбор на копии: демон/CLI `run` создаёт и обновляет строки; повторный запуск не плодит дубликатов; уменьшенный `ttl_days` удаляет просроченное; `purge=true` удаляет адреса источника.
4. Конкурентность: во время идущего сбора цикл запросов `curl /addresses` не даёт ошибок (WAL: чтение не блокируется записью); одновременный старт API и демона на пустой базе выполняет миграцию один раз.
5. Порча: записать мусор в `ripe.db` - API отвечает 503, файл переименован в `*.corrupt-*`, демон фиксирует ошибку.
6. Docker Compose на копии данных (как в прошлый раз, отдельный проект): оба контейнера работают с одной базой в томе (`healthy`, API видит адреса, собранные демоном, после `down`/`up` данные на месте); по окончании убрать тестовые контейнеры, тома и образы.