fix(swap): activate the previous swapfile in place when it cannot be moved back [P0]
The rollback still had a path that ended with two intact swapfiles on disk and neither of them running. With the original already off and moved to .previous, a failed rename back returned immediately - "the host may currently have no swap" - without trying to activate anything. Both candidates were sitting right there: the old swapfile under its .previous name, and the replacement that the earlier fix had preserved. That return threw away the whole point of preserving them. The rename back was never the requirement. The kernel activates a swap area by path and does not care that the path ends in .previous, so the old swapfile is brought up where it lies. The rollback now tracks both files by where they currently are rather than by a flag, and works down the candidates: original at the managed path, original at .previous, replacement wherever it ended up. Only when none of them activates does it report that the host has no managed swap - and then that statement is true. Two consequences fall out of tracking locations: - A replacement that could not be moved aside is still on the managed path, so the restore no longer renames the original over it. That rename would have unlinked one of the two files this rollback exists to keep. - When the original ends up serving from .previous the replacement is not released and nothing is deleted. The host has swap, so nothing is urgent, but the managed path does not hold what the ownership record describes - that is an operator's call, and the error names the location of both files and the commands that finish it. Three tests: the rename back fails and the previous swapfile is activated in place; it fails and the previous swapfile will not activate either, so the replacement takes over; a live replacement that cannot be released is left in place and nothing else is attempted. The last one needed a swapoff failure, which the fake could not express before. fix(daemon): let the docker events subsystem return to healthy [P1] The outer loop recorded an error every time the events stream ended, and nothing ever recorded a success for it. One Docker restart therefore pinned a failure in status permanently while the replacement stream was running normally. The reconnect now clears it and logs the recovery once. The per-subsystem transition logic moved onto LastActionState as record_ok/record_err, so it can be tested without the /run writes that set_ok/set_err perform. Clearing the last error also clears its timestamp - otherwise status showed an empty error next to the time it stopped being true. fix(swap): drop transition leftovers once the managed swap is confirmed live [P1] Leftovers were only cleared on the way *into* a resize. A process killed after the replacement went live left .previous on disk indefinitely: the next run finds a valid, active, correctly owned swapfile at the managed path, takes the fast path, and never looks. A swapfile is commonly sized after RAM, so that is not a rounding error. They are now dropped right after the managed swap is confirmed active, and only when inactive - an active leftover means the kernel is still swapping to it, which is the interrupted-rollback case that needs an operator. This also replaces two tests that asserted nothing about production code: both rebuilt the active-leftover check inline instead of calling it. Docs: the resize transition and its three degraded outcomes are now written down in IMPLEMENTATION_SPEC rather than living only in the code, the reconcile order gained the cleanup step, per-subsystem error tracking is described, and the acceptance checklist gained the fault cases to exercise on Debian - each one keyed to the exact wording the error must carry. cargo fmt --check, clippy -D warnings, cargo test --locked --workspace --release: 199 + 4 passed.
This commit is contained in:
@@ -485,6 +485,8 @@ Status обязан показывать наличие таблицы/chain/п
|
||||
- `/run/awg2-harness/last_action.state` — последний успех и последняя ошибка;
|
||||
- `/run/awg2-harness/events.log` — журнал событий, ограниченный 1000 строками и 256 KiB.
|
||||
|
||||
Ошибки отслеживаются по подсистемам (`traffic`, `firewall`, `swap`, `sysctl_tuning`, `docker_events`), а не одним общим полем. Общее поле в `last_action.state` показывает ту ошибку, которая ещё не устранена: успешный reconcile одной подсистемы не гасит незакрытую ошибку другой и не объявляет восстановление того, что не падало. Поток `docker events` при успешном переподключении тоже отмечается как восстановленный — иначе один рестарт Docker оставлял бы ошибку в `status` навсегда при исправно работающем новом потоке.
|
||||
|
||||
В `events.log` пишутся только **переходы состояния**: первое появление ошибки, изменение текста ошибки и восстановление после неё. Рутинный успех не пишется. Раньше каждый успешный цикл добавлял строку — при интервале 15 секунд это порядка 17 тысяч записей в сутки, каждая с полным чтением, дополнением и перезаписью файла, и на их фоне терялись записи, ради которых журнал и читают. Полная поцикловая картина остаётся в journald через tracing.
|
||||
|
||||
## 14. Блокировки
|
||||
@@ -632,14 +634,31 @@ inode = 123456
|
||||
3. подготовить управляемый swapfile (с учётом владения)
|
||||
4. swapon управляемого swap
|
||||
5. убедиться, что он реально появился в списке активных
|
||||
6. только теперь swapoff чужих swap
|
||||
7. перезаписать /etc/fstab транзакцией
|
||||
8. применить swappiness
|
||||
9. проверить итоговое состояние
|
||||
6. удалить неактивные остатки прерванной замены (.new, .previous)
|
||||
7. только теперь swapoff чужих swap
|
||||
8. перезаписать /etc/fstab транзакцией
|
||||
9. применить swappiness
|
||||
10. проверить итоговое состояние
|
||||
```
|
||||
|
||||
Шаги 2–5 не выполняют ни одного деструктивного действия над чужим состоянием. Прежний порядок отключал чужие swap первым шагом, поэтому ошибка в `fallocate`, `dd`, `mkswap` или `swapon` оставляла хост вообще без swap.
|
||||
|
||||
Шаг 6 нужен потому, что вычистка остатков перед заменой (шаг 3) не покрывает процесс, убитый уже **после** активации нового файла: следующий запуск видит по основному пути валидный, активный и корректно принадлежащий нам swapfile, идёт быстрым путём и на остатки не смотрит. Размер swapfile обычно сопоставим с объёмом RAM, поэтому «когда-нибудь» здесь не годится. Удаляются только неактивные остатки; активный означает, что ядро всё ещё свопит в него, — это случай прерванного отката, и он требует оператора, а не догадки.
|
||||
|
||||
### Замена активного swapfile
|
||||
|
||||
Живой swapfile нельзя изменить в размере на месте, поэтому изменение `swap.size_mb` выполняется заменой. Новый файл создаётся и активируется, **пока старый ещё работает**; старый затем не удаляется, а откладывается под именем `.awg2-harness.previous`, и до подтверждения нового файла оба лежат на диске.
|
||||
|
||||
Инвариант отката один: откат никогда не оставляет хост без swap. Он сформулирован не как «вернуть исходное состояние любой ценой» — исходное состояние может быть недостижимо, — а как «на хосте должен работать хотя бы один подготовленный swap». Отсюда порядок восстановления:
|
||||
|
||||
1. активную замену сначала выключить; если `swapoff` не удался, она остаётся на месте и **продолжает обслуживать** — двигать файл, в который ядро свопит, значит только рассинхронизировать `/proc/swaps` с файловой системой;
|
||||
2. неактивную замену не удалять, а отодвинуть под staging-имя: пока исходный файл не подтверждён рабочим, замена — единственная вторая swap-область на хосте;
|
||||
3. вернуть исходный файл на основной путь и активировать его с **прежним** priority, а не с применяемым: применяемый мог оказаться ровно тем, что ядро отвергло;
|
||||
4. если `rename` обратно не удался, исходный файл активируется прямо по пути `.awg2-harness.previous` — ядро активирует swap-область по пути, и имя файла для этого значения не имеет;
|
||||
5. если исходный файл поднять не удалось вовсе, активируется отложенная замена.
|
||||
|
||||
Соответственно у отката есть три деградированных исхода, и в каждом текст ошибки описывает **фактическое** состояние, а не намерение: замена осталась активной по основному пути; исходный swap работает из `.previous` и оба файла целы; ни один кандидат не активировался — хост без управляемого swap. Первые два сопровождаются точными командами, которыми оператор доводит состояние до нормального, когда хост это позволит.
|
||||
|
||||
### Безопасность `swap.path`
|
||||
|
||||
Проверка разделена на две части. Лексическая не обращается к файловой системе и потому пригодна для `validate-config` на хосте, где файла ещё нет:
|
||||
|
||||
Reference in New Issue
Block a user