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 f721cac5e5c Fix scheduler refusing to start when [scheduler] only_idle
is enabled (#76262)
f721cac5e5c is described below
commit f721cac5e5cf3a1e4ed3c249279bdaa50a660413
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Nov 3 22:08:26 2026 +0100
Fix scheduler refusing to start when [scheduler] only_idle is enabled
(#76262)
The --only-idle argument read its default from config when the parser was
built, so a config-enabled only_idle looked like an explicitly passed flag and
tripped the guard requiring a positive --num-runs, which defaults to -1. It
also made the option impossible to switch off from the command line.
Generated-by: Claude Opus 5
Co-authored-by: Eason09053360
<[email protected]>
---
airflow-core/src/airflow/cli/cli_config.py | 10 ++++--
.../src/airflow/cli/commands/scheduler_command.py | 17 ++++++++--
.../src/airflow/config_templates/config.yml | 1 +
.../unit/cli/commands/test_scheduler_command.py | 38 ++++++++++++++++++++++
airflow-core/tests/unit/cli/test_cli_parser.py | 21 ++++++++++++
5 files changed, 81 insertions(+), 6 deletions(-)
diff --git a/airflow-core/src/airflow/cli/cli_config.py
b/airflow-core/src/airflow/cli/cli_config.py
index 7bc1517700f..7e8a1533c28 100644
--- a/airflow-core/src/airflow/cli/cli_config.py
+++ b/airflow-core/src/airflow/cli/cli_config.py
@@ -846,9 +846,13 @@ ARG_NUM_RUNS = Arg(
ARG_ONLY_IDLE = Arg(
("-i", "--only-idle"),
- default=conf.getboolean("scheduler", "only_idle", fallback=False),
- help="Only count runs after the scheduler becomes idle.",
- action="store_true",
+ default=None,
+ # Square brackets would be eaten as Rich markup by the help formatter, so
name the option dotted.
+ help=(
+ "Only count runs after the scheduler becomes idle. Requires --num-runs
to be a positive number. "
+ "Defaults to the scheduler.only_idle config option; pass
--no-only-idle to override it."
+ ),
+ action=argparse.BooleanOptionalAction,
)
ARG_WITHOUT_MINGLE = Arg(
diff --git a/airflow-core/src/airflow/cli/commands/scheduler_command.py
b/airflow-core/src/airflow/cli/commands/scheduler_command.py
index 2217717e50e..8f583844cdb 100644
--- a/airflow-core/src/airflow/cli/commands/scheduler_command.py
+++ b/airflow-core/src/airflow/cli/commands/scheduler_command.py
@@ -37,13 +37,19 @@ from airflow.utils.scheduler_health import
serve_health_check
log = logging.getLogger(__name__)
+def _resolve_only_idle(args: Namespace) -> bool:
+ if args.only_idle is None:
+ return conf.getboolean("scheduler", "only_idle", fallback=False)
+ return args.only_idle
+
+
@enable_memray_trace(component=MemrayTraceComponents.scheduler)
def _run_scheduler_job(args) -> None:
set_component_mp_start_method("scheduler")
job_runner = SchedulerJobRunner(
job=Job(),
num_runs=args.num_runs,
- only_idle=args.only_idle,
+ only_idle=_resolve_only_idle(args),
)
enable_health_check = conf.getboolean("scheduler", "ENABLE_HEALTH_CHECK")
with _serve_logs(args.skip_serve_logs),
_serve_health_check(enable_health_check):
@@ -56,8 +62,13 @@ def scheduler(args: Namespace):
"""Start Airflow Scheduler."""
cli_utils.print_banner()
- if args.only_idle and args.num_runs <= 0:
- raise SystemExit("The --only-idle flag requires --num-runs to be set
to a positive number.")
+ if _resolve_only_idle(args) and args.num_runs <= 0:
+ if args.only_idle:
+ raise SystemExit("The --only-idle flag requires --num-runs to be
set to a positive number.")
+ log.warning(
+ "`[scheduler] only_idle` is enabled but has no effect because the
run limit is not a "
+ "positive number; set --num-runs or `[scheduler] num_runs` to a
positive number."
+ )
if cli_utils.should_enable_hot_reload(args):
from airflow.cli.hot_reload import run_with_reloader
diff --git a/airflow-core/src/airflow/config_templates/config.yml
b/airflow-core/src/airflow/config_templates/config.yml
index b87f27c5093..4b1ecfb99be 100644
--- a/airflow-core/src/airflow/config_templates/config.yml
+++ b/airflow-core/src/airflow/config_templates/config.yml
@@ -2758,6 +2758,7 @@ scheduler:
description: |
Only count scheduler runs where the scheduler was idle (no tasks
queued or finished)
toward the run limit set by ``[scheduler] num_runs``. The count resets
whenever a task is processed.
+ Has no effect unless ``[scheduler] num_runs`` is a positive number.
version_added: 3.2.0
type: boolean
example: ~
diff --git a/airflow-core/tests/unit/cli/commands/test_scheduler_command.py
b/airflow-core/tests/unit/cli/commands/test_scheduler_command.py
index 1089edc12e8..dcd8c6896d0 100644
--- a/airflow-core/tests/unit/cli/commands/test_scheduler_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_scheduler_command.py
@@ -194,3 +194,41 @@ class TestSchedulerCommand:
call_kwargs = mock_scheduler_job.call_args[1]
assert call_kwargs["only_idle"] is True
assert call_kwargs["num_runs"] == 5
+
+ @conf_vars({("scheduler", "only_idle"): "True"})
+ @mock.patch("airflow.cli.commands.scheduler_command.SchedulerJobRunner")
+ @mock.patch("airflow.cli.commands.scheduler_command.Process")
+ def test_only_idle_from_config_does_not_block_startup(self, mock_process,
mock_scheduler_job, caplog):
+ mock_scheduler_job.return_value.job_type = "SchedulerJob"
+ args = self.parser.parse_args(["scheduler"])
+ assert args.num_runs <= 0
+
+ scheduler_command.scheduler(args)
+
+ assert mock_scheduler_job.call_args[1]["only_idle"] is True
+ assert (
+ "`[scheduler] only_idle` is enabled but has no effect because the
run limit is not a "
+ "positive number; set --num-runs or `[scheduler] num_runs` to a
positive number." in caplog
+ )
+
+ @conf_vars({("scheduler", "only_idle"): "True"})
+ @mock.patch("airflow.cli.commands.scheduler_command.SchedulerJobRunner")
+ @mock.patch("airflow.cli.commands.scheduler_command.Process")
+ def test_no_only_idle_overrides_config(self, mock_process,
mock_scheduler_job):
+ mock_scheduler_job.return_value.job_type = "SchedulerJob"
+ args = self.parser.parse_args(["scheduler", "--no-only-idle"])
+
+ scheduler_command.scheduler(args)
+
+ assert mock_scheduler_job.call_args[1]["only_idle"] is False
+
+ @conf_vars({("scheduler", "only_idle"): "True"})
+ @mock.patch("airflow.cli.commands.scheduler_command.SchedulerJobRunner")
+ @mock.patch("airflow.cli.commands.scheduler_command.Process")
+ def test_only_idle_from_config_reaches_job_runner(self, mock_process,
mock_scheduler_job):
+ mock_scheduler_job.return_value.job_type = "SchedulerJob"
+ args = self.parser.parse_args(["scheduler", "-n", "5"])
+
+ scheduler_command.scheduler(args)
+
+ assert mock_scheduler_job.call_args[1]["only_idle"] is True
diff --git a/airflow-core/tests/unit/cli/test_cli_parser.py
b/airflow-core/tests/unit/cli/test_cli_parser.py
index f12b4da158d..3a4a62891e6 100644
--- a/airflow-core/tests/unit/cli/test_cli_parser.py
+++ b/airflow-core/tests/unit/cli/test_cli_parser.py
@@ -444,6 +444,27 @@ class TestCli:
args = parser.parse_args([])
assert args.test == 0
+ @pytest.mark.parametrize(
+ ("argv", "expected"),
+ [
+ # None, not a bool: the parser must not bake ``[scheduler]
only_idle`` in at import time.
+ ([], None),
+ (["--only-idle"], True),
+ (["--no-only-idle"], False),
+ ],
+ )
+ def test_only_idle_flag_is_tri_state(self, argv, expected):
+ args = cli_parser.get_parser().parse_args(["scheduler", *argv])
+ assert args.only_idle is expected
+
+ def test_only_idle_help_survives_rich_markup_rendering(self):
+ """The help formatter renders Rich markup, which silently swallows
``[section]`` text."""
+ stdout = StringIO()
+ with contextlib.redirect_stdout(stdout), pytest.raises(SystemExit):
+ cli_parser.get_parser().parse_args(["scheduler", "--help"])
+
+ assert "scheduler.only_idle" in stdout.getvalue()
+
def test_commands_and_command_group_sections(self):
parser = cli_parser.get_parser()