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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user