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))