From 4e6c18ad9c558fe901a3671d93a7f4362a14c5dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=87=E5=AE=B6=E8=89=AF?= <102887115+wjl8636@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:10:56 +0800 Subject: [PATCH] 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. --- .../memory/backends/deermem/deermem/core/storage.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py index bdf2c9cc27..aae958b3ad 100644 --- a/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py +++ b/backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py @@ -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):