Skip to content

Commit d48565f

Browse files
authored
Make ReentrantFileLock thread-safe and, thereby, fix race condition in virtualenv.cli_run (#2517)
1 parent e2a9ee5 commit d48565f

File tree

3 files changed

+37
-4
lines changed

3 files changed

+37
-4
lines changed

docs/changelog/2516.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Make ``ReentrantFileLock`` thread-safe and,
2+
thereby, fix race condition in ``virtualenv.cli_run`` - by :user:`radoering`.

src/virtualenv/util/lock.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@ def __init__(self, lock_file):
2323
self.thread_safe = RLock()
2424

2525
def acquire(self, timeout=None, poll_interval=0.05):
26-
with self.thread_safe:
27-
if self.count == 0:
28-
super().acquire(timeout, poll_interval)
29-
self.count += 1
26+
if not self.thread_safe.acquire(timeout=-1 if timeout is None else timeout):
27+
raise Timeout(self.lock_file)
28+
if self.count == 0:
29+
super().acquire(timeout, poll_interval)
30+
self.count += 1
3031

3132
def release(self, force=False):
3233
with self.thread_safe:
34+
if self.count > 0:
35+
self.thread_safe.release()
3336
if self.count == 1:
3437
super().release(force=force)
3538
self.count = max(self.count - 1, 0)

tests/unit/test_util.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import concurrent.futures
2+
import traceback
3+
4+
import pytest
5+
6+
from virtualenv.util.lock import ReentrantFileLock
17
from virtualenv.util.subprocess import run_cmd
28

39

@@ -6,3 +12,25 @@ def test_run_fail(tmp_path):
612
assert err
713
assert not out
814
assert code
15+
16+
17+
def test_reentrant_file_lock_is_thread_safe(tmp_path):
18+
lock = ReentrantFileLock(tmp_path)
19+
target_file = tmp_path / "target"
20+
target_file.touch()
21+
22+
def recreate_target_file():
23+
with lock.lock_for_key("target"):
24+
target_file.unlink()
25+
target_file.touch()
26+
27+
with concurrent.futures.ThreadPoolExecutor() as executor:
28+
tasks = []
29+
for _ in range(4):
30+
tasks.append(executor.submit(recreate_target_file))
31+
concurrent.futures.wait(tasks)
32+
for task in tasks:
33+
try:
34+
task.result()
35+
except Exception:
36+
pytest.fail(traceback.format_exc())

0 commit comments

Comments
 (0)