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

kaxil 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 ea955744e4d Make airflow standalone discover new Dag files within 
seconds (#74479)
ea955744e4d is described below

commit ea955744e4dc735cdfa874f64e8bf4c9bc04d174
Author: Kaxil Naik <[email protected]>
AuthorDate: Fri Oct 9 16:22:32 2026 +0100

    Make airflow standalone discover new Dag files within seconds (#74479)
    
    A Dag file added after startup is only found when its bundle refreshes, and
    [dag_processor] refresh_interval defaults to 300 seconds, so under
    airflow standalone a new file took up to five minutes to show up.
    
    Standalone now passes its components a refresh_interval equal to
    [dag_processor] bundle_refresh_check_interval (5 seconds by default), so the
    Dags folder is rescanned each time the Dag processor checks whether a bundle
    is due. It does this only while refresh_interval and dag_bundle_config_list
    are at their defaults and the interval is not set in the environment, so an
    explicit choice wins and remote bundles keep their fetch cadence.
---
 .../src/airflow/cli/commands/standalone_command.py | 31 +++++++++++++
 .../src/airflow/config_templates/config.yml        |  4 ++
 .../unit/cli/commands/test_standalone_command.py   | 51 ++++++++++++++++++++++
 3 files changed, 86 insertions(+)

diff --git a/airflow-core/src/airflow/cli/commands/standalone_command.py 
b/airflow-core/src/airflow/cli/commands/standalone_command.py
index 943b85d53f5..15a9f5edd1d 100644
--- a/airflow-core/src/airflow/cli/commands/standalone_command.py
+++ b/airflow-core/src/airflow/cli/commands/standalone_command.py
@@ -16,6 +16,7 @@
 # under the License.
 from __future__ import annotations
 
+import json
 import logging
 import os
 import socket
@@ -44,6 +45,8 @@ if TYPE_CHECKING:
 FORCE_COLOR = bool(os.environ.get("FORCE_COLOR", ""))
 NO_COLOR = bool(os.environ.get("NO_COLOR", ""))
 
+REFRESH_INTERVAL_ENV_VAR = "AIRFLOW__DAG_PROCESSOR__REFRESH_INTERVAL"
+
 
 class StandaloneCommand:
     """
@@ -191,8 +194,36 @@ class StandaloneCommand:
             env["AIRFLOW__CORE__AUTH_MANAGER"] = simple_auth_manager_classpath
             os.environ["AIRFLOW__CORE__AUTH_MANAGER"] = 
simple_auth_manager_classpath  # also in this process!
 
+        # A new Dag file is found only when its bundle refreshes, every few 
minutes by default. Have the
+        # Dag processor refresh the Dags folder each time it checks whether 
any bundle is due.
+        if self.uses_default_dag_discovery():
+            refresh_interval = conf.getint("dag_processor", 
"bundle_refresh_check_interval")
+            self.print_output(
+                "standalone",
+                f"Checking for new Dag files every {refresh_interval} seconds "
+                "(set [dag_processor] refresh_interval to change this)",
+            )
+            env[REFRESH_INTERVAL_ENV_VAR] = str(refresh_interval)
+
         return env
 
+    def uses_default_dag_discovery(self) -> bool:
+        """
+        Check that the user left the Dags folder bundle and its refresh 
interval at their defaults.
+
+        The generated airflow.cfg writes out every default, so a value equal 
to the default counts as
+        unset there; an environment variable counts as a choice whatever its 
value. Other bundles are
+        left alone, since refreshing a remote bundle fetches from it.
+        """
+        if REFRESH_INTERVAL_ENV_VAR in os.environ:
+            return False
+        if conf.get("dag_processor", "refresh_interval") != 
conf.get_default_value(
+            "dag_processor", "refresh_interval"
+        ):
+            return False
+        default_bundles = json.loads(conf.get_default_value("dag_processor", 
"dag_bundle_config_list"))
+        return conf.getjson("dag_processor", "dag_bundle_config_list") == 
default_bundles
+
     def find_user_info(self):
         if conf.get("core", "simple_auth_manager_all_admins").lower() == 
"true":
             # If we have no auth anyways, no need to print or do anything
diff --git a/airflow-core/src/airflow/config_templates/config.yml 
b/airflow-core/src/airflow/config_templates/config.yml
index 25cffae10c3..7581497d0c5 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -3317,6 +3317,10 @@ dag_processor:
     refresh_interval:
       description: |
         How often (in seconds) to refresh, or look for new files, in a DAG 
bundle.
+
+        When this option and ``dag_bundle_config_list`` are left at their 
defaults, ``airflow standalone``
+        refreshes the Dags folder every ``bundle_refresh_check_interval`` 
seconds instead, so new Dag
+        files show up within seconds.
       version_added: ~
       type: integer
       example: ~
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 cd87c16ee49..5cfed400c43 100644
--- a/airflow-core/tests/unit/cli/commands/test_standalone_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_standalone_command.py
@@ -17,6 +17,7 @@
 # under the License.
 from __future__ import annotations
 
+import json
 import os
 import subprocess
 from collections import deque
@@ -26,6 +27,7 @@ from unittest import mock
 import pytest
 
 from airflow.cli.commands.standalone_command import StandaloneCommand, 
SubCommand
+from airflow.configuration import conf
 from airflow.executors import executor_loader
 from airflow.executors.executor_constants import (
     CELERY_EXECUTOR,
@@ -36,6 +38,14 @@ from airflow.executors.executor_constants import (
 from tests_common.test_utils.config import conf_vars
 
 
+def default_dag_discovery_config():
+    """Dag discovery options at their defaults, as the generated airflow.cfg 
writes them."""
+    return {
+        ("dag_processor", key): conf.get_default_value("dag_processor", key)
+        for key in ("refresh_interval", "dag_bundle_config_list")
+    }
+
+
 class TestStandaloneCommand:
     @pytest.mark.parametrize(
         "conf_executor_name",
@@ -85,6 +95,47 @@ class TestStandaloneCommand:
 
         assert "AIRFLOW__CORE__AUTH_MANAGER" not in env
 
+    def 
test_calculate_env_refreshes_dags_folder_on_every_bundle_check_by_default(self):
+        """The generated airflow.cfg spells out every default, which must not 
count as a user choice."""
+        config = {**default_dag_discovery_config(), ("dag_processor", 
"bundle_refresh_check_interval"): "5"}
+        with conf_vars(config), mock.patch.dict(os.environ):
+            env = StandaloneCommand().calculate_env()
+
+        assert env["AIRFLOW__DAG_PROCESSOR__REFRESH_INTERVAL"] == "5"
+
+    @pytest.mark.parametrize(
+        ("config", "environ"),
+        [
+            pytest.param({("dag_processor", "refresh_interval"): "60"}, {}, 
id="interval-in-airflow-cfg"),
+            pytest.param({}, {"AIRFLOW__DAG_PROCESSOR__REFRESH_INTERVAL": 
"60"}, id="interval-in-env"),
+            pytest.param(
+                {}, {"AIRFLOW__DAG_PROCESSOR__REFRESH_INTERVAL": "300"}, 
id="default-interval-in-env"
+            ),
+            pytest.param(
+                {
+                    ("dag_processor", "dag_bundle_config_list"): json.dumps(
+                        [
+                            {
+                                "name": "my-git-repo",
+                                "classpath": 
"airflow.providers.git.bundles.git.GitDagBundle",
+                                "kwargs": {},
+                            }
+                        ]
+                    )
+                },
+                {},
+                id="custom-bundles",
+            ),
+        ],
+    )
+    def test_calculate_env_keeps_user_chosen_dag_discovery(self, config, 
environ):
+        with conf_vars({**default_dag_discovery_config(), **config}), 
mock.patch.dict(os.environ, environ):
+            env = StandaloneCommand().calculate_env()
+
+        assert env.get("AIRFLOW__DAG_PROCESSOR__REFRESH_INTERVAL") == 
environ.get(
+            "AIRFLOW__DAG_PROCESSOR__REFRESH_INTERVAL"
+        )
+
     @mock.patch("airflow.cli.commands.standalone_command.os.path.exists", 
return_value=False)
     @mock.patch("airflow.cli.commands.standalone_command.create_auth_manager")
     @mock.patch("airflow.cli.commands.standalone_command.conf.get")

Reply via email to