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

shahar1 pushed a commit to branch airflow-ctl/v0-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/airflow-ctl/v0-1-test by this 
push:
     new 1e13e1dab8f [airflow-ctl/v0-1-test] Stop airflowctl timing out after 5 
seconds on slow API servers (#76125) (#76138)
1e13e1dab8f is described below

commit 1e13e1dab8f77b8c8ff227eceed46ed7ff2411f2
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Oct 31 15:13:43 2026 +0200

    [airflow-ctl/v0-1-test] Stop airflowctl timing out after 5 seconds on slow 
API servers (#76125) (#76138)
    
    airflowctl built its HTTP client without a timeout, so httpx's 5 second
    default applied to every request. A busy API server can legitimately take
    longer to answer a PATCH or POST, for example the first "dags update" or
    "dags trigger" right after the scheduler picked up a run of the same Dag.
    Write requests are not retried after a timeout, because they may already
    have been applied, so the CLI exited 1 with "Read timeout error". This
    flaked in the Airflow CTL integration tests with the PROD image.
    
    Use a 30 second read, write and pool timeout and keep the connect
    timeout at 5 seconds so an unreachable host still fails fast. Set
    AIRFLOW_CLI_API_TIMEOUT to tune the value in seconds. A timeout passed to
    Client explicitly still wins.
    (cherry picked from commit 1bb64c55b6e30bd7cd931bc03732cb85736492f4)
    
    Co-authored-by: Shahar Epstein <[email protected]>
---
 airflow-ctl/docs/cli-and-env-variables-ref.rst   | 16 +++++++---
 airflow-ctl/src/airflowctl/api/client.py         |  4 +++
 airflow-ctl/tests/airflow_ctl/api/test_client.py | 37 ++++++++++++++++++++++++
 3 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/airflow-ctl/docs/cli-and-env-variables-ref.rst 
b/airflow-ctl/docs/cli-and-env-variables-ref.rst
index ab08b26a96a..2df9424260b 100644
--- a/airflow-ctl/docs/cli-and-env-variables-ref.rst
+++ b/airflow-ctl/docs/cli-and-env-variables-ref.rst
@@ -93,9 +93,17 @@ Environment Variables
     This is only used if you are using the Airflow API and have not set up
     authentication using a different method. The default value is 10 seconds.
 
+.. envvar:: AIRFLOW_CLI_API_TIMEOUT
+
+    How long, in seconds, to wait for the API server to read or write a 
request, or for a free
+    connection, before the request fails. Connecting is limited to 5 seconds. 
Read-only requests
+    that time out are retried (see :envvar:`AIRFLOW_CLI_API_RETRIES`), so the 
total wait can be up
+    to that many times this value; other requests are not retried. The default 
value is 30 seconds.
+
 .. note::
 
-    A value for any of the three retry variables above that is not a whole 
number, or that is
-    out of range (below 1 for :envvar:`AIRFLOW_CLI_API_RETRIES`, negative for 
the two wait
-    variables), is reported on standard error and the default is used instead. 
Setting a
-    variable to an empty string is treated the same as leaving it unset.
+    A value for any of the retry variables above or for 
:envvar:`AIRFLOW_CLI_API_TIMEOUT` that is
+    not a whole number, or that is out of range (below 1 for 
:envvar:`AIRFLOW_CLI_API_RETRIES` and
+    :envvar:`AIRFLOW_CLI_API_TIMEOUT`, negative for the two wait variables), 
is reported on
+    standard error and the default is used instead. Setting a variable to an 
empty string is
+    treated the same as leaving it unset.
diff --git a/airflow-ctl/src/airflowctl/api/client.py 
b/airflow-ctl/src/airflowctl/api/client.py
index b15881b7785..99a506b9cb2 100644
--- a/airflow-ctl/src/airflowctl/api/client.py
+++ b/airflow-ctl/src/airflowctl/api/client.py
@@ -375,6 +375,7 @@ def _get_int_env(name: str, default: int, minimum: int = 0) 
-> int:
 API_RETRIES = _get_int_env("AIRFLOW_CLI_API_RETRIES", 3, minimum=1)
 API_RETRY_WAIT_MIN = _get_int_env("AIRFLOW_CLI_API_RETRY_WAIT_MIN", 1)
 API_RETRY_WAIT_MAX = _get_int_env("AIRFLOW_CLI_API_RETRY_WAIT_MAX", 10)
+API_TIMEOUT = _get_int_env("AIRFLOW_CLI_API_TIMEOUT", 30, minimum=1)
 
 
 class Client(httpx.Client):
@@ -392,6 +393,9 @@ class Client(httpx.Client):
         if kind != ClientKind.NO_AUTH:
             auth = BearerAuth(token)
         kwargs["base_url"] = self._get_base_url(base_url=base_url, kind=kind)
+        # Connect stays short so an unreachable host fails fast; the rest is 
generous because
+        # write requests are not retried after a timeout.
+        kwargs.setdefault("timeout", httpx.Timeout(API_TIMEOUT, connect=5.0))
         pyver = f"{'.'.join(map(str, sys.version_info[:3]))}"
         super().__init__(
             auth=auth,
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py 
b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index f602535b00c..511428c2135 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -767,6 +767,7 @@ class TestRetryConfigurationEnvVars:
             "AIRFLOW_CLI_API_RETRIES",
             "AIRFLOW_CLI_API_RETRY_WAIT_MIN",
             "AIRFLOW_CLI_API_RETRY_WAIT_MAX",
+            "AIRFLOW_CLI_API_TIMEOUT",
         ):
             monkeypatch.delenv(name, raising=False)
 
@@ -798,6 +799,29 @@ class TestRetryConfigurationEnvVars:
         assert result.stdout.strip() == expected
         assert ("AIRFLOW_CLI_API_RETRIES" in result.stderr) is warns
 
+    @pytest.mark.parametrize(
+        ("value", "expected", "warns"),
+        [
+            pytest.param("120", "120", False, id="integer-is-used"),
+            pytest.param("", "30", False, id="empty-means-unset"),
+            pytest.param("0", "30", True, id="zero-falls-back"),
+            pytest.param("-5", "30", True, id="negative-falls-back"),
+            pytest.param("abc", "30", True, id="typo-falls-back"),
+        ],
+    )
+    def test_timeout_env_var(self, value, expected, warns):
+        result = subprocess.run(
+            [sys.executable, "-c", "import airflowctl.api.client as c; 
print(c.API_TIMEOUT)"],
+            env={**os.environ, "AIRFLOW_CLI_API_TIMEOUT": value},
+            check=False,
+            text=True,
+            capture_output=True,
+        )
+
+        assert result.returncode == 0, result.stderr
+        assert result.stdout.strip() == expected
+        assert ("AIRFLOW_CLI_API_TIMEOUT" in result.stderr) is warns
+
     def test_zero_wait_is_allowed(self):
         """Only the retry count needs a floor of 1; waiting 0 seconds between 
retries is valid."""
         result = subprocess.run(
@@ -813,6 +837,19 @@ class TestRetryConfigurationEnvVars:
         assert result.stderr == ""
 
 
+class TestClientTimeout:
+    @pytest.mark.parametrize(
+        ("kwargs", "expected"),
+        [
+            pytest.param({}, httpx.Timeout(45, connect=5.0), id="default"),
+            pytest.param({"timeout": 2}, httpx.Timeout(2), id="explicit"),
+        ],
+    )
+    @patch("airflowctl.api.client.API_TIMEOUT", new=45)
+    def test_timeout(self, kwargs, expected):
+        assert Client(base_url="http://localhost:8080";, **kwargs).timeout == 
expected
+
+
 class TestGetClientEnvironment:
     """``get_client`` and ``@provide_api_client`` commands resolve the config 
file for the requested environment."""
 

Reply via email to