refactor(lock): use kernel file locking instead of a hand-rolled PID lock [P2]

The previous implementation had every failure mode a home-grown PID lock has:

- contention was detected by matching the substring "lock already exists" in an error
  message, so any change to the message would have turned a busy lock into a hard error;
- stale-lock recovery removed the file and created it again as two separate operations,
  so two processes could each decide the lock was stale and each take it;
- a reused PID made a live lock look stale, since the check was only whether /proc/<pid>
  exists;
- Drop deleted the lock file, racing with a process that had already opened it and was
  sleeping on the retry loop - it would then lock a file that no longer has a directory
  entry, and the next arrival would create a fresh one and lock that instead.

Replaced with advisory kernel locking through std::fs::File::try_lock, stabilised in Rust
1.89 and backed by flock on Linux. No new dependency. The kernel releases the lock when
the holder exits by any means, including kill -9, so there is no stale state to reason
about and no recovery heuristic to get wrong. The PID is still written to the file, but
purely as diagnostics - nothing reads it back to make a decision.

The lock file is now never deleted. Its presence carries no meaning; only the lock held
on it does. That removes the race the old Drop introduced.

Five tests: a second acquisition of a live lock fails, timeout reports lock busy timeout,
the lock is free again once the guard drops, the file survives release, and a leftover
file from a previous run does not block acquisition.

Behaviour note for the four call sites (traffic, firewall, sysctl_tuning, swap): timeouts
are unchanged, and a process killed mid-reconcile no longer leaves anything that needs
cleaning up before the next run.

cargo test --workspace: 158 + 4 passed. clippy -D warnings clean.
This commit is contained in:
2026-08-06 22:56:59 +05:00
parent a9b6e3c15d
commit bf43b763cd
2 changed files with 111 additions and 66 deletions
+7 -3
View File
@@ -494,12 +494,16 @@ Status обязан показывать наличие таблицы/chain/п
| `/run/awg2-harness/firewall.lock` | 10 с | `apply_firewall` (host + anti-torrent) |
| `/run/awg2-harness/sysctl_tuning.lock` | 30 с | `ensure_sysctl_tuning` |
Реализация — advisory-блокировка ядра (`flock` на Linux) через `std::fs::File`, а не PID-файл.
Поведение:
- ожидание захвата — с интервалом 100 мс до таймаута, затем ошибка `lock busy timeout`;
- в файл записывается PID владельца;
- если процесс-владелец мёртв (`/proc/<pid>` отсутствует) или содержимое файла не парсится как PID, блокировка считается устаревшей и перезахватывается;
- освобождение автоматическое при выходе из области видимости, включая пути с ошибкой.
- PID пишется в файл **только как диагностика**; ни одно решение на его основе не принимается;
- освобождение выполняет ядро при завершении процесса — любым способом, включая `kill -9`. Понятия «устаревшая блокировка» не существует;
- файл блокировки никогда не удаляется: сам факт его наличия ничего не значит, значение имеет только удерживаемая на нём блокировка.
Прежняя реализация на PID-файлах имела типичные для такого подхода дефекты: конфликт определялся по подстроке в тексте ошибки; «устаревшая» блокировка удалялась и создавалась заново двумя отдельными операциями, поэтому два процесса могли одновременно счесть её устаревшей и оба захватить; переиспользованный PID делал живую блокировку похожей на мёртвую; удаление файла в `Drop` гонялось с процессом, который уже открыл его и ждёт.
## 15. Контракт вывода status