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 5263cc8ea35 Fix airflow standalone announcing readiness before the API
server is up (#73025)
5263cc8ea35 is described below
commit 5263cc8ea35ff6bc83fb94c163c455660733de10
Author: Y-C <[email protected]>
AuthorDate: Sun Sep 13 04:11:34 2026 +0800
Fix airflow standalone announcing readiness before the API server is up
(#73025)
The readiness banner is the signal developers wait on before opening the
browser, so it has to account for the component that serves it. The API
server is the slowest of the four to come up, and until it binds its port
the banner points users at a connection they cannot make.
The port check existed from the command's introduction and was lost as
collateral damage when the old webserver was removed: #46942 deleted the
attribute holding the port but left the call reading it, and the follow-up
hot-fix #47145 resolved the resulting AttributeError by deleting the call
rather than repointing it at the renamed config key. The probe and its
tests have been unreachable since.
Resolving the port once during startup, as the pre-#47145 code did, also
keeps a raising config lookup out of the poll loop, whose only handler is
KeyboardInterrupt and whose component shutdown sits after the try block.
Co-authored-by: Eason09053360
<[email protected]>
---
.../src/airflow/cli/commands/standalone_command.py | 10 ++++--
.../unit/cli/commands/test_standalone_command.py | 39 +++++++++++++++++-----
2 files changed, 37 insertions(+), 12 deletions(-)
diff --git a/airflow-core/src/airflow/cli/commands/standalone_command.py
b/airflow-core/src/airflow/cli/commands/standalone_command.py
index 97b327b426b..943b85d53f5 100644
--- a/airflow-core/src/airflow/cli/commands/standalone_command.py
+++ b/airflow-core/src/airflow/cli/commands/standalone_command.py
@@ -62,6 +62,7 @@ class StandaloneCommand:
self.output_queue = deque()
self.ready_time = None
self.ready_delay = 3
+ self.api_server_port = None
@providers_configuration_loaded
def run(self):
@@ -69,6 +70,7 @@ class StandaloneCommand:
# Silence built-in logging at INFO
logging.getLogger("").setLevel(logging.WARNING)
# Startup checks and prep
+ self.api_server_port = conf.getint("api", "port")
env = self.calculate_env()
self.find_user_info()
self.initialize_database()
@@ -226,10 +228,12 @@ class StandaloneCommand:
"""
Detect when all Airflow components are ready to serve.
- For now, it's simply time-based.
+ Ready means the API server accepts connections and the scheduler, Dag
+ processor and triggerer are all heartbeating.
"""
return (
- self.job_running(SchedulerJobRunner)
+ self.port_open(self.api_server_port)
+ and self.job_running(SchedulerJobRunner)
and self.job_running(DagProcessorJobRunner)
and self.job_running(TriggererJobRunner)
)
@@ -238,7 +242,7 @@ class StandaloneCommand:
"""
Check if the given port is listening on the local machine.
- Used to tell if webserver is alive.
+ Used to tell if the API server is alive.
"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
diff --git a/airflow-core/tests/unit/cli/commands/test_standalone_command.py
b/airflow-core/tests/unit/cli/commands/test_standalone_command.py
index b2e7c2496fa..cd87c16ee49 100644
--- a/airflow-core/tests/unit/cli/commands/test_standalone_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_standalone_command.py
@@ -33,6 +33,8 @@ from airflow.executors.executor_constants import (
LOCAL_EXECUTOR,
)
+from tests_common.test_utils.config import conf_vars
+
class TestStandaloneCommand:
@pytest.mark.parametrize(
@@ -196,18 +198,37 @@ class TestStandaloneCommand:
result =
StandaloneCommand().job_running(mock.Mock(job_type="scheduler"))
assert result is True
- def test_is_ready_true_when_all_components_running(self, monkeypatch):
+ @pytest.mark.parametrize(
+ ("api_server_listening", "jobs_running", "expected"),
+ [
+ pytest.param(True, [True, True, True], True,
id="every-component-ready"),
+ pytest.param(True, [True, False], False,
id="component-not-heartbeating"),
+ pytest.param(False, [], False, id="api-server-port-closed"),
+ ],
+ )
+ def test_is_ready(self, monkeypatch, api_server_listening, jobs_running,
expected):
cmd = StandaloneCommand()
- monkeypatch.setattr(cmd, "job_running", lambda *_: True)
-
- assert cmd.is_ready() is True
-
- def test_is_ready_false_when_any_component_missing(self, monkeypatch):
+ cmd.api_server_port = 9999
+ port_open = mock.Mock(return_value=api_server_listening)
+ jobs = iter(jobs_running)
+ monkeypatch.setattr(cmd, "port_open", port_open)
+ monkeypatch.setattr(cmd, "job_running", lambda *_: next(jobs))
+
+ assert cmd.is_ready() is expected
+ port_open.assert_called_once_with(9999)
+
+ @conf_vars({("api", "port"): "9999"})
+ @mock.patch("airflow.cli.commands.standalone_command.SubCommand",
autospec=True)
+ @mock.patch.object(StandaloneCommand, "initialize_database")
+ @mock.patch.object(StandaloneCommand, "find_user_info")
+ @mock.patch.object(StandaloneCommand, "calculate_env", return_value={})
+ @mock.patch.object(StandaloneCommand, "print_output")
+ def test_run_resolves_the_api_server_port_from_config(self, *_):
cmd = StandaloneCommand()
- calls = iter([True, False, True])
- monkeypatch.setattr(cmd, "job_running", lambda *_: next(calls))
+ with mock.patch.object(cmd, "update_output",
side_effect=KeyboardInterrupt):
+ cmd.run()
- assert cmd.is_ready() is False
+ assert cmd.api_server_port == 9999
@pytest.mark.parametrize("exc", [OSError, ValueError])
def test_port_open_returns_false_on_errors(self, monkeypatch, exc):