Skip to content

Fix airflow standalone announcing readiness before the API server is up - #73025

Merged
potiuk merged 1 commit into
apache:mainfrom
Eason09053360:fix-standalone-ready-banner-before-api-server
Sep 12, 2026
Merged

potiuk merged 1 commit into
apache:mainfrom
Eason09053360:fix-standalone-ready-banner-before-api-server

Conversation

@Eason09053360

Copy link
Copy Markdown
Contributor

Why

airflow standalone prints "Airflow is ready" without checking the component that serves the UI. is_ready() only looks at the scheduler, Dag processor and triggerer heartbeats, so the banner fires while the API server is still importing the app and binding its port. Developers follow the banner to the browser and get a connection refused.

The check is not new. is_ready() gated on the port from the command's introduction in #15826, and #20505 later fixed it to read the configured port instead of a hard-coded 8080. #46942 ("Remove old UI and webserver") then deleted the attribute holding the port but left the call that read it, so is_ready() raised AttributeError; the follow-up hot-fix #47145 resolved that by deleting the call rather than repointing it at the renamed [api] port. port_open and its two tests have been unreachable since.

What

airflow-core/src/airflow/cli/commands/standalone_command.py:

  • is_ready() gates on port_open again, first in the chain so the three heartbeat queries are skipped while the API server is still coming up.
  • run() resolves [api] port once at startup, matching the shape the pre-Fix standalone command by removing the usage of webserver_port property #47145 code used. Keeping the lookup out of the poll loop matters: conf.getint raises on a malformed value, the loop only handles KeyboardInterrupt, and the command.stop() / command.join() shutdown sits after the try rather than in a finally, so an escape would leave all four components running as orphans. With the lookup in run(), a bad [api] port fails before anything is spawned.
  • Two stale docstrings corrected: is_ready() claimed to be time-based, which it has not been since Add Airflow Standalone command #15826, and port_open still referred to the webserver.

airflow-core/tests/unit/cli/commands/test_standalone_command.py: the is_ready tests are consolidated into one parametrized test covering port-open, a component not heartbeating, and port-closed. It asserts port_open is called with the resolved port, and the port-closed case passes an empty iterator so the short-circuit is pinned. A new test covers the config resolution in run(). Reverting the source change fails all four.

Deliberately not in this PR

port_open is restored as it was, so its pre-existing limitations come back with it and are worth a maintainer's call:

  • It probes 127.0.0.1 over IPv4 and ignores [api] host. The default 0.0.0.0 is fine, but a user who binds a specific non-loopback address would never see the banner.
  • A TCP accept proves something is listening, not that it is this standalone's API server. A stale process holding the port still yields a ready banner.
  • With [api] workers > 1 or server_type = gunicorn the parent binds before workers import the app, so the accept precedes app readiness.

All three point at the same deeper fix: polling GET /api/v2/monitor/health, which is already the idiom in the Helm chart, breeze, and dev/verify_*_rc.sh, and which would subsume the heartbeat checks too. That is a larger change than restoring a lost gate, so I have kept it out.

Separately, ready_time latches and is never re-evaluated, so the banner still makes a 3-second-stale claim, and standalone never notices a component dying. Both are pre-existing and unchanged here.

.github/workflows/basic-tests.yml greps this banner, so that job now genuinely depends on the API server binding.

How to test

Run airflow standalone and confirm the banner appears only once http://localhost:8080 actually answers.


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 5)

Generated-by: Claude Code (Opus 5) following the guidelines

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: apache#46942 deleted the
attribute holding the port but left the call reading it, and the follow-up
hot-fix apache#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-apache#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.
@potiuk
potiuk merged commit 5263cc8 into apache:main Sep 12, 2026
73 checks passed
xvega pushed a commit to xvega/airflow that referenced this pull request Sep 13, 2026
…up (apache#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: apache#46942 deleted the
attribute holding the port but left the call reading it, and the follow-up
hot-fix apache#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-apache#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 <185830721+Eason09053360@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants