diff --git a/showtime/__init__.py b/showtime/__init__.py index 1747151..12b555a 100644 --- a/showtime/__init__.py +++ b/showtime/__init__.py @@ -4,7 +4,7 @@ Circus tent emoji state tracking for Apache Superset ephemeral environments. """ -__version__ = "0.6.11" +__version__ = "0.6.12" __author__ = "Maxime Beauchemin" __email__ = "maximebeauchemin@gmail.com" diff --git a/showtime/core/aws.py b/showtime/core/aws.py index f12b2d7..24e1177 100644 --- a/showtime/core/aws.py +++ b/showtime/core/aws.py @@ -176,8 +176,9 @@ def create_environment( return EnvironmentResult(success=False, error="Service failed to become stable") # Step 7: Health check the new service (longer timeout for Superset + examples) + # Note: Superset example loading takes more tha 10 mins in some cases print(f"🏥 Health checking service {service_name}...") - if not self._health_check_service(service_name, max_attempts=20): # 10 minutes total + if not self._health_check_service(service_name, max_attempts=30): # 15 minutes total return EnvironmentResult(success=False, error="Service failed health checks") # Step 8: Get IP after health checks pass diff --git a/tests/unit/test_aws_health_check.py b/tests/unit/test_aws_health_check.py new file mode 100644 index 0000000..c719678 --- /dev/null +++ b/tests/unit/test_aws_health_check.py @@ -0,0 +1,59 @@ +""" +Tests for health check retry behavior. +""" + +from unittest.mock import MagicMock, patch + +import httpx +import pytest + +from showtime.core.aws import AWSInterface + + +@pytest.fixture +def aws() -> AWSInterface: + """AWSInterface with mocked clients""" + return AWSInterface( + ecs_client=MagicMock(), + ecr_client=MagicMock(), + ec2_client=MagicMock(), + ) + + +class TestHealthCheckRetries: + """Core retry behavior tests""" + + def test_succeeds_on_healthy_response(self, aws: AWSInterface) -> None: + """Returns True when health endpoint responds 200""" + with patch.object(aws, "get_environment_ip", return_value="1.2.3.4"): + with patch("httpx.Client") as mock_client: + mock_response = MagicMock(status_code=200) + mock_client.return_value.__enter__.return_value.get.return_value = mock_response + + assert aws._health_check_service("test-service", max_attempts=3) is True + + def test_retries_and_succeeds(self, aws: AWSInterface) -> None: + """Retries on failure and succeeds when health check eventually passes""" + with patch.object(aws, "get_environment_ip", return_value="1.2.3.4"): + with patch("httpx.Client") as mock_client: + mock_instance = mock_client.return_value.__enter__.return_value + # Fail twice (health + fallback each), then succeed + mock_instance.get.side_effect = [ + httpx.RequestError("refused"), MagicMock(status_code=503), # attempt 1 + httpx.RequestError("refused"), MagicMock(status_code=503), # attempt 2 + MagicMock(status_code=200), # attempt 3 succeeds + ] + + with patch("time.sleep"): + assert aws._health_check_service("test-service", max_attempts=5) is True + + def test_fails_after_max_attempts_exhausted(self, aws: AWSInterface) -> None: + """Returns False after all attempts fail""" + with patch.object(aws, "get_environment_ip", return_value="1.2.3.4"): + with patch("httpx.Client") as mock_client: + mock_client.return_value.__enter__.return_value.get.side_effect = ( + httpx.RequestError("refused") + ) + + with patch("time.sleep"): + assert aws._health_check_service("test-service", max_attempts=3) is False