lum1nxus opened a new issue, #66858:
URL: https://github.com/apache/airflow/issues/66858

   # Apache Airflow version
   3.1.x (chart `1.18.x`, current `main`)
   
   # What happened?
   
   The Airflow Helm chart stores `config.dag_processor.dag_bundle_config_list` 
inside the global `airflow.cfg` ConfigMap (`<release>-config`). Every 
long-running Airflow workload (scheduler, api-server, worker, triggerer, 
dag-processor, flower) carries a `checksum/airflow-config` pod annotation 
computed as `sha256sum` of that ConfigMap.
   
   Any time the bundle list changes — most commonly a `tracking_ref` bump 
emitted by a CI pipeline that promotes a new DAG version into the 
`GitDagBundle` configuration — the checksum flips and **every** component pod 
is rolled. In our environment a routine `helm upgrade` from a CI job tears down 
the scheduler, the api-server, all Celery workers, and the triggerer purely to 
refresh a value that only the dag-processor actually re-reads.
   
   This is a production-relevant restart blast radius: in-flight Celery tasks 
are interrupted, the api-server returns 503 for the duration of the rollout, 
and any DAG run boundary that lands in the rollout window is delayed.
   
   # What you think should happen instead?
   
   Mutations of `dag_bundle_config_list` should only roll the dag-processor. 
All other Airflow components either do not consume the value at runtime 
(api-server, flower) or only consume it lazily on bundle access with a per-task 
version pin (scheduler, worker, triggerer), so a `tracking_ref` change is 
functionally a no-op for them until they next refresh.
   
   We see strong precedent for "do not annotate a workload with a checksum it 
does not depend on" in **#63204 "Remove JWT secrets from triggerer, worker and 
dag-processor"**, which removed `checksum/jwt-secret` annotations from 
workloads that do not need the JWT secret. The exact same principle applies 
here.
   
   # Why is only the dag-processor a true consumer?
   
   We traced the runtime behavior in the current `main` branch:
   
   1. **dag-processor** — the only process that actively re-reads 
`dag_bundle_config_list` from `airflow.cfg`, instantiates each bundle, and 
calls `bundle.initialize()` on a refresh interval. The initialize path runs 
`self.repo.git.checkout(self.tracking_ref)` and is the only place where 
`tracking_ref` is materially used to determine what the bundle points at:
   
      `providers/git/src/airflow/providers/git/bundles/git.py:204`
      ```python
      self.repo.git.checkout(self.tracking_ref)
      ```
   
   2. **scheduler / api-server** — never call `bundle.initialize()`:
      - `airflow-core/src/airflow/jobs/` (scheduler job runners) contains 
**zero** references to `DagBundlesManager` or `bundle.initialize`.
      - `airflow-core/src/airflow/api_fastapi/` (api-server) likewise contains 
**zero** references to either symbol.
      - The single scheduler-side touch point is 
`dag_processing/dagbag.py:135`, gated by `if conf.getboolean("core", 
"multi_team")` and used only to read `_bundle_config[bundle_name].team_name` 
for executor selection — a config lookup, not an initialize.
      - All `bundle.initialize()` call sites in `airflow-core/` live in 
`dag_processing/manager.py` (dag-processor process) and 
`cli/commands/dag_command.py` / `utils/cli.py` (explicit operator CLI actions).
   
   3. **worker (Celery / KubernetesExecutor) / triggerer** — load bundle code 
via `BundleVersionLock` against the **persisted `bundle_version`** for the 
specific `TaskInstance` 
(`task-sdk/src/airflow/sdk/execution_time/task_runner.py:2002-2004`). The 
runtime hand-off is:
   
      - `task-sdk/.../task_runner.py:788` calls 
`DagBundlesManager().get_bundle(...)` to obtain a bundle instance,
      - then `task_runner.py:2002-2004` enters 
`BundleVersionLock(bundle_version=ti.bundle_instance.version, ...)` — i.e. the 
worker locks to the commit SHA recorded for that TI in the metadata DB, not to 
whatever `tracking_ref` currently points at.
   
      Inside `GitDagBundle._initialize` the `tracking_ref` checkout is 
overridden when `self.version` is set:
   
      `providers/git/src/airflow/providers/git/bundles/git.py:204-210`
      ```python
      self.repo.git.checkout(self.tracking_ref)
      self._log.debug("bundle initialize", version=self.version)
      if self.version:
          if not self._has_version(self.repo, self.version):
              self.repo.remotes.origin.fetch()
          self.repo.head.set_reference(str(self.repo.commit(self.version)))
          self.repo.head.reset(index=True, working_tree=True)
      ```
   
      So a worker that resumes after a `tracking_ref` bump will:
      - still find its pinned commit via local repo cache, or
      - `origin.fetch()` and pick it up from the remote.
   
      The only failure mode is when the **previous tag** is force-moved or 
deleted upstream — but that is an external bundle-management problem, not 
something a pod restart can fix.
   
   The net effect: rolling scheduler/api-server/worker/triggerer on a 
`dag_bundle_config_list` change buys nothing operationally and causes 
user-visible disruption.
   
   # How to reproduce
   
   Minimal reproducer using the official chart:
   
   ```bash
   # 1. Install with a Git bundle pointed at tag v1
   helm install airflow apache-airflow/airflow \
     --set dagProcessor.enabled=true \
     --set "dagProcessor.dagBundleConfigList[0].name=demo" \
     --set 
"dagProcessor.dagBundleConfigList[0].classpath=airflow.providers.git.bundles.git.GitDagBundle"
 \
     --set 
"dagProcessor.dagBundleConfigList[0].kwargs.repo_url=https://github.com/apache/airflow.git";
 \
     --set "dagProcessor.dagBundleConfigList[0].kwargs.tracking_ref=v1"
   
   # 2. Upgrade with only the tracking_ref changed
   helm upgrade airflow apache-airflow/airflow \
     --reuse-values \
     --set "dagProcessor.dagBundleConfigList[0].kwargs.tracking_ref=v2"
   
   # 3. Observe rollout
   kubectl get pods -w
   ```
   
   Expected (after fix): only the dag-processor pod is recreated.
   Actual (today): scheduler, api-server, worker, triggerer, flower, 
