shahar1 commented on code in PR #74915:
URL: https://github.com/apache/airflow/pull/74915#discussion_r4367631126


##########
airflow-core/src/airflow/dag_processing/bundles/manager.py:
##########
@@ -691,6 +825,10 @@ def get_bundle(
 
         :return: The DAG bundle.
         """
+        if name not in self._bundle_config:
+            if name not in _get_cached_bundle_config_snapshot().names:
+                _refresh_bundle_config_snapshot(force=True)

Review Comment:
   **minor** — Every `get_bundle()` call for a name that is not configured 
forces a fresh read of the command or secret, and ignores 
`bundle_refresh_check_interval`. Nothing limits how often this happens. In the 
Dag processor this runs in the main loop: `prepare_callback_bundle()` calls 
`get_bundle()` once for each versioned callback. After a bundle is removed, 
every callback for its in-flight runs starts a synchronous command or 
secrets-backend call. A slow or remote source then stalls parsing. I confirmed 
it at 42d2b4ef with a mocked `run_command`: five `get_bundle("removed-bundle")` 
calls ran the command five more times, after the one read at startup.
   
   **Suggestion:** Rate-limit the forced read as well, e.g. at most once per 
`bundle_refresh_check_interval`, or remember the names a forced read did not 
find until the next scheduled read. Add a test that two lookups of the same 
unknown name read the source only once.



##########
airflow-core/src/airflow/dag_processing/manager.py:
##########
@@ -1003,15 +1036,65 @@ def _refresh_dag_bundles(self, known_files: dict[str, 
set[DagFileInfo]]):
             self._resort_file_queue()
             self._add_new_files_to_queue(known_files=known_files)
 
+    def _reconcile_bundles(self, known_files: dict[str, set[DagFileInfo]]) -> 
None:
+        if not is_bundle_config_reload_enabled():
+            return
+        try:
+            dag_bundle_manager = DagBundlesManager()
+            bundle_names = set(dag_bundle_manager.get_all_bundle_names())
+        except Exception:
+            self.log.exception("Could not load Dag bundles; keeping the 
current bundles")
+            return
+
+        loaded_bundles = {bundle.name: bundle for bundle in self._dag_bundles}
+        if self.bundle_names_to_parse:
+            bundle_names.intersection_update(self.bundle_names_to_parse)
+        removed_names = loaded_bundles.keys() - bundle_names
+        bundle_instances: dict[str, BaseDagBundle] = {}
+        for name in sorted(bundle_names - loaded_bundles.keys()):
+            try:
+                bundle_instances[name] = dag_bundle_manager.get_bundle(name)
+            except Exception:
+                self.log.exception("Could not create Dag bundle %s; will 
retry", name)
+
+        try:
+            # Other processors can overwrite active state even when our 
configuration is unchanged.
+            dag_bundle_manager.sync_bundles_to_db(
+                deactivate_missing=not self.bundle_names_to_parse,
+                bundle_instances=bundle_instances,
+            )
+        except Exception:
+            self.log.exception("Could not reconcile Dag bundles; keeping the 
current bundles")

Review Comment:
   **minor** — `_refresh_bundle_config_snapshot()` accepts a new bundle without 
checking that its `team_name` exists. `sync_bundles_to_db()` then raises `Team 
'...' does not exist` on every check, and `_reconcile_bundles()` returns before 
it applies anything. One typo in one new bundle's team therefore blocks all 
unrelated additions and removals in this processor until the list changes 
again. The author also cannot fix the typo in place, because changing an 
accepted bundle's definition is rejected ("Use a new bundle name ..."). The 
bundle has to be removed or renamed. In the meantime the API server and workers 
have already accepted the list with that bundle in it.
   
   **Suggestion:** Check that the teams exist when the list is accepted (the 
Dag processor has DB access), so the bad list is rejected as a whole like an 
invalid classpath. Or skip only the failing bundle and apply the rest of the 
list. Either way, add a test with a new bundle whose team does not exist next 
to a removal.



##########
airflow-core/src/airflow/dag_processing/bundles/manager.py:
##########
@@ -387,40 +498,49 @@ def _extract_and_sign_template(bundle_name: str) -> 
tuple[str | None, dict]:
                     self.log.debug("Signed URL template for bundle %s", 
bundle_name)
             return new_template_, new_params_
 
-        stored = {b.name: b for b in 
session.scalars(select(DagBundleModel)).all()}
+        stored = {
+            b.name: b
+            for b in 
session.scalars(select(DagBundleModel).options(selectinload(DagBundleModel.teams))).all()
+        }
         bundle_to_team = {
             bundle.name: bundle.teams[0].name if len(bundle.teams) == 1 else 
None
             for bundle in stored.values()
         }
 
+        requested_teams = {config.team_name for config in 
self._bundle_config.values() if config.team_name}
+        teams = {
+            team.name: team for team in 
session.scalars(select(Team).where(Team.name.in_(requested_teams)))
+        }
+
         for name, config in self._bundle_config.items():
             team: Team | None = None
             if config.team_name:
-                team = session.scalars(select(Team).where(Team.name == 
config.team_name)).one_or_none()
+                team = teams.get(config.team_name)
                 if not team:
                     raise _bundle_item_exc(f"Team '{config.team_name}' does 
not exist")
 
-            try:
-                new_template, new_params = _extract_and_sign_template(name)
-            except Exception as e:
-                self.log.exception("Error creating bundle '%s': %s", name, e)
-                continue
-
-            if bundle := stored.pop(name, None):
-                bundle.active = True
+            bundle = stored.pop(name, None)
+            update_template = bundle_instances is None or name in 
bundle_instances
+            if update_template:
+                try:
+                    new_template, new_params = _extract_and_sign_template(name)
+                except Exception as e:
+                    self.log.exception("Error creating bundle '%s': %s", name, 
e)
+                    if bundle is None and bundle_instances is None:
+                        continue
+                    update_template = False

Review Comment:
   **nit** — A newly added bundle is signed only in the reconcile that adds it, 
while it is still in `bundle_instances`. If `view_url_template()` or the 
parameter extraction fails at that moment, the row is created active and has no 
template. Later reconciles pass that bundle as already loaded, so 
`update_template = name in bundle_instances` stays `False`. The URL is then 
missing until the processor restarts. This differs from the "failed 
construction is retried" behaviour in the docs.
   
   **Suggestion:** Retry signing while the stored template is missing, or keep 
the bundle in a "needs signing" set until signing succeeds.



##########
airflow-core/src/airflow/dag_processing/processor.py:
##########
@@ -116,6 +117,9 @@ class DagFileParseRequest(BaseModel):
     bundle_name: str
     """Bundle name for team-specific executor validation."""
 
+    bundle_config: dict[str, Any] | None = None

Review Comment:
   **minor** — The PR body says the change adds no "`DagFileParseRequest` 
change". The diff does change it: it adds `bundle_config` to 
`DagFileParseRequest` and to `StartLangSDKRuntime`, adds a `VersionChange` 
(`AddBundleConfigToDagFileParseRequest`) to the in-progress supervisor schema 
version, and regenerates the Go and TypeScript schemas. The schema steps follow 
`task-sdk/src/airflow/sdk/execution_time/schema/AGENTS.md`, so the code is 
fine. But reviewers of the lang-SDK contract need to know about this change. 
The field is also an untyped `dict[str, Any]` in the cross-language schema, 
although only the Python parser reads it.
   
   **Suggestion:** Correct the body and say why the field is needed: parse 
children must not read the bundle config source again. Consider a typed model 
(`importers`, `team_name`) instead of `dict[str, Any]`, so the cross-language 
schema says what the field holds.



-- 
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