fix(state): require nlink==0 before treating a /proc fd as an unlinked WAL sidecar

iter_deleted_sqlite_sidecar_holders() and SessionDB._wal_generation_was_lost()
both treated a `` (deleted)`` suffix on a /proc/<pid>/fd/* target as proof that
state.db-wal or state.db-shm was unlinked. On OpenZFS that suffix is not proof:
a live, still-linked file whose dentry was unhashed is reported the same way,
with st_nlink still 1 and the same (dev, ino) as the path. The guard then fires
permanently and the gateway falls back to JSONL forever, because the WAL was
never actually deleted.

Add _fd_is_truly_unlinked(), which confirms via os.stat(fd_path).st_nlink == 0
before a target counts as an orphaned generation. An unstattable descriptor
still counts as deleted, so the guard keeps failing closed. _iter_proc_fd_targets()
and _proc_fd_targets() now also yield the /proc fd path itself so both call
sites (open-path and the sticky write-path probe) can run the check.
This commit is contained in:
chelsealong
2026-09-10 14:10:08 +00:00
committed by Teknium
parent 7dec81568e
commit 84a3c4de74
4 changed files with 89 additions and 12 deletions

View File

@@ -69,13 +69,14 @@ _fd_usage_lock = threading.Lock()
_fd_usage_cache: "tuple[float, Optional[int]]" = (0.0, None)
def _proc_fd_targets(pid: int) -> Iterator[str]:
"""readlink() of every entry in /proc/<pid>/fd (unreadable links skipped).
Raises OSError when the fd directory itself cannot be listed."""
def _proc_fd_targets(pid: int) -> "Iterator[tuple[str, str]]":
"""Yield ``(readlink target, fd path)`` for every entry in /proc/<pid>/fd (unreadable
links skipped). Raises OSError when the fd directory itself cannot be listed."""
fd_dir = f"/proc/{pid}/fd"
for fd in os.listdir(fd_dir):
fd_path = f"{fd_dir}/{fd}"
try:
yield os.readlink(f"{fd_dir}/{fd}")
yield os.readlink(fd_path), fd_path
except OSError:
continue