Skip to content

Commit

Permalink
Make ReentrantFileLock thread-safe and, thereby, fix race condition…
Browse files Browse the repository at this point in the history
… in `virtualenv.cli_run` (#2517)
  • Loading branch information
radoering committed Mar 12, 2023
1 parent e2a9ee5 commit d48565f
Show file tree
Hide file tree
Showing 3 changed files with 37 additions and 4 deletions.
2 changes: 2 additions & 0 deletions docs/changelog/2516.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Make ``ReentrantFileLock`` thread-safe and,
thereby, fix race condition in ``virtualenv.cli_run`` - by :user:`radoering`.
11 changes: 7 additions & 4 deletions src/virtualenv/util/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@ def __init__(self, lock_file):
self.thread_safe = RLock()

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

def release(self, force=False):
with self.thread_safe:
if self.count > 0:
self.thread_safe.release()
if self.count == 1:
super().release(force=force)
self.count = max(self.count - 1, 0)
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/test_util.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import concurrent.futures
import traceback

import pytest

from virtualenv.util.lock import ReentrantFileLock
from virtualenv.util.subprocess import run_cmd


Expand All @@ -6,3 +12,25 @@ def test_run_fail(tmp_path):
assert err
assert not out
assert code


def test_reentrant_file_lock_is_thread_safe(tmp_path):
lock = ReentrantFileLock(tmp_path)
target_file = tmp_path / "target"
target_file.touch()

def recreate_target_file():
with lock.lock_for_key("target"):
target_file.unlink()
target_file.touch()

with concurrent.futures.ThreadPoolExecutor() as executor:
tasks = []
for _ in range(4):
tasks.append(executor.submit(recreate_target_file))
concurrent.futures.wait(tasks)
for task in tasks:
try:
task.result()
except Exception:
pytest.fail(traceback.format_exc())

0 comments on commit d48565f

Please sign in to comment.