diff --git a/awg2-harness.service b/awg2-harness.service index 200b80e..b5847f4 100644 --- a/awg2-harness.service +++ b/awg2-harness.service @@ -3,16 +3,29 @@ Description=AWG2 Harness multi-orchestrator After=network-online.target docker.service Wants=network-online.target Requires=docker.service -StartLimitIntervalSec=60 -StartLimitBurst=5 +# Restart backoff below can span ~5 minutes, so the failure window has to be wider than +# the old 60s or the limit would trip during a single legitimate backoff sequence. +StartLimitIntervalSec=15min +StartLimitBurst=10 [Service] -Type=simple +# Type=exec, not simple: startup is only considered successful once the binary has been +# executed, so a missing or non-executable /usr/sbin/awg2-harness fails the unit instead +# of being reported as started. +Type=exec User=root -ExecStartPre=/usr/sbin/awg2-harness --config /etc/awg2-harness/config.toml ensure-swap +# ensure-swap is not run here. The daemon performs it as the first step of its own startup +# sequence, where it shares one error contract, one lock and one log stream. Running it +# twice per start only duplicated the work and split the failure path across two units. ExecStart=/usr/sbin/awg2-harness --config /etc/awg2-harness/config.toml daemon -Restart=always -RestartSec=10 +# on-failure, not always: a clean exit is deliberate and must not be restarted. +Restart=on-failure +# Stepped backoff 10 -> 20 -> 40 -> 80 -> 160s. A tight restart loop against a dependency +# that is still coming up (Docker, the container, the network) only burns the start limit +# before the dependency has a chance to recover. +RestartSec=10s +RestartSteps=4 +RestartMaxDelaySec=160s StandardOutput=journal StandardError=journal diff --git a/awg2-reboot-scheduler.service b/awg2-reboot-scheduler.service index b96a6ad..d3a23a3 100644 --- a/awg2-reboot-scheduler.service +++ b/awg2-reboot-scheduler.service @@ -2,16 +2,22 @@ Description=AWG2 Harness reboot scheduler After=network-online.target Wants=network-online.target +StartLimitIntervalSec=15min +StartLimitBurst=10 [Service] -Type=simple +Type=exec User=root ExecStart=/usr/sbin/awg2-reboot-scheduler --config /etc/awg2-harness/config.toml +# The scheduler exits 0 in two normal cases: reboot.enabled = false, and after systemd has +# accepted the reboot request. Neither is a failure, so on-failure is required here - +# Restart=always would respawn it in a loop on a host with reboots disabled. Restart=on-failure -RestartSec=60 +RestartSec=10s +RestartSteps=4 +RestartMaxDelaySec=160s StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target - diff --git a/docs/ACCEPTANCE_CHECKLIST.md b/docs/ACCEPTANCE_CHECKLIST.md index f759ad8..d410691 100644 --- a/docs/ACCEPTANCE_CHECKLIST.md +++ b/docs/ACCEPTANCE_CHECKLIST.md @@ -64,6 +64,10 @@ - [ ] `docker restart amnezia-awg2` → лимиты восстанавливаются автоматически. - [ ] `systemctl restart docker` → после возвращения контейнера лимиты восстанавливаются автоматически. - [ ] `systemctl restart awg2-harness` → сервис сам находит контейнер и применяет/проверяет лимиты. +- [ ] `systemd-analyze verify` проходит для обоих unit-файлов на целевом Debian 13. +- [ ] Правка `config.toml` без restart не меняет поведение; после `systemctl restart awg2-harness.service` изменения применяются. +- [ ] `ensure-swap` за один старт сервиса выполняется ровно один раз (в journal нет двух подряд swap-reconcile). +- [ ] При устойчивом падении демона интервал перезапусков растёт (10 → 20 → 40 → 80 → 160 с), а не остаётся 10 с. - [ ] Reboot хоста → сервис стартует и восстанавливает лимиты. - [ ] Изменение имени интерфейса на допустимое fallback-имя не ломает detector. - [ ] Если контейнер временно отсутствует, daemon логирует ошибку reconcile и продолжает работу. @@ -90,7 +94,7 @@ ## G. Планировщик перезагрузки -- [ ] Если `reboot.enabled = false`, сервис не планирует reboot. +- [ ] Если `reboot.enabled = false`, сервис не планирует reboot и **не** уходит в цикл перезапусков (`systemctl status` показывает завершение с кодом 0, а не рестарты). - [ ] Если `reboot.enabled = true`, в journal видна запланированная перезагрузка. - [ ] Время попадает в окно `window_start_utc`–`window_end_utc`. - [ ] Вызов reboot выполняется через `systemctl --no-block reboot`. diff --git a/docs/IMPLEMENTATION_SPEC.md b/docs/IMPLEMENTATION_SPEC.md index fabdbea..f324449 100644 --- a/docs/IMPLEMENTATION_SPEC.md +++ b/docs/IMPLEMENTATION_SPEC.md @@ -41,6 +41,36 @@ Production-пакет AWG2 Harness содержит два Rust-бинаря: Право планировать перезагрузку принадлежит сервису `awg2-reboot-scheduler`. `awg2-harness` не планирует перезагрузки внутри себя. +### Конфигурация читается один раз + +Оба процесса загружают `/etc/awg2-harness/config.toml` при старте и дальше работают с этой копией. Правка файла **не применяется автоматически**. Реализовывать hot reload сейчас намеренно не планируется: это потребовало бы повторной валидации, атомарной подмены текущего конфига, перепланирования reboot, смены контейнера и интерфейса на лету и отката при неверной конфигурации. + +Порядок применения изменений: + +```bash +awg2-harness --config /etc/awg2-harness/config.toml validate-config +awg2-harness --config /etc/awg2-harness/config.toml validate-runtime +systemctl restart awg2-harness.service +``` + +При изменении секции `[reboot]` дополнительно: + +```bash +systemctl restart awg2-reboot-scheduler.service +``` + +`systemctl daemon-reload` для правки TOML не нужен — он относится к unit-файлам, а не к конфигу приложения. `ExecReload` в units намеренно отсутствует: его добавление без реальной обработки SIGHUP создало бы ложное впечатление, что перезагрузка конфига поддерживается. + +### Политика systemd + +Обе службы используют `Type=exec`, а не `Type=simple`: старт считается успешным только после фактического запуска бинаря, поэтому отсутствующий или неисполняемый файл роняет unit, а не рапортует об успешном старте. + +`Restart=on-failure` для обеих. Для планировщика это обязательно: он штатно завершается с кодом 0 в двух случаях — при `reboot.enabled = false` и после того, как systemd принял запрос на перезагрузку. С `Restart=always` он перезапускался бы в цикле на хосте с отключёнными перезагрузками. + +Задержка перезапуска ступенчатая: `RestartSec=10s`, `RestartSteps=4`, `RestartMaxDelaySec=160s`, то есть 10 → 20 → 40 → 80 → 160 секунд. Плотный цикл перезапусков против ещё поднимающейся зависимости (Docker, контейнер, сеть) только сжигает лимит стартов до того, как зависимость успеет восстановиться. Окно лимита расширено до `StartLimitIntervalSec=15min` при `StartLimitBurst=10`, иначе полная последовательность backoff не укладывалась бы в него. + +`ExecStartPre` с `ensure-swap` удалён. Демон выполняет swap первым шагом собственной startup-последовательности, где действуют единый error contract, единая блокировка и единый поток логов. Дублирование давало две блокировки, два чтения `/proc/swaps` и раздвоенный путь диагностики отказа. Отдельная CLI-команда `ensure-swap` при этом остаётся доступной оператору. + Подкоманды: ```bash