This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new ed90dbb161a Stop Breeze from hanging on unresponsive Docker (#72439)
ed90dbb161a is described below

commit ed90dbb161ab45716540470d60dd49a50cd72e26
Author: Andrew Chang <[email protected]>
AuthorDate: Tue Sep 8 02:42:26 2026 +0800

    Stop Breeze from hanging on unresponsive Docker (#72439)
    
    Docker CLI can wait indefinitely when the daemon or configured endpoint 
does not respond, leaving Breeze commands without useful feedback.
---
 .../airflow_breeze/utils/docker_command_utils.py   | 43 ++++++++++------
 dev/breeze/tests/test_docker_command_utils.py      | 58 ++++++++++++++++++++++
 dev/breeze/tests/test_run_utils.py                 | 12 +++++
 3 files changed, 98 insertions(+), 15 deletions(-)

diff --git a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py 
b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
index f3644aa1f1b..957ca69d8b0 100644
--- a/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
+++ b/dev/breeze/src/airflow_breeze/utils/docker_command_utils.py
@@ -120,6 +120,8 @@ VOLUMES_FOR_SELECTED_MOUNTS = [
     ("ts-sdk", "/opt/airflow/ts-sdk"),
 ]
 
+DOCKER_INFO_TIMEOUT = 30
+
 
 def check_docker_resources(airflow_image_name: str) -> RunCommandResult:
     """
@@ -145,6 +147,30 @@ def check_docker_resources(airflow_image_name: str) -> 
RunCommandResult:
     )
 
 
+def _run_docker_info_or_exit(command: list[str]) -> RunCommandResult:
+    try:
+        return run_command(
+            command,
+            no_output_dump_on_exception=True,
+            capture_output=True,
+            text=True,
+            check=False,
+            timeout=DOCKER_INFO_TIMEOUT,
+        )
+    except subprocess.TimeoutExpired:
+        console_print(
+            f"[error]Docker did not respond within {DOCKER_INFO_TIMEOUT} 
seconds.[/]\n"
+            "[warning]Please make sure Docker is running and responsive.[/]"
+        )
+        sys.exit(1)
+    except FileNotFoundError:
+        console_print(
+            "[error]Docker executable was not found.[/]\n"
+            "[warning]Please install Docker and ensure `docker` is available 
on PATH.[/]"
+        )
+        sys.exit(1)
+
+
 def check_docker_permission_denied() -> bool:
     """
     Checks if we have permission to write to docker socket. By default, on 
Linux you need to add your user
@@ -155,14 +181,7 @@ def check_docker_permission_denied() -> bool:
     :return: True if permission is denied
     """
     permission_denied = False
-    docker_permission_command = ["docker", "info"]
-    command_result = run_command(
-        docker_permission_command,
-        no_output_dump_on_exception=True,
-        capture_output=True,
-        text=True,
-        check=False,
-    )
+    command_result = _run_docker_info_or_exit(["docker", "info"])
     if command_result.returncode != 0:
         permission_denied = True
         if command_result.stdout and "Got permission denied while trying to 
connect" in command_result.stdout:
@@ -183,13 +202,7 @@ def check_docker_is_running():
     Checks if docker is running. Suppressed Dockers stdout and stderr output.
 
     """
-    response = run_command(
-        ["docker", "info"],
-        no_output_dump_on_exception=True,
-        text=True,
-        capture_output=True,
-        check=False,
-    )
+    response = _run_docker_info_or_exit(["docker", "info"])
     if response.returncode != 0:
         console_print(
             "[error]Docker is not running.[/]\n[warning]Please make sure 
Docker is installed and running.[/]"
diff --git a/dev/breeze/tests/test_docker_command_utils.py 
b/dev/breeze/tests/test_docker_command_utils.py
index d433ea7bb14..555cf897b3c 100644
--- a/dev/breeze/tests/test_docker_command_utils.py
+++ b/dev/breeze/tests/test_docker_command_utils.py
@@ -17,6 +17,7 @@
 from __future__ import annotations
 
 import json
+import subprocess
 from unittest import mock
 from unittest.mock import call
 
@@ -33,6 +34,8 @@ from airflow_breeze.utils.docker_command_utils import (
     autodetect_docker_context,
     bring_all_compose_projects_down,
     check_docker_compose_version,
+    check_docker_is_running,
+    check_docker_permission_denied,
     check_docker_version,
     discover_running_compose_projects,
     enter_shell,
@@ -43,6 +46,61 @@ from airflow_breeze.utils.docker_command_utils import (
 )
 
 
[email protected](
+    ("exception", "expected_message"),
+    [
+        pytest.param(
+            subprocess.TimeoutExpired(["docker", "info"], 30),
+            "[error]Docker did not respond within 30 seconds.[/]\n"
+            "[warning]Please make sure Docker is running and responsive.[/]",
+            id="timeout",
+        ),
+        pytest.param(
+            FileNotFoundError(2, "No such file or directory", "docker"),
+            "[error]Docker executable was not found.[/]\n"
+            "[warning]Please install Docker and ensure `docker` is available 
on PATH.[/]",
+            id="missing-executable",
+        ),
+    ],
+)
[email protected]("airflow_breeze.utils.docker_command_utils.console_print")
[email protected]("airflow_breeze.utils.docker_command_utils.run_command")
+def test_check_docker_is_running_reports_unavailable_docker(
+    mock_run_command, mock_console_print, exception, expected_message
+):
+    mock_run_command.side_effect = exception
+
+    with pytest.raises(SystemExit) as error:
+        check_docker_is_running()
+
+    assert error.value.code == 1
+    mock_run_command.assert_called_once_with(
+        ["docker", "info"],
+        no_output_dump_on_exception=True,
+        text=True,
+        capture_output=True,
+        check=False,
+        timeout=30,
+    )
+    mock_console_print.assert_called_once_with(expected_message)
+
+
[email protected]("airflow_breeze.utils.docker_command_utils.run_command")
+def 
test_check_docker_permission_denied_uses_bounded_info_probe(mock_run_command):
+    mock_run_command.return_value.returncode = 0
+
+    assert check_docker_permission_denied() is False
+
+    mock_run_command.assert_called_once_with(
+        ["docker", "info"],
+        no_output_dump_on_exception=True,
+        capture_output=True,
+        text=True,
+        check=False,
+        timeout=30,
+    )
+
+
 
@mock.patch("airflow_breeze.utils.docker_command_utils.check_docker_permission_denied")
 @mock.patch("airflow_breeze.utils.docker_command_utils.run_command")
 @mock.patch("airflow_breeze.utils.docker_command_utils.console_print")
diff --git a/dev/breeze/tests/test_run_utils.py 
b/dev/breeze/tests/test_run_utils.py
index 541eb55f987..59ac4dd5292 100644
--- a/dev/breeze/tests/test_run_utils.py
+++ b/dev/breeze/tests/test_run_utils.py
@@ -18,6 +18,7 @@ from __future__ import annotations
 
 import socket
 import stat
+import subprocess
 from unittest import mock
 
 import pytest
@@ -63,6 +64,17 @@ def 
test_run_command_dry_run_quiet_does_not_execute(mock_subprocess_run):
     assert result.stderr == ""
 
 
[email protected]("verbose", [False, True])
[email protected]("airflow_breeze.utils.run_utils.subprocess.run")
+def test_run_command_forwards_timeout(mock_subprocess_run, verbose):
+    mock_subprocess_run.side_effect = subprocess.TimeoutExpired(["echo", 
"hello"], 30)
+
+    with pytest.raises(subprocess.TimeoutExpired):
+        run_command(["echo", "hello"], timeout=30, verbose_override=verbose)
+
+    assert mock_subprocess_run.call_args.kwargs["timeout"] == 30
+
+
 def test_find_occupied_local_ports():
     with socket.socket() as unused_socket:
         unused_socket.bind(("127.0.0.1", 0))

Reply via email to