# План: разделение сборщика и API на два процесса ## Context Сейчас планировщик (APScheduler) живёт внутри процесса API: перезапуск или падение API останавливает сбор, а тяжёлый сбор делит процесс с обработкой запросов. Плюс ASN и FQDN можно менять только через CLI и правкой `config.json`. Запрос состоит из двух частей, порядок выполнения важен, так как часть B опирается на часть A: - **Часть A - разделение на два процесса: планируется и выполняется** после утверждения плана. - **Часть B - управление ASN и FQDN через API: только план.** Реализация начнётся отдельной командой после части A. Артефакты по правилам проекта: `docs/plan-process-split.md` и `docs/plan-asn-fqdn-api.md` (копии соответствующих частей плана, первым шагом), затем `docs/summary-process-split.md` (после части A) и обновление `README.md`. ## Часть A. Разделение на два процесса (выполняется) ### Архитектура Два независимых сервиса без IPC, общение через файлы в каталоге проекта (уже атомарные и под `flock`): - **API** (`uvicorn api_server:app`): только читает данные и пишет `config.json`; планировщика в нём больше нет. - **Сборщик-демон** (`python collector_daemon.py`, новый): держит расписание, запускает сбор, пишет `status.json`. Изменение расписания через `POST /schedule` доходит до демона через `config.json`: демон раз в 30 секунд сверяет секцию `schedule` и перепланирует задания без перезапуска (задержка применения до 30 с, документируется). ### Изменения 1. **`collector_daemon.py` (новый)** - `BlockingScheduler`, задания `asn_job` и `fqdn_job` из `schedule` (значения по умолчанию как сейчас: `0 2 * * *` / `0 3 * * *`), `max_instances=1`, `coalesce=True`. - Задание `sync_schedule` каждые 30 с: читает `load_full_config()`, при изменении cron делает `reschedule_job`; невалидный cron логируется и игнорируется (остаётся прежнее расписание). - `run_job(name, collector_cls)` переезжает из `api_server.py` (`CIDRCollector`/`FQDNCollector` создаются заново на каждый запуск - свежий конфиг); статусы `last_run`, `last_error`, `next_run` пишутся в `status.json` атомарно вместе с `updated_at` (heartbeat каждые 30 с). - Единственный экземпляр: неблокирующий `flock` на `collector.daemon.lock`; второй экземпляр завершается с понятной ошибкой. - Корректное завершение по `SIGTERM`/`SIGINT` (`scheduler.shutdown`). 2. **`storage.py`**: добавить `try_lock(path)` (неблокирующая эксклюзивная блокировка для singleton). 3. **`cidr_collector.py`**: константа `STATUS_FILE` рядом с остальными путями (единая точка для API и тестов). 4. **`api_server.py`** - Убрать `BackgroundScheduler`, `lifespan`, `job_state`, `run_*_job`, `JOBS`, `start_scheduler`. - `POST /schedule`: валидация cron через `CronTrigger.from_crontab` (как сейчас), запись в `config.json` под блокировкой; ответ уточняет, что применение демоном до 30 с. - `GET /health`: читает `status.json`; `collector_alive = now - updated_at < 120 с`; `status = ok` только если демон жив и нет `last_error`, иначе `degraded` (нет файла = демон не запускался). HTTP-код остаётся 200. 5. **Развёртывание (`README.md`, `.gitignore`, `.dockerignore`)** - Второй сервис `ripe-collector` для systemd и OpenRC (та же учётная запись `ripe`, `ExecStart=.../python collector_daemon.py`, `Restart=always`); юниты API и сборщика независимы. - Раздел про cron остаётся как альтернатива ручного запуска (`cidr_collector.py run`), но основной путь - демон. - **Примечание об обновлении:** после разделения одного `ripe-api` недостаточно, без `ripe-collector` сбор не идёт; `/health` покажет `degraded`. - Переписать раздел «Scheduler Logic» под новую схему; добавить `status.json` в `.gitignore` и `.dockerignore`. ### Тесты (минимум, в контейнере) 1. `sync_schedule`: смена cron в `config.json` перепланирует задание; невалидный cron игнорируется (планировщик без запуска, проверка триггера задания). 2. `/health`: свежий `status.json` -> `collector_alive: true`, устаревший или отсутствующий -> `false` и `degraded`. ### Проверка 1. `docker build -f Dockerfile.test -t ripe-collector-test . && docker run --rm ripe-collector-test` - все тесты (прежние 9 + новые) проходят. 2. Вручную на копии данных в scratchpad (реальные данные не трогаем): запустить демон с расписанием `*/1 * * * *` и API в двух процессах; убедиться, что сбор идёт без API; `POST /schedule` с токеном меняет расписание демона не позднее чем через 30 с (по логу и `/health`); остановка демона -> `/health` через 2 минуты `degraded`, `collector_alive: false`; второй запуск демона завершается с ошибкой singleton; `kill -TERM` останавливает демон чисто. 3. Перезапуск API во время работы демона не прерывает сбор. ## Критичные файлы `collector_daemon.py` (новый), `api_server.py`, `cidr_collector.py`, `storage.py`, `README.md`, `.gitignore`, `.dockerignore`, `tests/`. Переиспользуем: `load_full_config`, `save_json_atomic`, `file_lock`, `load_json`, `verify_token`, `merge_entry`. ## Порядок выполнения после утверждения 1. Скопировать планы в `docs/`. 2. Выполнить часть A (код, тесты, проверка, README, `docs/summary-process-split.md`). 3. Часть B не реализуется, пока не будет отдельной команды.