Skip to content

Commit 11dfca4

Browse files
committed
apply thread locking to maybe_install
We need to protect the virtualenv where fromager runs and where we install build tools. This change refactors the thread locking in another function to create a reusable decorator which is then applied to the old function and also to _safe_install(). Cursor logs: https://gist.github.com/dhellmann/b95c1545e462123904cb700793c18ec3 Signed-off-by: Doug Hellmann <dhellmann@redhat.com>
1 parent ec2257d commit 11dfca4

3 files changed

Lines changed: 51 additions & 24 deletions

File tree

src/fromager/build_environment.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from . import dependencies, external_commands, metrics, resolver
1616
from .requirements_file import RequirementType
17+
from .threading_utils import with_thread_lock
1718

1819
if typing.TYPE_CHECKING:
1920
from . import context
@@ -268,6 +269,7 @@ def prepare_build_environment(
268269
return build_env
269270

270271

272+
@with_thread_lock()
271273
def _safe_install(
272274
ctx: context.WorkContext,
273275
req: Requirement,

src/fromager/server.py

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from packaging.utils import parse_wheel_filename
1111

12+
from .threading_utils import with_thread_lock
13+
1214
if typing.TYPE_CHECKING:
1315
from . import context
1416

@@ -59,28 +61,24 @@ def serve_forever(server: http.server.ThreadingHTTPServer) -> None:
5961
return t
6062

6163

62-
# Lock for thread-safe wheel mirror updates
63-
_wheel_mirror_lock = threading.Lock()
64-
65-
64+
@with_thread_lock()
6665
def update_wheel_mirror(ctx: context.WorkContext) -> None:
67-
with _wheel_mirror_lock:
68-
for wheel in ctx.wheels_build.glob("*.whl"):
69-
logger.info("adding %s to local wheel server", wheel.name)
70-
downloads_dest_filename = ctx.wheels_downloads / wheel.name
71-
# Always move the file so the code managing the timer for the
72-
# wheels does not find more than one wheel in the build
73-
# directory.
74-
shutil.move(wheel, downloads_dest_filename)
75-
76-
for wheel in ctx.wheels_downloads.glob("*.whl"):
77-
# Now also symlink the files into the simple hierarchy. We always
78-
# process all files to be safe.
79-
(normalized_name, _, _, _) = parse_wheel_filename(wheel.name)
80-
simple_dest_filename = ctx.wheel_server_dir / normalized_name / wheel.name
81-
if simple_dest_filename.exists():
82-
logger.debug("already have %s", simple_dest_filename)
83-
continue
84-
logger.debug("linking %s into local index", wheel.name)
85-
simple_dest_filename.parent.mkdir(parents=True, exist_ok=True)
86-
simple_dest_filename.symlink_to(wheel)
66+
for wheel in ctx.wheels_build.glob("*.whl"):
67+
logger.info("adding %s to local wheel server", wheel.name)
68+
downloads_dest_filename = ctx.wheels_downloads / wheel.name
69+
# Always move the file so the code managing the timer for the
70+
# wheels does not find more than one wheel in the build
71+
# directory.
72+
shutil.move(wheel, downloads_dest_filename)
73+
74+
for wheel in ctx.wheels_downloads.glob("*.whl"):
75+
# Now also symlink the files into the simple hierarchy. We always
76+
# process all files to be safe.
77+
(normalized_name, _, _, _) = parse_wheel_filename(wheel.name)
78+
simple_dest_filename = ctx.wheel_server_dir / normalized_name / wheel.name
79+
if simple_dest_filename.exists():
80+
logger.debug("already have %s", simple_dest_filename)
81+
continue
82+
logger.debug("linking %s into local index", wheel.name)
83+
simple_dest_filename.parent.mkdir(parents=True, exist_ok=True)
84+
simple_dest_filename.symlink_to(wheel)

src/fromager/threading_utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Threading utilities for the fromager package."""
2+
3+
import functools
4+
import threading
5+
import typing
6+
7+
8+
def with_thread_lock() -> typing.Callable[[typing.Callable], typing.Callable]:
9+
"""Decorator factory that creates a thread-safe wrapper for a function.
10+
11+
Each decorated function gets its own lock instance, ensuring that different
12+
functions using this decorator don't block each other.
13+
14+
Returns:
15+
A decorator that wraps the target function with thread-safe execution
16+
"""
17+
lock = threading.Lock()
18+
19+
def decorator(func: typing.Callable) -> typing.Callable:
20+
@functools.wraps(func)
21+
def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any:
22+
with lock:
23+
return func(*args, **kwargs)
24+
25+
return wrapper
26+
27+
return decorator

0 commit comments

Comments
 (0)