Description
TL;DR:
The core concept is to use a pre-commit hook to precompute and serialize the results of get_cli_commands
into _generated_commands.py
.
After conducting some benchmarking ( check last part of issue ) and a proof of concept (POC), I found that the Airflow CLI can be sped up by 3 to 4 times for non-custom AuthManagers and Executors.
Why
While exploring airflow/cli/cli_parser.py
, I noticed that get_auth_manager_cls
is imported from airflow.api_fastapi.app
. Since FastAPI is unrelated to the CLI, I investigated further.
The primary reasons why the current CLI is slow:
airflow/airflow/cli/cli_parser.py
Lines 39 to 87 in 4c031cb
- It loads the entire executor module.
- It loads the entire auth manager module.
Although these modules are dynamically loaded using
import_string
, there is still significant overhead when loadingCeleryKubernetesExecutor
, as it requires thecelery
andkubernetes
packages.
How
Instead of fully decoupling the CLI interface from AuthManagers and Executors—by separating get_cli_commands
from Base<AuthManager/Executor>
interfaces, which could introduce breaking changes and require significant migration effort.
By adding a pre-commit hook to collect all CLI commands and store them precomputed and statically in airflow.cli.commands._generated_commands.py
which can significantly speed up CLI response times.
Note:
For custom AuthManagers or Executors, the original approach will still be used, meaning get_cli_commands
will be dynamically loaded and called as before.
Steps
Step 3 is the key focus.
- Decouple
auth_manager
from heavy dependencies likeFastAPI
andFlask
by only importing them within methods.Since the
auth_manager
implementation itself does not importFastAPI
directly (only uses it for type hints), this ensures lighter module loading. - Introduce
auth_manager_constants.py
as a single source of truth for available AuthManagers, similar to howexecutor_constants.py
works for Executors. - Add a pre-commit hook to generate
EXECUTORS_CLI_COMMANDS
andAUTH_MANAGERS_CLI_COMMANDS
by precomputing CLI commands for all supported Executors and AuthManagers.
What
This change is expected to speed up the CLI by 3–4× for non-custom AuthManagers and Executors.
It could be a significant performance improvement in either Airflow 3.0 or a 2.10.x release!
Details of the Pre-Commit Script
Example of _generated_commands.py
EXECUTORS_CLI_COMMANDS = {
"KubernetesExecutor": [
GroupCommand(
name="kubernetes",
help="Tools to help run the KubernetesExecutor",
subcommands=[
ActionCommand(
name="cleanup-pods",
help="Clean up Kubernetes pods (created by KubernetesExecutor/KubernetesPodOperator) in evicted/failed/succeeded/pending states",
func=lazy_load_command(
"airflow.providers.cncf.kubernetes.cli.kubernetes_command.cleanup_pods"
),
args=[
Arg(
flags=("--namespace",),
help="Kubernetes Namespace. Default value is `[kubernetes] namespace` in configuration.",
default=conf.get("kubernetes_executor", "namespace"),
),
Arg(
flags=("--min-pending-minutes",),
help="Pending pods created before the time interval are to be cleaned up, measured in minutes. Default value is 30(m). The minimum value is 5(m).",
default=30,
type=positive_int(allow_zero=False),
),
Arg(
flags=("-v", "--verbose"),
help="Make logging output more verbose",
action="store_true",
),
],
description=None,
epilog=None,
hide=False,
),
# ...
Key Considerations for Serialization
-
The
func: Callable
field inActionCommand
- The function reference must match exactly how
lazy_load_command
is called for thefunc
field. - This is handled using closures and free variables.
- The function reference must match exactly how
-
The
type: type | Callable
field inArg
- The
type
field can be a built-in type such asint
,str
, orbool
, or a callable likepositive_int
fromairflow.cli.cli_config
orparse
fromairflow.utils.timezone
. - This is resolved by comparing the
__module__
attribute and mapping it to the correct callable.
- The
-
The
default
field inArg
- The
default
field can either be a hardcoded value or retrieved usingconf.get*(section, key)
. - If it comes from
conf.get
,conf.getboolean
, orconf.getint
, it should be serialized as aconf.get*
string instead of a precomputed result.- This ensures that configuration values respect changes in
airflow.cfg
orAIRFLOW__*
environment variables.
- This ensures that configuration values respect changes in
- This is addressed using a monkey patch pattern + wrapper type:
- The
conf.get*
methods are monkey-patched by adding a decorator that returns a wrapper type with a custom__conf_source__
attribute. - By checking for the presence of
__conf_source__
, determine whether to serialize it as aconf.get*
string or a hardcoded constant.
- The
- The
Benchmark
MacOS, M2 Chip, 16G RAB, In breeze container.
Terminate breeze container and start a new session every benchmark.
breeze shell --answer n
/files/timer.sh airflow --help
With LocalExecutor
Configuration | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Average |
---|---|---|---|---|---|---|
Original | 3.0536 | 3.0363 | 3.1202 | 3.2155 | 3.1527 | 3.1157 |
Remove ExecutorLoader |
3.5628 | 3.0111 | 3.0560 | 3.0268 | 3.2067 | 3.1727 |
Remove get_auth_manager_cls |
1.1342 | 0.9723 | 1.1694 | 1.1146 | 0.9813 | 1.0744 |
Remove ExecutorLoader & get_auth_manager_cls |
1.0761 | 1.0334 | 0.9444 | 0.9392 | 0.9404 | 0.9867 |
My POC | 1.1256 | 0.9538 | 0.9181 | 0.9486 | 0.9374 | 0.9767 |
3.1157 / 0.9767 = 3.19002764, 3.19x faster !
With CeleryKubernetesExecutor
Configuration | Run 1 | Run 2 | Run 3 | Run 4 | Run 5 | Average |
---|---|---|---|---|---|---|
Original | 4.0599 | 3.9483 | 3.7732 | 3.5978 | 3.6112 | 3.7980 |
Remove ExecutorLoader |
3.1474 | 2.8519 | 2.8384 | 2.8141 | 2.7976 | 2.8899 |
Remove get_auth_manager_cls |
2.0906 | 1.7065 | 1.7159 | 1.7245 | 1.7101 | 1.7895 |
Remove ExecutorLoader & get_auth_manager_cls |
1.1131 | 0.9080 | 0.9181 | 0.9265 | 0.9177 | 0.9567 |
My POC | 1.0236 | 0.9300 | 0.9308 | 0.9221 | 0.9177 | 0.9448 |
3.7980 / 0.9448 = 4.01989839, 4x faster !
Related issues
No response
Are you willing to submit a PR?
- Yes I am willing to submit a PR!
Code of Conduct
- I agree to follow this project's Code of Conduct
Activity