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."""