docs: описать транзакционный guard и взаимное исключение операций

- docs/07: полный порядок staged apply, инвариант снятия guard, объяснение
  почему окно 45 секунд не обязано покрывать smoke и почему guard не трогает
  nftables.service, семантическая проверка эффективного firewall;
- docs/11: разделы A5e/A5f для новых unit-тестов и серверные сценарии D1e
  (guard доходит до дедлайна), D1f (конкурентные операции), D1g (успешная
  операция не оставляет следов транзакции); матрица и acceptance criteria
  дополнены;
- docs/12: разбор отказов "уже выполняется другая операция" и
  firewall_guard_fired;
- docs/13: строки журнала guard в таблице recovery, новый раздел 8a про замок
  операций;
- docs/14 и purge-v0.sh: очистка /run/hy2xs, замка операций и candidate-файлов
  firewall — /run это tmpfs, но очистка не имеет права требовать перезагрузки;
- README: защита от потери доступа при смене firewall и раздел "Одна операция
  за раз";
- CHANGELOG: шестой проход.
This commit is contained in:
2026-08-31 01:31:15 +05:00
parent 50ec4d9717
commit 39139e95f7
8 changed files with 532 additions and 11 deletions
+66
View File
@@ -81,6 +81,29 @@ sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env
| `rollback finished with N failed stage(s); manual recovery may be required` | итог: перечисленные стадии требуют ручной проверки |
| `rollback completed: N stage(s) succeeded` | восстановление отработало полностью |
| `manual recovery data preserved at /run/hy2xs/rollback/<op>` | firewall восстановлен не полностью; прежние `nftables.conf` и `hy2xs.nft` лежат по этому пути |
| `firewall rollback guard armed: … fires in 45s` | guard взведён; с этого момента операция обязана снять его до фиксации успеха |
| `firewall rollback guard disarmed and proven inactive` | guard снят, и это подтверждено состоянием юнитов и отсутствием маркера срабатывания |
| `automatic firewall rollback has already fired` | guard успел сработать; сервер работает на **прежнем** firewall, операция обязана завершиться отказом |
| `firewall rollback guard <unit> is still in state "…"` | остановить guard не удалось; фиксация успеха запрещена, разбирайтесь с systemd |
Отдельно про сработавший guard. Окно 45 секунд намеренно короче худшего случая
smoke и не обязано его покрывать: доказательством служит не время, а маркер
```text
/run/hy2xs/rollback/<op-id>/auto-rollback-fired
```
Если он есть — операция откатывается независимо от результата smoke, и в
маркере установки появляется `phase: firewall_guard_fired`. Это значит: сервер
жив и работает на прежнем firewall, а причину, по которой проход не уложился в
окно, надо искать в journal — обычно это медленный старт одного из сервисов.
Юнит автоотката при частичном восстановлении уходит в `failed`, поэтому его
стоит прочитать целиком:
```bash
journalctl -u 'hy2xs-fw-rollback-*' --no-pager
```
Резервные копии не удаляются, пока восстановление не подтверждено, и переживают
долговечную запись `phase: installed`. Поэтому при разборе неудачи всегда
@@ -92,6 +115,8 @@ ls -la /etc/hy2xs/backups/ # копия состояния до пос
cat /etc/hy2xs/backups/*/manifest.json
```
Имя каталога совпадает с полем `op_id` в `/var/lib/hy2xs/install-state.json`.
Манифест прямо говорит, какие файлы существовали до операции, а какие нет:
запись `"present": false` означает, что откат обязан был файл **удалить**, а не
восстановить.
@@ -100,6 +125,47 @@ cat /etc/hy2xs/backups/*/manifest.json
проблему внутри отката: последняя — это информация о том, что осталось не
восстановленным, а не причина отказа.
## 8a. Одна операция за раз
`install`, `reconfigure`, `repair` и `doctor` сериализованы эксклюзивным
замком:
```text
/run/lock/hy2xs-orchestrator.lock
```
Вторая операция отказывает сразу и **до первой мутации** — до снятия резервной
копии, до записи конфигов, до firewall:
```text
another HY2XS operation is already in progress: reconfigure (pid 4242, started at …)
```
`doctor` тоже берёт замок: диагностика в середине транзакции описывает
промежуточное состояние сервера и выдаёт бессмысленные ошибки по временным
несоответствиям.
`status` и `diagnostics collect` замок **не** берут — они нужны в том числе во
время долгой операции, — но сообщают о ней:
```bash
hy2xs-orchestrator status --package-dir /usr/local/lib/hy2xs/package
# "operation_in_progress": "reconfigure (pid 4242, started at …)"
```
Замок снимается при любом завершении держателя: штатном, по `Ctrl+C`, по
SIGTERM от systemd и при обрыве SSH. Если процесс был убит `kill -9`, следующая
операция обнаружит мёртвого держателя и переиспользует замок сама:
```text
operation lock … is held by install (pid 1234), which is no longer running; reclaiming it
```
`/run/lock` — это tmpfs, поэтому перезагрузка снимает замок в любом случае.
Удалять файл руками нужно только если в нём оказалось непонятное содержимое:
такой замок сознательно не переиспользуется автоматически — непонятый файл не
доказывает, что операции нет.
## 9. Reconfigure flow
```bash