2121from contextlib import contextmanager
2222
2323import docker
24+ import filelock
2425import pytest
2526from docker .errors import BuildError
2627
@@ -149,44 +150,45 @@ def gpu_instance_type():
149150
150151
151152@pytest .fixture (scope = "session" )
152- def dummy_container_without_error (sagemaker_session , compatible_python_version ):
153- ecr_uri = _build_container (sagemaker_session , compatible_python_version , DOCKERFILE_TEMPLATE )
153+ def dummy_container_without_error (sagemaker_session , compatible_python_version , sagemaker_sdk_tar_path ):
154+ ecr_uri = _build_container (sagemaker_session , compatible_python_version , DOCKERFILE_TEMPLATE , sagemaker_sdk_tar_path )
154155 return ecr_uri
155156
156157
157158@pytest .fixture (scope = "session" )
158- def dummy_container_with_user_and_workdir (sagemaker_session , compatible_python_version ):
159+ def dummy_container_with_user_and_workdir (sagemaker_session , compatible_python_version , sagemaker_sdk_tar_path ):
159160 ecr_uri = _build_container (
160161 sagemaker_session ,
161162 compatible_python_version ,
162163 DOCKERFILE_TEMPLATE_WITH_USER_AND_WORKDIR ,
164+ sagemaker_sdk_tar_path ,
163165 )
164166 return ecr_uri
165167
166168
167169@pytest .fixture (scope = "session" )
168- def dummy_container_incompatible_python_runtime (sagemaker_session , incompatible_python_version ):
169- ecr_uri = _build_container (sagemaker_session , incompatible_python_version , DOCKERFILE_TEMPLATE )
170+ def dummy_container_incompatible_python_runtime (sagemaker_session , incompatible_python_version , sagemaker_sdk_tar_path ):
171+ ecr_uri = _build_container (sagemaker_session , incompatible_python_version , DOCKERFILE_TEMPLATE , sagemaker_sdk_tar_path )
170172 return ecr_uri
171173
172174
173175@pytest .fixture (scope = "session" )
174- def dummy_container_with_conda (sagemaker_session , compatible_python_version ):
176+ def dummy_container_with_conda (sagemaker_session , compatible_python_version , sagemaker_sdk_tar_path ):
175177 ecr_uri = _build_container (
176- sagemaker_session , compatible_python_version , DOCKERFILE_TEMPLATE_WITH_CONDA
178+ sagemaker_session , compatible_python_version , DOCKERFILE_TEMPLATE_WITH_CONDA , sagemaker_sdk_tar_path
177179 )
178180 return ecr_uri
179181
180182
181183@pytest .fixture (scope = "session" )
182- def auto_capture_test_container (sagemaker_session ):
183- ecr_uri = _build_auto_capture_client_container ("3.10" , AUTO_CAPTURE_CLIENT_DOCKER_TEMPLATE )
184+ def auto_capture_test_container (sagemaker_session , sagemaker_sdk_tar_path ):
185+ ecr_uri = _build_auto_capture_client_container ("3.10" , AUTO_CAPTURE_CLIENT_DOCKER_TEMPLATE , sagemaker_sdk_tar_path )
184186 return ecr_uri
185187
186188
187189@pytest .fixture (scope = "session" )
188- def spark_test_container (sagemaker_session ):
189- ecr_uri = _build_container (sagemaker_session , "3.9" , DOCKERFILE_TEMPLATE )
190+ def spark_test_container (sagemaker_session , sagemaker_sdk_tar_path ):
191+ ecr_uri = _build_container (sagemaker_session , "3.9" , DOCKERFILE_TEMPLATE , sagemaker_sdk_tar_path )
190192 return ecr_uri
191193
192194
@@ -208,6 +210,27 @@ def conda_env_yml():
208210 os .remove (conda_yml_file_name )
209211
210212
213+ @pytest .fixture (scope = "session" )
214+ def sagemaker_sdk_tar_path (tmp_path_factory ):
215+ """Build the sagemaker-core sdist once and share it across all xdist workers.
216+
217+ Uses a file lock so only one worker runs the build; others wait and reuse
218+ the already-built tar.gz from the shared temp directory.
219+ """
220+ # tmp_path_factory.getbasetemp().parent is shared across all xdist workers
221+ root_tmp = tmp_path_factory .getbasetemp ().parent
222+ tar_dir = root_tmp / "sagemaker_sdk_tar"
223+ tar_dir .mkdir (exist_ok = True )
224+ lock_file = root_tmp / "sagemaker_sdk_tar.lock"
225+
226+ with filelock .FileLock (str (lock_file )):
227+ existing = list (tar_dir .glob ("*.tar.gz" ))
228+ if not existing :
229+ _generate_sagemaker_sdk_tar (str (tar_dir ))
230+ existing = list (tar_dir .glob ("*.tar.gz" ))
231+ return str (existing [0 ])
232+
233+
211234def _tmpdir ():
212235 """Create a temporary directory context manager."""
213236 import tempfile
@@ -222,7 +245,7 @@ def _tmpdir():
222245_tmpdir = contextmanager (_tmpdir )
223246
224247
225- def _build_container (sagemaker_session , py_version , docker_template ):
248+ def _build_container (sagemaker_session , py_version , docker_template , sdk_tar_path ):
226249 """Build a dummy test container locally and push to ECR."""
227250 region = sagemaker_session .boto_region_name
228251 image_tag = f"{ py_version .replace ('.' , '-' )} -{ sagemaker_timestamp ()} "
@@ -231,7 +254,8 @@ def _build_container(sagemaker_session, py_version, docker_template):
231254
232255 with _tmpdir () as tmpdir :
233256 print ("building docker image locally in " , tmpdir )
234- source_archive = _generate_sagemaker_sdk_tar (tmpdir )
257+ source_archive = os .path .basename (sdk_tar_path )
258+ shutil .copy2 (sdk_tar_path , os .path .join (tmpdir , source_archive ))
235259 with open (os .path .join (tmpdir , "Dockerfile" ), "w" ) as file :
236260 content = docker_template .format (py_version = py_version , source_archive = source_archive )
237261 print (f"Dockerfile contents: \n { content } \n " )
@@ -267,10 +291,11 @@ def _build_container(sagemaker_session, py_version, docker_template):
267291 return ecr_image
268292
269293
270- def _build_auto_capture_client_container (py_version , docker_template ):
294+ def _build_auto_capture_client_container (py_version , docker_template , sdk_tar_path ):
271295 """Build a test docker container for auto_capture tests."""
272296 with _tmpdir () as tmpdir :
273- source_archive = _generate_sdk_tar_with_public_version (tmpdir )
297+ source_archive = os .path .basename (sdk_tar_path )
298+ shutil .copy2 (sdk_tar_path , os .path .join (tmpdir , source_archive ))
274299 _move_auto_capture_test_file (tmpdir )
275300 with open (os .path .join (tmpdir , "Dockerfile" ), "w" ) as file :
276301 content = docker_template .format (py_version = py_version , source_archive = source_archive )
@@ -304,7 +329,11 @@ def _ecr_image_uri(account, region, image_name, tag):
304329def _generate_sagemaker_sdk_tar (destination_folder ):
305330 """Run build to generate the SDK tar file."""
306331 command = f"python -m build --sdist -o { destination_folder } "
307- result = subprocess .run (command , shell = True , check = True , capture_output = True )
332+ try :
333+ subprocess .run (command , shell = True , check = True , capture_output = True , text = True )
334+ except subprocess .CalledProcessError as e :
335+ print (f"Error when building sagemaker-core sdist: { e .stderr } " )
336+ raise
308337 destination_folder_contents = os .listdir (destination_folder )
309338 source_archive = [f for f in destination_folder_contents if f .endswith ("tar.gz" )][0 ]
310339 return source_archive
0 commit comments