dag-processor are all recreated.
   
   The same symptom is reproducible with any other field of 
`dagBundleConfigList` (adding a new bundle, changing `subdir`, rotating 
`git_conn_id`) — anything that mutates the JSON string causes a global restart.
   
   # Solution shapes considered
   
   We have a working local fix and are happy to upstream it. Before opening a 
PR we would like maintainer guidance on the preferred shape. Three options, in 
order of increasing surface area:
   
   ## A. Documentation-only (smallest)
   Document the existing escape hatch in `chart/docs/manage-dag-files.rst`: a 
Flux `spec.postRenderers` / Argo Kustomize patch that pins 
`checksum/airflow-config` to a static value on the non-dag-processor workloads. 
Zero chart code change. Reaches Flux / Argo users but not plain `helm install` 
users. Operator owns the trade-off explicitly.
   
   ## B. Per-component opt-out flag (small)
   Add `<component>.restartOnConfigChange: true` (default `true`) for scheduler 
/ api-server / worker / triggerer / flower. When set to `false`, the chart 
simply omits the `checksum/airflow-config` annotation on that component. 
Follows the same shape as PR **#18776** (opt-out for hardcoded annotations). 
Diff: ~5 lines per deployment template + values.yaml/schema + tests.
   
   Caveat: this is a blunt instrument — disabling the annotation also stops 
*all* `airflow.cfg` changes (e.g. `executor` swap, DB URL rotation, image 
upgrade alongside a config change) from automatically rolling that component. 
Operator footgun, but explicit and documented.
   
   ## C. Isolated bundle ConfigMap (cleanest, biggest)
   Introduce an opt-in `dagProcessor.isolateBundleConfigRestart: false` flag. 
When `true`:
   - `dag_bundle_config_list` is rendered into a **dedicated** ConfigMap 
(`<release>-dag-bundle-config`) only, not into the global `airflow.cfg`.
   - `AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST` is injected as an env var 
(via `configMapKeyRef`) into every Airflow component pod, so Python config 
resolution behaves identically to today.
   - Only the dag-processor deployment grows a `checksum/dag-bundle-config` 
annotation pointing at the new ConfigMap.
   - `checksum/airflow-config` remains, and continues to roll every component 
when *real* `airflow.cfg` keys change (executor, DB, etc.).
   
   This is the same blast-radius surgery as PR **#63204** applied to the bundle 
list: stop annotating workloads with a checksum they do not depend on, and 
route the now-isolated value through a narrower ConfigMap.
   
   Local diff size, for reference:
   - 4 modified templates (`configmap.yaml`, `_helpers.yaml`, 
`dag-processor-deployment.yaml`, `values.schema.json`)
   - 1 modified default (`values.yaml`)
   - 1 new template (`configmaps/dag-bundle-config-configmap.yaml`)
   - 1 new test file (16 tests, all green locally)
   - 1 newsfragment
   - Docs update in `manage-dag-files.rst` describing the runbook for 
structural bundle changes (operator must `kubectl rollout restart` if they 
intend a global refresh)
   
   Default is `false`, so this is fully backwards-compatible.
   
   # Why we think C is the right long-term shape
   
   - It mirrors **#63204** precisely: don't carry a checksum dependency on a 
value the workload does not consume.
   - It composes cleanly with #60645 (which introduced the structured 
`dagBundleConfigList` value) — that PR improved the *authoring* side; this 
proposal improves the *delivery* side.
   - It does not require chart 2.0 or a breaking change: behavior under default 
`isolateBundleConfigRestart=false` is bit-identical to today.
   - The runbook cost is minimal and bounded: the only case where an operator 
needs a manual `kubectl rollout restart` is a *structural* bundle change 
(executor swap of `BundleVersionLock` semantics in a future version), which is 
rare and explicit.
   
   We also considered:
   
   - **Using `_cmd` suffix to read the bundle list from a file** — rejected, 
`_cmd` is gated to sensitive values only and Airflow does not generically 
support file-includes in `airflow.cfg`.
   - **Computing two checksums (full airflow.cfg + a subset without the bundle 
list)** — possible but requires re-rendering `airflow.cfg` twice in the chart 
and produces a confusing pod annotation surface. ConfigMap split is simpler.
   - **`tpl`-driven user override of `checksum/airflow-config` via 
`podAnnotations`** — already works today (YAML last-key-wins lets a 
user-supplied annotation win) but is an undocumented behavior, fragile across 
chart upgrades, and silently disables checksum for *all* airflow.cfg changes.
   
   # Ask
   
   Before we open a PR, we would appreciate a maintainer's steer on the 
preferred shape (A / B / C / something else). We have C implemented and tested 
locally and are ready to open the PR as-is, but we want to make sure it aligns 
with the direction of chart 2.0 restructuring discussed in **#61039**.
   
   # Anything else?
   
   This bites us on **every** CI-driven `tracking_ref` bump, which on a busy 
DAG repo happens 10+ times per day. We are happy to provide additional traces, 
manifest diffs, or to break C into smaller incremental PRs if that is easier to 
review.
   
   # Are you willing to submit PR?
   
   - [x] Yes I am willing to submit a PR!
   
   # Code of Conduct
   
   - [x] I agree to follow this project's Code of Conduct
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to