Skip to content

Commit e98739b

Browse files
egorkosaretskyclaudemohamedzeidan2021
authored
fix: make local mode health-check timeout configurable via session config (#3362) (#5778)
Allow users to set `local.health_check_timeout` in their session config to override the hard-coded 120s limit, fixing failures when large model archives or slow networks cause container startup to exceed the default. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Mohamed Zeidan <81834882+mohamedzeidan2021@users.noreply.github.com>
1 parent c9886ea commit e98739b

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

‎sagemaker-core/src/sagemaker/core/local/entities.py‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,11 @@ def start(self, input_data, output_data, transform_resources, **kwargs):
338338
self.container.serve(self.primary_container["ModelDataUrl"], environment)
339339

340340
serving_port = get_config_value("local.serving_port", self.local_session.config) or 8080
341-
_wait_for_serving_container(serving_port)
341+
health_check_timeout = (
342+
get_config_value("local.health_check_timeout", self.local_session.config)
343+
or HEALTH_CHECK_TIMEOUT_LIMIT
344+
)
345+
_wait_for_serving_container(serving_port, timeout=health_check_timeout)
342346

343347
# Get capabilities from Container if needed
344348
endpoint_url = "http://%s:%d/execution-parameters" % (get_docker_host(), serving_port)
@@ -632,7 +636,11 @@ def serve(self):
632636
)
633637

634638
serving_port = get_config_value("local.serving_port", self.local_session.config) or 8080
635-
_wait_for_serving_container(serving_port)
639+
health_check_timeout = (
640+
get_config_value("local.health_check_timeout", self.local_session.config)
641+
or HEALTH_CHECK_TIMEOUT_LIMIT
642+
)
643+
_wait_for_serving_container(serving_port, timeout=health_check_timeout)
636644
# the container is running and it passed the healthcheck status is now InService
637645
self.state = _LocalEndpoint._IN_SERVICE
638646

@@ -655,15 +663,21 @@ def describe(self):
655663
return response
656664

657665

658-
def _wait_for_serving_container(serving_port):
659-
"""Placeholder docstring."""
666+
def _wait_for_serving_container(serving_port, timeout=HEALTH_CHECK_TIMEOUT_LIMIT):
667+
"""Wait for the serving container to become healthy.
668+
669+
Args:
670+
serving_port (int): The port the serving container is listening on.
671+
timeout (int): Maximum number of seconds to wait for the container to become healthy.
672+
Defaults to ``HEALTH_CHECK_TIMEOUT_LIMIT``.
673+
"""
660674
i = 0
661675
http = urllib3.PoolManager()
662676

663677
endpoint_url = "http://%s:%d/ping" % (get_docker_host(), serving_port)
664678
while True:
665679
i += 5
666-
if i >= HEALTH_CHECK_TIMEOUT_LIMIT:
680+
if i >= timeout:
667681
raise RuntimeError("Giving up, endpoint didn't launch correctly")
668682

669683
logger.info("Checking if serving container is up, attempt: %s", i)

‎sagemaker-core/tests/unit/local/test_entities.py‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,47 @@ def test_endpoint_serve(self, mock_container_class, mock_wait, mock_session_clas
610610
assert endpoint.state == endpoint._IN_SERVICE
611611
mock_container.serve.assert_called_once()
612612

613+
@patch("sagemaker.core.local.local_session.LocalSession")
614+
@patch("sagemaker.core.local.entities._wait_for_serving_container")
615+
@patch("sagemaker.core.local.entities._SageMakerContainer")
616+
def test_endpoint_serve_custom_health_check_timeout(
617+
self, mock_container_class, mock_wait, mock_session_class
618+
):
619+
"""Test that health_check_timeout from session config is passed to _wait_for_serving_container"""
620+
mock_session = Mock()
621+
mock_client = Mock()
622+
623+
mock_client.describe_endpoint_config.return_value = {
624+
"EndpointConfigName": "test-config",
625+
"ProductionVariants": [
626+
{
627+
"VariantName": "AllTraffic",
628+
"ModelName": "test-model",
629+
"InitialInstanceCount": 1,
630+
"InstanceType": "local",
631+
}
632+
],
633+
}
634+
635+
mock_client.describe_model.return_value = {
636+
"PrimaryContainer": {
637+
"Image": "test-image:latest",
638+
"ModelDataUrl": "s3://bucket/model.tar.gz",
639+
"Environment": {},
640+
}
641+
}
642+
643+
mock_session.sagemaker_client = mock_client
644+
mock_session.config = {"local": {"health_check_timeout": 600}}
645+
646+
mock_container = Mock()
647+
mock_container_class.return_value = mock_container
648+
649+
endpoint = _LocalEndpoint("test-endpoint", "test-config", None, mock_session)
650+
endpoint.serve()
651+
652+
mock_wait.assert_called_once_with(8080, timeout=600)
653+
613654
@patch("sagemaker.core.local.local_session.LocalSession")
614655
def test_endpoint_stop(self, mock_session_class):
615656
"""Test stopping an endpoint"""
@@ -704,6 +745,33 @@ def test_wait_timeout(self, mock_sleep, mock_get_host, mock_perform_request):
704745
with pytest.raises(RuntimeError, match="Giving up"):
705746
_wait_for_serving_container(8080)
706747

748+
@patch("sagemaker.core.local.entities._perform_request")
749+
@patch("sagemaker.core.local.entities.get_docker_host")
750+
@patch("time.sleep")
751+
def test_wait_custom_timeout(self, mock_sleep, mock_get_host, mock_perform_request):
752+
"""Test that custom timeout is respected"""
753+
mock_get_host.return_value = "localhost"
754+
mock_perform_request.return_value = (None, 500)
755+
756+
with pytest.raises(RuntimeError, match="Giving up"):
757+
_wait_for_serving_container(8080, timeout=10)
758+
759+
# With timeout=10 and step=5, only 2 iterations before i(=10) >= timeout(=10)
760+
assert mock_perform_request.call_count == 1
761+
762+
@patch("sagemaker.core.local.entities._perform_request")
763+
@patch("sagemaker.core.local.entities.get_docker_host")
764+
@patch("time.sleep")
765+
def test_wait_uses_default_timeout_constant(self, mock_sleep, mock_get_host, mock_perform_request):
766+
"""Test that _wait_for_serving_container defaults to HEALTH_CHECK_TIMEOUT_LIMIT"""
767+
mock_get_host.return_value = "localhost"
768+
# Succeed on first attempt so we can verify it was called
769+
mock_perform_request.return_value = (Mock(), 200)
770+
771+
_wait_for_serving_container(8080)
772+
773+
mock_perform_request.assert_called_once()
774+
707775

708776
class TestPerformRequest:
709777
"""Test cases for _perform_request"""

0 commit comments

Comments
 (0)