55 lines
7.3 KiB
Markdown
55 lines
7.3 KiB
Markdown
# План: разделение сборщика и 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 не реализуется, пока не будет отдельной команды.
|