Skip to content

Commit a8024cb

Browse files
committed
singularity: cache imageID lookups and add thread safety
1 parent f166143 commit a8024cb

File tree

1 file changed

+59
-42
lines changed

1 file changed

+59
-42
lines changed

cwltool/singularity.py

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Support for executing Docker format containers using Singularity {2,3}.x or Apptainer 1.x."""
22

3+
import copy
34
import logging
45
import os
56
import os.path
67
import re
78
import shutil
89
import sys
10+
import threading
911
from collections.abc import Callable, MutableMapping
1012
from subprocess import check_call, check_output # nosec
1113
from typing import cast
@@ -35,6 +37,10 @@
3537
_SINGULARITY_FLAVOR: str = ""
3638

3739

40+
_IMAGES: dict[str, str] = {}
41+
_IMAGES_LOCK = threading.Lock()
42+
43+
3844
def get_version() -> tuple[list[int], str]:
3945
"""
4046
Parse the output of 'singularity --version' to determine the flavor and version.
@@ -180,18 +186,26 @@ def get_image(
180186
cache_folder = None
181187
debug = _logger.isEnabledFor(logging.DEBUG)
182188

189+
with _IMAGES_LOCK:
190+
if "dockerImageId" in dockerRequirement:
191+
if (d_image_id := dockerRequirement["dockerImageId"]) in _IMAGES:
192+
if (resolved_image_id := _IMAGES[d_image_id]) != d_image_id:
193+
dockerRequirement["dockerImage_id"] = resolved_image_id
194+
return True
195+
196+
docker_req = copy.deepcopy(dockerRequirement) # thread safety
183197
if "CWL_SINGULARITY_CACHE" in os.environ:
184198
cache_folder = os.environ["CWL_SINGULARITY_CACHE"]
185199
elif is_version_2_6() and "SINGULARITY_PULLFOLDER" in os.environ:
186200
cache_folder = os.environ["SINGULARITY_PULLFOLDER"]
187201

188-
if "dockerFile" in dockerRequirement:
202+
if "dockerFile" in docker_req:
189203
if cache_folder is None: # if environment variables were not set
190204
cache_folder = create_tmp_dir(tmp_outdir_prefix)
191205

192206
absolute_path = os.path.abspath(cache_folder)
193-
if "dockerImageId" in dockerRequirement:
194-
image_name = dockerRequirement["dockerImageId"]
207+
if "dockerImageId" in docker_req:
208+
image_name = docker_req["dockerImageId"]
195209
image_path = os.path.join(absolute_path, image_name)
196210
if os.path.exists(image_path):
197211
found = True
@@ -204,19 +218,19 @@ def get_image(
204218
# FATAL: Unable to create build: 'noexec' mount option set on
205219
# /tmp, temporary root filesystem won't be usable at this location
206220
with open(dockerfile_path, "w") as dfile:
207-
dfile.write(dockerRequirement["dockerFile"])
221+
dfile.write(docker_req["dockerFile"])
208222

209223
singularityfile = SingularityWriter(DockerParser(dockerfile_path).parse()).convert()
210224
with open(singularityfile_path, "w") as file:
211225
file.write(singularityfile)
212226

213227
os.environ["APPTAINER_TMPDIR"] = absolute_path
214228
singularity_options = ["--fakeroot"] if not shutil.which("proot") else []
215-
if "dockerImageId" in dockerRequirement:
229+
if "dockerImageId" in docker_req:
216230
Client.build(
217231
recipe=singularityfile_path,
218232
build_folder=absolute_path,
219-
image=dockerRequirement["dockerImageId"],
233+
image=docker_req["dockerImageId"],
220234
sudo=False,
221235
options=singularity_options,
222236
)
@@ -228,25 +242,25 @@ def get_image(
228242
options=singularity_options,
229243
)
230244
found = True
231-
elif "dockerImageId" not in dockerRequirement and "dockerPull" in dockerRequirement:
232-
match = re.search(pattern=r"([a-z]*://)", string=dockerRequirement["dockerPull"])
233-
img_name = _normalize_image_id(dockerRequirement["dockerPull"])
245+
elif "dockerImageId" not in docker_req and "dockerPull" in docker_req:
246+
match = re.search(pattern=r"([a-z]*://)", string=docker_req["dockerPull"])
247+
img_name = _normalize_image_id(docker_req["dockerPull"])
234248
candidates.append(img_name)
235249
if is_version_3_or_newer():
236-
sif_name = _normalize_sif_id(dockerRequirement["dockerPull"])
250+
sif_name = _normalize_sif_id(docker_req["dockerPull"])
237251
candidates.append(sif_name)
238-
dockerRequirement["dockerImageId"] = sif_name
252+
docker_req["dockerImageId"] = sif_name
239253
else:
240-
dockerRequirement["dockerImageId"] = img_name
254+
docker_req["dockerImageId"] = img_name
241255
if not match:
242-
dockerRequirement["dockerPull"] = "docker://" + dockerRequirement["dockerPull"]
243-
elif "dockerImageId" in dockerRequirement:
244-
if os.path.isfile(dockerRequirement["dockerImageId"]):
256+
docker_req["dockerPull"] = "docker://" + docker_req["dockerPull"]
257+
elif "dockerImageId" in docker_req:
258+
if os.path.isfile(docker_req["dockerImageId"]):
245259
found = True
246-
candidates.append(dockerRequirement["dockerImageId"])
247-
candidates.append(_normalize_image_id(dockerRequirement["dockerImageId"]))
260+
candidates.append(docker_req["dockerImageId"])
261+
candidates.append(_normalize_image_id(docker_req["dockerImageId"]))
248262
if is_version_3_or_newer():
249-
candidates.append(_normalize_sif_id(dockerRequirement["dockerImageId"]))
263+
candidates.append(_normalize_sif_id(docker_req["dockerImageId"]))
250264

251265
targets = [os.getcwd()]
252266
if "CWL_SINGULARITY_CACHE" in os.environ:
@@ -263,11 +277,11 @@ def get_image(
263277
"Using local copy of Singularity image found in %s",
264278
dirpath,
265279
)
266-
dockerRequirement["dockerImageId"] = path
280+
docker_req["dockerImageId"] = path
267281
found = True
268282
if (force_pull or not found) and pull_image:
269283
cmd: list[str] = []
270-
if "dockerPull" in dockerRequirement:
284+
if "dockerPull" in docker_req:
271285
if cache_folder:
272286
env = os.environ.copy()
273287
if is_version_2_6():
@@ -277,23 +291,23 @@ def get_image(
277291
"pull",
278292
"--force",
279293
"--name",
280-
dockerRequirement["dockerImageId"],
281-
str(dockerRequirement["dockerPull"]),
294+
docker_req["dockerImageId"],
295+
str(docker_req["dockerPull"]),
282296
]
283297
else:
284298
cmd = [
285299
"singularity",
286300
"pull",
287301
"--force",
288302
"--name",
289-
"{}/{}".format(cache_folder, dockerRequirement["dockerImageId"]),
290-
str(dockerRequirement["dockerPull"]),
303+
"{}/{}".format(cache_folder, docker_req["dockerImageId"]),
304+
str(docker_req["dockerPull"]),
291305
]
292306

293307
_logger.info(str(cmd))
294308
check_call(cmd, env=env, stdout=sys.stderr) # nosec
295-
dockerRequirement["dockerImageId"] = "{}/{}".format(
296-
cache_folder, dockerRequirement["dockerImageId"]
309+
docker_req["dockerImageId"] = "{}/{}".format(
310+
cache_folder, docker_req["dockerImageId"]
297311
)
298312
found = True
299313
else:
@@ -302,44 +316,47 @@ def get_image(
302316
"pull",
303317
"--force",
304318
"--name",
305-
str(dockerRequirement["dockerImageId"]),
306-
str(dockerRequirement["dockerPull"]),
319+
str(docker_req["dockerImageId"]),
320+
str(docker_req["dockerPull"]),
307321
]
308322
_logger.info(str(cmd))
309323
check_call(cmd, stdout=sys.stderr) # nosec
310324
found = True
311325

312-
elif "dockerLoad" in dockerRequirement:
326+
elif "dockerLoad" in docker_req:
313327
if is_version_3_1_or_newer():
314-
if "dockerImageId" in dockerRequirement:
315-
name = "{}.sif".format(dockerRequirement["dockerImageId"])
328+
if "dockerImageId" in docker_req:
329+
name = "{}.sif".format(docker_req["dockerImageId"])
316330
else:
317-
name = "{}.sif".format(dockerRequirement["dockerLoad"])
331+
name = "{}.sif".format(docker_req["dockerLoad"])
318332
cmd = [
319333
"singularity",
320334
"build",
321335
name,
322-
"docker-archive://{}".format(dockerRequirement["dockerLoad"]),
336+
"docker-archive://{}".format(docker_req["dockerLoad"]),
323337
]
324338
_logger.info(str(cmd))
325339
check_call(cmd, stdout=sys.stderr) # nosec
326340
found = True
327-
dockerRequirement["dockerImageId"] = name
341+
docker_req["dockerImageId"] = name
328342
else:
329-
raise SourceLine(
330-
dockerRequirement, "dockerLoad", WorkflowException, debug
331-
).makeError(
343+
raise SourceLine(docker_req, "dockerLoad", WorkflowException, debug).makeError(
332344
"dockerLoad is not currently supported when using the "
333345
"Singularity runtime (version less than 3.1) for Docker containers."
334346
)
335-
elif "dockerImport" in dockerRequirement:
336-
raise SourceLine(
337-
dockerRequirement, "dockerImport", WorkflowException, debug
338-
).makeError(
347+
elif "dockerImport" in docker_req:
348+
raise SourceLine(docker_req, "dockerImport", WorkflowException, debug).makeError(
339349
"dockerImport is not currently supported when using the "
340350
"Singularity runtime for Docker containers."
341351
)
342-
352+
if found:
353+
with _IMAGES_LOCK:
354+
if "dockerImageId" in dockerRequirement:
355+
_IMAGES[dockerRequirement["dockerImageId"]] = docker_req["dockerImageId"]
356+
dockerRequirement.clear()
357+
dockerRequirement |= docker_req
358+
if "dockerImageId" in docker_req:
359+
_IMAGES[docker_req["dockerImageId"]] = docker_req["dockerImageId"]
343360
return found
344361

345362
def get_from_requirements(

0 commit comments

Comments
 (0)