fix(memory): retry the scope lock open to absorb a Windows create race (#6870)

On Windows, two threads/processes racing to create a not-yet-existing
lock file with an append open can transiently fail on open() itself with
PermissionError (EACCES), before the byte-range lock is ever attempted.
The open was outside the retry loop, so the error surfaced from
load()/save() instead of being retried. Move the open inside the loop and
close the handle on any OSError before retrying.
This commit is contained in:
万家良 2026-10-14 16:10:56 +08:00 committed by GitHub
parent 064bb50602
commit 4e6c18ad9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -483,12 +483,17 @@ def _atomic_write(path: Path, raw: bytes) -> None:
def _process_file_lock(lock_path: Path, timeout_seconds: float) -> Iterator[None]:
"""Cross-process advisory lock for one scope, using only the stdlib."""
lock_path.parent.mkdir(parents=True, exist_ok=True)
handle = open(lock_path, "a+b")
deadline = time.monotonic() + timeout_seconds
acquired = False
handle: Any = None
try:
while not acquired:
try:
# (Re)open inside the retry loop: on Windows two threads/processes
# racing to create a not-yet-existing lock file with an append
# open transiently raise PermissionError (EACCES) on ``open()``
# itself, before the byte-range lock below is ever attempted.
handle = open(lock_path, "a+b")
if os.name == "nt":
import msvcrt
@ -504,6 +509,9 @@ def _process_file_lock(lock_path: Path, timeout_seconds: float) -> Iterator[None
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
acquired = True
except OSError:
if handle is not None:
handle.close()
handle = None
if time.monotonic() >= deadline:
raise TimeoutError(f"Timed out acquiring memory scope lock {lock_path}")
time.sleep(0.05)
@ -522,7 +530,8 @@ def _process_file_lock(lock_path: Path, timeout_seconds: float) -> Iterator[None
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
except OSError:
logger.warning("Failed to release memory scope lock %s", lock_path)
handle.close()
if handle is not None:
handle.close()
class MemoryStorage(abc.ABC):