deer-flow/backend/tests/test_lark_cli_install_lock.py
sjr666666 2fe325f8ae
fix(lark): retry the Windows install lock instead of failing after ~10s (#6881)
* fix(lark): retry the Windows install lock instead of failing after ~10s

msvcrt.LK_LOCK gives up after roughly ten seconds and surfaces contention
as Permission denied, while a real install (download + setup of the
lark/sandbox CLI) can hold the lock far longer. Retry the non-blocking
acquisition on the Windows fallback branch so concurrent Gateway workers
serialize instead of failing, mirroring the extensions manager's lock
introduced in #6020. The POSIX flock branch is unchanged.

Closes #6879

* test(lark): inject msvcrt stub with raising=False for Linux runners

The regression test patches the module-level ``msvcrt`` attribute, which only
exists on Windows (the POSIX branch imports ``fcntl`` instead), so
``monkeypatch.setattr`` with the default ``raising=True`` raised AttributeError
on Linux CI (backend-unit-tests shard 4).

Pass ``raising=False`` so the stub is injected on POSIX runners: ``fcntl`` is
patched to ``None`` first, and ``_exclusive_install_lock`` then takes the
Windows fallback branch against the fake. Windows behavior is unchanged.

Scope: backend/tests/test_lark_cli_install_lock.py, 1 line.
Verification: pytest tests/test_lark_cli_install_lock.py on Windows
(Python 3.12, pytest 9.0.3) -- 3 passed incl. the real-contention case;
Linux CI rerun is the authoritative check.

* fix(lark): harden and share advisory lock retries

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-10-15 08:03:10 +08:00

186 lines
6.6 KiB
Python

"""Behavior tests for the cross-process install lock in ``lark_cli``.
Windows' blocking lock mode (``msvcrt.LK_LOCK``) gives up after roughly ten
seconds and surfaces contention as ``Permission denied``. A real install
(download + setup of the lark/sandbox CLI) can hold the lock far longer, so
the fallback branch must retry the non-blocking acquisition until the region
frees, mirroring the extensions manager's lock.
"""
from __future__ import annotations
import errno
import threading
import time
from types import SimpleNamespace
import pytest
from deerflow.integrations import lark_cli
class _ContendedFakeMsvcrt:
"""Fake ``msvcrt`` whose lock region stays busy for the first N attempts.
Records every ``locking`` call so tests can assert the retry cadence and
the final unlock, independently of the real Windows semantics.
"""
LK_UNLCK = 0
LK_LOCK = 1
LK_NBLCK = 2
def __init__(self, fail_times: int, *, error_number: int = errno.EDEADLK) -> None:
self.fail_times = fail_times
self.error_number = error_number
self.attempts: list[int] = []
self.unlocked = 0
def locking(self, fd: int, mode: int, nbytes: int) -> None:
self.attempts.append(mode)
if mode == self.LK_UNLCK:
self.unlocked += 1
return None
blocking_attempts = len(self.attempts) - self.unlocked
if blocking_attempts <= self.fail_times:
raise OSError(self.error_number, "Lock unavailable")
return None
def test_install_lock_retries_until_contention_clears(tmp_path, monkeypatch) -> None:
"""Contention is retried on the Windows fallback instead of failing fast."""
fake = _ContendedFakeMsvcrt(fail_times=3)
monkeypatch.setattr(lark_cli, "fcntl", None)
monkeypatch.setattr(lark_cli, "msvcrt", fake, raising=False)
sleeps: list[float] = []
monkeypatch.setattr(lark_cli, "time", SimpleNamespace(sleep=sleeps.append, monotonic=time.monotonic))
lock_path = tmp_path / ".lark-cli.install.lock"
entered = False
with lark_cli._exclusive_install_lock(lock_path, threading.Lock()):
entered = True
assert entered
assert fake.attempts == [fake.LK_NBLCK] * 4 + [fake.LK_UNLCK]
assert fake.unlocked == 1
assert sleeps == [0.2] * 3
def test_install_lock_posix_branch_still_flocks(tmp_path, monkeypatch) -> None:
"""The POSIX branch keeps using ``fcntl.flock`` unchanged."""
class _FakeFcntl:
LOCK_EX = 2
LOCK_UN = 8
def __init__(self) -> None:
self.ops: list[int] = []
def flock(self, fd: int, operation: int) -> None:
self.ops.append(operation)
fake_fcntl = _FakeFcntl()
monkeypatch.setattr(lark_cli, "fcntl", fake_fcntl)
lock_path = tmp_path / ".sandbox-cli.install.lock"
entered = False
with lark_cli._exclusive_install_lock(lock_path, threading.Lock()):
entered = True
assert entered
assert fake_fcntl.ops == [_FakeFcntl.LOCK_EX, _FakeFcntl.LOCK_UN]
@pytest.mark.skipif(lark_cli.fcntl is not None, reason="exercises the real msvcrt fallback branch")
def test_install_lock_waits_for_real_windows_contention(tmp_path) -> None:
"""A real second contender acquires the lock once the holder releases.
The holder keeps the region past ``LK_LOCK``'s ~10s give-up window: a
blocking-mode contender dies with ``Permission denied`` there, while a
retrying contender acquires as soon as the holder lets go.
"""
import msvcrt
lock_path = tmp_path / ".lark-cli.install.lock"
lock_path.write_bytes(b"\0")
holder = lock_path.open("a+b")
holder.seek(0)
msvcrt.locking(holder.fileno(), msvcrt.LK_NBLCK, 1)
acquired = threading.Event()
release = threading.Event()
errors: list[BaseException] = []
def contender() -> None:
try:
with lark_cli._exclusive_install_lock(lock_path, threading.Lock()):
acquired.set()
release.wait(timeout=30)
except BaseException as exc: # surfaced via the assertion below
errors.append(exc)
thread = threading.Thread(target=contender, daemon=True)
thread.start()
try:
# Keep the region locked past LK_LOCK's ~10s give-up window before
# releasing it: a blocking-mode contender dies there, a retrying one
# acquires as soon as the holder lets go.
time.sleep(12)
holder.seek(0)
msvcrt.locking(holder.fileno(), msvcrt.LK_UNLCK, 1)
holder.close()
assert acquired.wait(timeout=5), f"contender failed to acquire the contended lock; errors={errors!r}"
finally:
release.set()
thread.join(timeout=10)
assert not errors
@pytest.mark.parametrize("error_number", [errno.EBADF, errno.EINVAL, errno.EIO, errno.EPERM])
def test_install_lock_propagates_non_contention_errors(tmp_path, monkeypatch, error_number):
fake = _ContendedFakeMsvcrt(fail_times=1, error_number=error_number)
monkeypatch.setattr(lark_cli, "fcntl", None)
monkeypatch.setattr(lark_cli, "msvcrt", fake, raising=False)
def unexpected_sleep(_delay):
raise AssertionError("Permanent lock errors must not be retried")
monkeypatch.setattr(lark_cli, "time", SimpleNamespace(sleep=unexpected_sleep, monotonic=lambda: 0.0))
thread_lock = threading.Lock()
with pytest.raises(OSError) as caught:
with lark_cli._exclusive_install_lock(tmp_path / ".lock", thread_lock):
pytest.fail("Must not enter after a failed acquisition")
assert caught.value.errno == error_number
assert fake.attempts == [fake.LK_NBLCK]
assert fake.unlocked == 0
assert not thread_lock.locked()
def test_install_lock_contention_has_a_deadline(tmp_path, monkeypatch):
fake = _ContendedFakeMsvcrt(fail_times=100)
monkeypatch.setattr(lark_cli, "fcntl", None)
monkeypatch.setattr(lark_cli, "msvcrt", fake, raising=False)
monkeypatch.setattr(lark_cli, "_INSTALL_LOCK_TIMEOUT_SECONDS", 0.5, raising=False)
now = 0.0
sleeps = []
def sleep(delay):
nonlocal now
sleeps.append(delay)
now += delay
assert now <= 1, "Lock acquisition must not retry forever"
monkeypatch.setattr(lark_cli, "time", SimpleNamespace(sleep=sleep, monotonic=lambda: now))
thread_lock = threading.Lock()
with pytest.raises(TimeoutError, match="Lark install lock timed out"):
with lark_cli._exclusive_install_lock(tmp_path / ".lock", thread_lock):
pytest.fail("Must not enter while the lock is held")
assert sleeps == pytest.approx([0.2, 0.2, 0.1])
assert fake.attempts == [fake.LK_NBLCK] * 4
assert fake.unlocked == 0
assert not thread_lock.locked()