Skip to content

Commit

Permalink
Fixed runtime locking (#184)
Browse files Browse the repository at this point in the history
  • Loading branch information
openvmp authored Sep 15, 2024
1 parent ff18c49 commit 93b104d
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 67 deletions.
25 changes: 15 additions & 10 deletions partcad/src/partcad/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def __init__(self, root_path=None, search_root=True):

self.option_create_dirs = False
self.runtimes_python = {}
self.runtimes_python_lock = threading.Lock()

self.stats_packages = 0
self.stats_packages_instantiated = 0
Expand Down Expand Up @@ -929,16 +930,20 @@ def render(self, project_path=None, format=None, output_dir=None):
project.render(format=format, output_dir=output_dir)

def get_python_runtime(self, version=None, python_runtime=None):
if version is None:
version = "%d.%d" % (sys.version_info.major, sys.version_info.minor)
if python_runtime is None:
python_runtime = user_config.python_runtime
runtime_name = python_runtime + "-" + version
if not runtime_name in self.runtimes_python:
self.runtimes_python[runtime_name] = runtime_python_all.create(
self, version, python_runtime
)
return self.runtimes_python[runtime_name]
with self.runtimes_python_lock:
if version is None:
version = "%d.%d" % (
sys.version_info.major,
sys.version_info.minor,
)
if python_runtime is None:
python_runtime = user_config.python_runtime
runtime_name = python_runtime + "-" + version
if not runtime_name in self.runtimes_python:
self.runtimes_python[runtime_name] = runtime_python_all.create(
self, version, python_runtime
)
return self.runtimes_python[runtime_name]

def ensure_dirs(self, path):
if not self.option_create_dirs:
Expand Down
21 changes: 16 additions & 5 deletions partcad/src/partcad/runtime_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,18 @@ def __init__(self, ctx, sandbox, version=None):
# Runtimes are meant to be executed from dedicated threads, outside of
# the asyncio event loop. So a threading lock is appropriate here.
self.lock = threading.RLock()
self.tls = threading.local()

def get_async_lock(self):
locals = threading.local()
alock = f"lock_{id(self)}"
if alock not in locals.__dict__:
locals.__dict__[alock] = asyncio.Lock()
return locals.__dict__[alock]
if not hasattr(self.tls, "async_locks"):
self.tls.async_locks = {}
self_id = id(self)
if self_id not in self.tls.async_locks:
self.tls.async_locks[self_id] = asyncio.Lock()
return self.tls.async_locks[self_id]

async def once(self):
pass

async def run(self, cmd, stdin="", cwd=None):
pc_logging.debug("Running: %s", cmd)
Expand Down Expand Up @@ -73,6 +78,8 @@ async def run(self, cmd, stdin="", cwd=None):
return stdout, stderr

async def ensure(self, python_package):
self.once()

# TODO(clairbee): expire the guard file after a certain time

python_package_hash = hashlib.sha256(
Expand All @@ -91,6 +98,8 @@ async def ensure(self, python_package):
pathlib.Path(guard_path).touch()

async def prepare_for_package(self, project):
self.once()

# TODO(clairbee): expire the guard file after a certain time

# Check if this project has python requirements
Expand Down Expand Up @@ -126,6 +135,8 @@ async def prepare_for_package(self, project):
await self.ensure(req)

async def prepare_for_shape(self, config):
self.once()

# Install dependencies of this part
if "pythonRequirements" in config:
for req in config["pythonRequirements"]:
Expand Down
106 changes: 54 additions & 52 deletions partcad/src/partcad/runtime_python_conda.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,65 +46,67 @@ def __init__(self, ctx, version=None):
path=search_path_strings,
)

async def run(self, cmd, stdin="", cwd=None):
def once(self):
with self.lock:
async with self.get_async_lock():
if not self.initialized:
with pc_logging.Action("Conda", "create", self.version):
if self.conda_path is None:
raise Exception(
"ERROR: PartCAD is configured to use conda, but conda is missing"
)
if not self.initialized:
with pc_logging.Action("Conda", "create", self.version):
if self.conda_path is None:
raise Exception(
"ERROR: PartCAD is configured to use conda, but conda is missing"
)

try:
os.makedirs(self.path)
try:
os.makedirs(self.path)

# Install new conda environment with the preferred Python version
p = subprocess.Popen(
[
self.conda_path,
"create",
"-y",
"-q",
"--json",
"-p",
self.path,
"python=%s" % self.version,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
# Install new conda environment with the preferred Python version
p = subprocess.Popen(
[
self.conda_path,
"create",
"-y",
"-q",
"--json",
"-p",
self.path,
"python=%s" % self.version,
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
_, stderr = p.communicate()
if not stderr is None and stderr != b"":
pc_logging.error(
"conda env install error: %s" % stderr
)
_, stderr = p.communicate()
if not stderr is None and stderr != b"":
pc_logging.error(
"conda env install error: %s" % stderr
)

# Install pip into the newly created conda environment
p = subprocess.Popen(
[
self.conda_path,
"install",
"-y",
"-q",
"--json",
"-p",
self.path,
"pip",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
# Install pip into the newly created conda environment
p = subprocess.Popen(
[
self.conda_path,
"install",
"-y",
"-q",
"--json",
"-p",
self.path,
"pip",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
_, stderr = p.communicate()
if not stderr is None and stderr != b"":
pc_logging.error(
"conda pip install error: %s" % stderr
)
_, stderr = p.communicate()
if not stderr is None and stderr != b"":
pc_logging.error(
"conda pip install error: %s" % stderr
)

self.initialized = True
except Exception as e:
shutil.rmtree(self.path)
raise e
self.initialized = True
except Exception as e:
shutil.rmtree(self.path)
raise e

async def run(self, cmd, stdin="", cwd=None):
self.once()

return await super().run(
[
Expand Down

0 comments on commit 93b104d

Please sign in to comment.