diff --git a/hermes_state_dbfile.py b/hermes_state_dbfile.py index b0acf2141f..1aaa326005 100644 --- a/hermes_state_dbfile.py +++ b/hermes_state_dbfile.py @@ -10,6 +10,7 @@ call time, so tests that monkeypatch ``hermes_state.`` keep intercepting. from __future__ import annotations import contextlib +import errno import hashlib import json import logging @@ -165,11 +166,12 @@ def _fd_is_truly_unlinked(fd_path: str, watched_path: str) -> bool: names — the guard keeps failing closed.""" try: fd_stat = os.stat(fd_path) - except FileNotFoundError: - # The descriptor was closed after /proc was read. It cannot keep a - # retired generation alive, so do not turn this scan race into a halt. - return False - except OSError: + except OSError as exc: + # ENOENT: the descriptor was closed after /proc was read. ESRCH: the whole + # process exited mid-scan. Neither can keep a retired generation alive, so + # do not turn this scan race into a refusal (mirrors hermes_state_holders). + if exc.errno in (errno.ENOENT, errno.ESRCH): + return False return True return _identity_is_truly_unlinked((fd_stat.st_dev, fd_stat.st_ino), watched_path) diff --git a/tests/hermes_state/test_deleted_wal_generation_guard.py b/tests/hermes_state/test_deleted_wal_generation_guard.py index bfa16643ee..ae4cfc1710 100644 --- a/tests/hermes_state/test_deleted_wal_generation_guard.py +++ b/tests/hermes_state/test_deleted_wal_generation_guard.py @@ -7,6 +7,7 @@ fail closed on both the open and write paths instead of creating the second generation. """ +import errno import gc import os import sqlite3 @@ -160,8 +161,10 @@ def test_iter_holders_ignores_live_unhashed_dentry(tmp_path, force_wal, monkeypa db.close() -def test_iter_holders_ignores_descriptor_closed_during_scan(tmp_path, monkeypatch): - """A descriptor gone after ``readlink`` cannot hold a retired generation.""" +@pytest.mark.parametrize("vanish_errno", [errno.ENOENT, errno.ESRCH]) +def test_iter_holders_ignores_descriptor_closed_during_scan(tmp_path, monkeypatch, vanish_errno): + """A descriptor (ENOENT) or its whole process (ESRCH) gone after ``readlink`` + cannot hold a retired generation.""" path = tmp_path / "state.db" wal = Path(str(path) + "-wal") wal.write_bytes(b"current generation") @@ -172,6 +175,14 @@ def test_iter_holders_ignores_descriptor_closed_during_scan(tmp_path, monkeypatc "_iter_proc_fd_targets", lambda: iter([(os.getpid(), str(wal) + " (deleted)", str(vanished_fd))]), ) + real_stat = os.stat + + def stat_vanished(target, *args, **kwargs): + if str(target) == str(vanished_fd): + raise OSError(vanish_errno, os.strerror(vanish_errno), str(target)) + return real_stat(target, *args, **kwargs) + + monkeypatch.setattr(hermes_state_dbfile.os, "stat", stat_vanished) assert iter_deleted_sqlite_sidecar_holders(path) == []