Skip to content

Commit f00c062

Browse files
fix(concurrency): add inter-process install lock and cache recheck to prevent parallel download races (#700, #631) (#725)
1 parent 971da2b commit f00c062

3 files changed

Lines changed: 105 additions & 3 deletions

File tree

tests/test_manager_concurrency.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from webdriver_manager.core.manager import DriverManager
2+
3+
4+
class FakeCacheManager:
5+
def __init__(self):
6+
self.find_calls = 0
7+
self.saved = False
8+
9+
def find_driver(self, _driver):
10+
self.find_calls += 1
11+
if self.find_calls == 1:
12+
return None
13+
return "/tmp/cached/chromedriver"
14+
15+
def get_driver_lock_path(self, _driver_name, _os_type):
16+
return "/tmp/wdm-test-lock"
17+
18+
def save_file_to_cache(self, _driver, _file):
19+
self.saved = True
20+
return "/tmp/new/chromedriver"
21+
22+
23+
class FakeDownloadManager:
24+
def __init__(self):
25+
self.download_calls = 0
26+
self.http_client = None
27+
28+
def download_file(self, _url):
29+
self.download_calls += 1
30+
return object()
31+
32+
33+
class FakeDriver:
34+
def get_name(self):
35+
return "chromedriver"
36+
37+
def get_driver_download_url(self, _os_type):
38+
return "https://example.invalid/chromedriver.zip"
39+
40+
def get_browser_type(self):
41+
return "google-chrome"
42+
43+
44+
class FakeOSManager:
45+
def get_os_type(self):
46+
return "linux64"
47+
48+
49+
def test_get_driver_binary_path_rechecks_cache_after_lock(monkeypatch):
50+
cache = FakeCacheManager()
51+
downloader = FakeDownloadManager()
52+
manager = DriverManager(
53+
download_manager=downloader,
54+
cache_manager=cache,
55+
os_system_manager=FakeOSManager(),
56+
)
57+
58+
monkeypatch.setattr(DriverManager, "_acquire_lock", staticmethod(lambda _path, timeout=60.0, poll_interval=0.1: 1))
59+
monkeypatch.setattr(DriverManager, "_release_lock", staticmethod(lambda _fd, _path: None))
60+
61+
path = manager._get_driver_binary_path(FakeDriver())
62+
63+
assert path == "/tmp/cached/chromedriver"
64+
assert cache.find_calls == 2
65+
assert downloader.download_calls == 0
66+
assert cache.saved is False

webdriver_manager/core/driver_cache.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def __init__(self, root_dir=None, valid_range=1, file_manager=None, os_system_ma
4444
if not self._file_manager:
4545
self._file_manager = FileManager(self._os_system_manager)
4646

47+
def get_driver_lock_path(self, driver_name: str, os_type: str) -> str:
48+
os.makedirs(self._root_dir, exist_ok=True)
49+
return os.path.join(self._root_dir, f".wdm-lock-{driver_name}-{os_type}")
50+
4751
def save_archive_file(self, file: File, path):
4852
return self._file_manager.save_archive_file(file, path)
4953

webdriver_manager/core/manager.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from webdriver_manager.core.driver_cache import DriverCacheManager
33
from webdriver_manager.core.logger import log
44
from webdriver_manager.core.os_manager import OperationSystemManager
5+
import os
6+
import time
57

68

79
class DriverManager(object):
@@ -38,9 +40,39 @@ def _get_driver_binary_path(self, driver):
3840
return binary_path
3941

4042
os_type = self.get_os_type()
41-
file = self._download_manager.download_file(driver.get_driver_download_url(os_type))
42-
binary_path = self._cache_manager.save_file_to_cache(driver, file)
43-
return binary_path
43+
lock_path = self._cache_manager.get_driver_lock_path(driver.get_name(), os_type)
44+
lock_fd = self._acquire_lock(lock_path)
45+
try:
46+
# Re-check cache after lock to avoid duplicate downloads in concurrent runs.
47+
binary_path = self._cache_manager.find_driver(driver)
48+
if binary_path:
49+
return binary_path
50+
51+
file = self._download_manager.download_file(driver.get_driver_download_url(os_type))
52+
binary_path = self._cache_manager.save_file_to_cache(driver, file)
53+
return binary_path
54+
finally:
55+
self._release_lock(lock_fd, lock_path)
4456

4557
def get_os_type(self):
4658
return self._os_system_manager.get_os_type()
59+
60+
@staticmethod
61+
def _acquire_lock(lock_path: str, timeout: float = 60.0, poll_interval: float = 0.1):
62+
start = time.time()
63+
while True:
64+
try:
65+
return os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_RDWR)
66+
except FileExistsError:
67+
if time.time() - start >= timeout:
68+
raise TimeoutError(f"Timed out waiting for webdriver-manager lock: {lock_path}")
69+
time.sleep(poll_interval)
70+
71+
@staticmethod
72+
def _release_lock(lock_fd, lock_path: str):
73+
if lock_fd is not None:
74+
os.close(lock_fd)
75+
try:
76+
os.unlink(lock_path)
77+
except FileNotFoundError:
78+
pass

0 commit comments

Comments
 (0)