Skip to content

Commit 7ab501d

Browse files
committed
pytest_plugin(refactor): Use _internal.file_lock for atomic initialization
why: Consolidate locking logic into dedicated, well-tested module. what: - Import atomic_init from libvcs._internal.file_lock - Remove inline _acquire_lock, _release_lock, _is_lock_stale, _atomic_repo_init - Replace _atomic_repo_init calls with atomic_init (4 locations) - Remove unused contextlib import - Net reduction: 122 lines removed, 5 lines added
1 parent c9a9f74 commit 7ab501d

1 file changed

Lines changed: 5 additions & 122 deletions

File tree

src/libvcs/pytest_plugin.py

Lines changed: 5 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import asyncio
6-
import contextlib
76
import dataclasses
87
import functools
98
import getpass
@@ -21,6 +20,7 @@
2120
import pytest
2221

2322
from libvcs import exc
23+
from libvcs._internal.file_lock import atomic_init
2424
from libvcs._internal.run import _ENV, run
2525
from libvcs.sync.git import GitRemote, GitSync
2626
from libvcs.sync.hg import HgSync
@@ -149,123 +149,6 @@ def get_vcs_version(cmd: list[str]) -> str:
149149
return "not-installed"
150150

151151

152-
# Stale lock timeout (5 minutes - covers slow hg operations)
153-
_LOCK_TIMEOUT = 5 * 60
154-
155-
156-
def _acquire_lock(lock_path: pathlib.Path) -> int | None:
157-
"""Atomically acquire lock file. Returns fd if acquired, None otherwise.
158-
159-
Uses filelock's SoftFileLock pattern: os.O_CREAT | os.O_EXCL for atomicity.
160-
"""
161-
try:
162-
fd = os.open(
163-
str(lock_path),
164-
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
165-
0o644,
166-
)
167-
except FileExistsError:
168-
return None
169-
else:
170-
# Write PID for debugging stale locks
171-
os.write(fd, str(os.getpid()).encode())
172-
return fd
173-
174-
175-
def _release_lock(lock_path: pathlib.Path, fd: int) -> None:
176-
"""Release lock file."""
177-
os.close(fd)
178-
with contextlib.suppress(OSError):
179-
lock_path.unlink()
180-
181-
182-
def _is_lock_stale(lock_path: pathlib.Path) -> bool:
183-
"""Check if lock is stale (older than timeout)."""
184-
try:
185-
mtime = lock_path.stat().st_mtime
186-
return time.time() - mtime > _LOCK_TIMEOUT
187-
except OSError:
188-
return True
189-
190-
191-
def _atomic_repo_init(
192-
repo_path: pathlib.Path,
193-
init_fn: t.Callable[[], None],
194-
marker_name: str = ".libvcs_initialized",
195-
timeout: float = 60.0,
196-
poll_interval: float = 0.05,
197-
) -> bool:
198-
"""Atomically initialize a repository with file-based lock coordination.
199-
200-
Uses filelock-inspired pattern for pytest-xdist worker coordination.
201-
Two-file approach: .lock (temporary) vs marker (permanent).
202-
203-
Parameters
204-
----------
205-
repo_path : pathlib.Path
206-
Path to the repository directory to initialize
207-
init_fn : Callable[[], None]
208-
Function to call to perform initialization (creates repo_path)
209-
marker_name : str
210-
Name of the marker file indicating successful completion
211-
timeout : float
212-
Maximum seconds to wait for another process to complete
213-
poll_interval : float
214-
Seconds between polling attempts (default 50ms like filelock)
215-
216-
Returns
217-
-------
218-
bool
219-
True if this process performed initialization, False if waited for another
220-
"""
221-
marker = repo_path / marker_name
222-
lock_path = repo_path.parent / f".{repo_path.name}.lock"
223-
224-
# Fast path: already initialized
225-
if marker.exists():
226-
return False
227-
228-
# Ensure parent directory exists for lock file
229-
lock_path.parent.mkdir(parents=True, exist_ok=True)
230-
231-
start_time = time.perf_counter()
232-
233-
while True:
234-
# Try to acquire lock
235-
fd = _acquire_lock(lock_path)
236-
237-
if fd is not None:
238-
# We got the lock
239-
try:
240-
# Double-check marker (another process may have finished)
241-
if marker.exists():
242-
return False
243-
# Clean partial state and initialize
244-
if repo_path.exists():
245-
shutil.rmtree(repo_path)
246-
init_fn()
247-
marker.touch()
248-
return True
249-
finally:
250-
_release_lock(lock_path, fd)
251-
252-
# Lock held by another process - check if done or stale
253-
if marker.exists():
254-
return False
255-
256-
if _is_lock_stale(lock_path):
257-
with contextlib.suppress(OSError):
258-
lock_path.unlink()
259-
continue # Retry immediately
260-
261-
# Timeout check
262-
if time.perf_counter() - start_time >= timeout:
263-
msg = f"Timeout waiting for {repo_path} initialization"
264-
raise TimeoutError(msg)
265-
266-
time.sleep(poll_interval)
267-
268-
269152
def get_cache_key() -> str:
270153
"""Generate cache key from VCS versions and libvcs version.
271154
@@ -805,7 +688,7 @@ def do_init() -> None:
805688
env=git_commit_envvars,
806689
)
807690

808-
_atomic_repo_init(repo_path, do_init)
691+
atomic_init(repo_path, do_init, marker_name=".libvcs_initialized")
809692
return repo_path
810693

811694

@@ -929,7 +812,7 @@ def svn_remote_repo(
929812
def do_init() -> None:
930813
shutil.copytree(empty_svn_repo, repo_path)
931814

932-
_atomic_repo_init(repo_path, do_init)
815+
atomic_init(repo_path, do_init, marker_name=".libvcs_initialized")
933816
return repo_path
934817

935818

@@ -954,7 +837,7 @@ def do_init() -> None:
954837
shutil.copytree(svn_remote_repo, repo_path)
955838
svn_remote_repo_single_commit_post_init(remote_repo_path=repo_path)
956839

957-
_atomic_repo_init(repo_path, do_init)
840+
atomic_init(repo_path, do_init, marker_name=".libvcs_initialized")
958841
return repo_path
959842

960843

@@ -1073,7 +956,7 @@ def do_init() -> None:
1073956
env={"HGRCPATH": str(hgconfig)},
1074957
)
1075958

1076-
_atomic_repo_init(repo_path, do_init)
959+
atomic_init(repo_path, do_init, marker_name=".libvcs_initialized")
1077960
return repo_path
1078961

1079962

0 commit comments

Comments
 (0)