fix(systemd): drop the duplicate ensure-swap, use Type=exec and stepped restart backoff [P1]
awg2-harness.service ran ensure-swap from ExecStartPre while the daemon runs it again as the first step of its own startup. Idempotency kept it harmless, but it cost two locks, two reads of /proc/swaps, two persistence checks, and split the startup failure path across two units so an operator reading systemctl status could not tell which invocation failed. The daemon owns the startup sequence, its error contract and its log stream, so the ExecStartPre is removed. The standalone ensure-swap subcommand is unaffected. Type=simple reports a unit as started the moment systemd forks, before the binary is known to be executable. Type=exec waits for the exec to succeed, so a missing or non-executable /usr/sbin/awg2-harness fails the unit instead of appearing to start. Restart policy: the daemon moves from Restart=always to on-failure, matching the scheduler. A clean exit is deliberate and should not be respawned. For the scheduler this was already load-bearing and is now documented - it exits 0 both when reboot.enabled is false and after systemd accepts the reboot request, so Restart=always would have spun it in a loop on any host with scheduled reboots disabled. RestartSec=10s with StartLimitBurst=5 over StartLimitIntervalSec=60 could exhaust the limit in about 50 seconds, which is easy to hit when the daemon is failing because Docker or the container is still coming up - the restarts burn the budget before the dependency can recover. Replaced with a stepped backoff, 10 -> 20 -> 40 -> 80 -> 160s via RestartSteps=4 and RestartMaxDelaySec=160s, and the limit window widened to 15 minutes with a burst of 10 so a full backoff sequence fits inside it. Both directives require systemd 254+; Debian 13 ships 257. Documented alongside, because it is the same lifecycle contract and had no home in the docs: configuration is read once at process start and a config edit needs a restart of the affected service. daemon-reload is not involved - it applies to unit files, not to the application config. ExecReload is deliberately absent, since adding it without real SIGHUP handling would imply a reload path that does not exist. Unit files are installed from the repo root by tools/build.sh, which already runs systemd-analyze verify on the build host when it is available, so these changes are verified there. Not verifiable in this environment: no Debian, no systemd. Acceptance checklist gained five checks covering unit verification, the restart-required contract, single ensure-swap per start, growing backoff, and the scheduler not looping when reboots are disabled.
This commit is contained in:
+19
-6
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user