kaxil commented on code in PR #70298:
URL: https://github.com/apache/airflow/pull/70298#discussion_r3980597453
##########
dev/registry/extract_parameters.py:
##########
@@ -432,6 +433,41 @@ def is_durable_capable(cls: type, resumable_mixin: type |
None) -> bool:
return "execute_resumable" in source
+# Hand verified set that contains classes where self.defer()/self.deferrable
is reachable only
+# through a helper method one call away from execute() (e.g.
TriggerDagRunOperator
+# delegates to _trigger_dag_af_2()), so the source grep below can't find it.
+# Add an entry only after confirming self.defer() is genuinely reachable.
+_DEFERRABLE_EXCEPTIONS = {
+
"airflow.providers.standard.operators.trigger_dagrun.TriggerDagRunOperator",
+}
+
+
+def supports_deferrable(cls: type) -> bool:
+ """Return True if the class's resolved execute() actually references
deferral.
+
+ Checking for a `deferrable` constructor parameter isn't enough: a subclass
can
+ inherit the parameter while overriding execute() with code that never
reads it,
+ and an operator that always defers unconditionally has no parameter
+ to find at all. Resolving execute() via getattr and checking its own
source for
+ self.deferrable or self.defer answers "does this code path use deferral"
+ directly, instead of proxying through whether a setting merely exists
somewhere
+ in the class hierarchy.
+ """
+ qualified_name = f"{cls.__module__}.{cls.__qualname__}"
+ if qualified_name in _DEFERRABLE_EXCEPTIONS:
+ return True
+
+ execute = getattr(cls, "execute", None)
+ if execute is None:
+ return False
+ try:
+ source = inspect.getsource(execute)
+ except (OSError, TypeError):
+ return False
+
+ return "self.deferrable" in source or "self.defer" in source
Review Comment:
Ran this against the tree (1292 registered operator, sensor and transfer
classes): `DiscordWebhookOperator` stays False, so the false positive is gone,
but the exception list covers one of four delegation shapes and eleven
registered classes that defer come back False:
- `raise TaskDeferred(` with no `self.defer` anywhere:
`VespaIngestOperator.execute` (`vespa_ingest.py:85`, `vespa/provider.yaml:53`).
`self.defer` is already a prefix of `self.deferrable`, so the first clause on
this line is dead; `"TaskDeferred" in source` is the one worth having.
- A child `execute` wrapping `super().execute(context)`: `EksPodOperator`
(`eks.py:1244`; the branch is in KPO's `execute` at `pod.py:803`) and the
deprecated `GlueCrawlerOperator` (`glue_crawler.py:317` into
`GlueCrawlerRunOperator.execute` at `:182`). Following `super().execute(` up
the MRO fixes both without touching Discord, whose `execute` never calls super.
- `self.defer` behind a helper: `DatabricksStartWarehouseOperator` and
`DatabricksStopWarehouseOperator` (`warehouse.py:183`, `:209` into
`_wait_or_defer` at `:117-118`); `DatabricksTaskBaseOperator` via
`monitor_databricks_job` to `_defer_on_run` (`databricks.py:2197`,
`:2185-2186`, `:2071`), inherited unchanged by `DatabricksNotebookOperator` and
`DatabricksTaskOperator`;
`TeradataComputeClusterProvision/Resume/SuspendOperator` via
`_compute_cluster_execute` to `_handle_cc_status`
(`teradata_compute_cluster.py:168`, unconditional).
With the count in the label the misses are visible: the Databricks page
renders `(4)` at HEAD where your round-2 screenshot showed `(7)`, and the PR
description's own "deferrable only" list (`DatabricksNotebookOperator`,
`DatabricksTaskBaseOperator`, `DatabricksTaskOperator`) is False at HEAD. The
description also still describes the constructor-parameter detector and
`get_params_from_class`, so it wants a refresh once this settles.
A bounded walk from the resolved `execute` (follow `self.<name>(` through
`getattr`, follow `super().execute(` to the next `execute` in the MRO, stop on
`self.defer`/`self.deferrable`/`TaskDeferred`) goes from 243 to 254 True across
those 1292 classes, the eleven above exactly, with `TriggerDagRunOperator`
found without the list and Discord still False since its only self-call is
`self.hook.execute()`. If the list stays instead, it needs the ten call-chain
names and a test that fails when a listed class starts matching the grep.
Separately, match `self.defer` on a word boundary: `self.defer_for_approval(`
in the five common.ai LLM operators (`llm.py:169`) counts today by prefix
accident. Whether they should carry the badge is its own call, since on 3.3+
that path raises `TaskAwaitingInput` with no trigger at all
(`approval.py:168-172`) and only pre-3.3 cores fall back to
`self.defer(HITLTrigger)`, so the Triggerer promise in the tooltip is wrong for
them on current cores.
##########
dev/registry/extract_parameters.py:
##########
@@ -463,7 +499,7 @@ def discover_classes_from_provider(
"""Discover classes from a single provider by importing its modules at
runtime.
Reads the provider.yaml to find which modules/classes to inspect, imports
them,
- and returns metadata for each discovered class with all 12 Module fields.
+ and returns metadata for each discovered class with all 13 Module fields.
Review Comment:
Ash's nit about the field count landed on `make_entry` but not here or on
`test_has_all_13_fields` (`test_extract_parameters.py:362`); both bump every
time a field is added.
##########
registry/src/provider-version.njk:
##########
@@ -403,6 +427,9 @@ eleventyComputed:
{% if module.supports_durable_execution %}
<span class="durable-badge" title="Reconnects to an
already-running job on retry instead of resubmitting. Requires durable=True
(default) and depends on operator configuration.">Durable</span>
{% endif %}
+ {% if module.supports_deferrable %}
+ <span class="deferrable-badge" title="Frees the worker slot
while waiting. If the Triggerer crashes, another one picks it up using the same
trigger class and arguments. Requires deferrable=True.">Deferrable</span>
Review Comment:
With the new predicate the badge now lands on operators that defer
unconditionally and have no `deferrable` parameter, so "Requires
deferrable=True" is an instruction that fails for them. `DateTimeSensorAsync`
(`date_time.py:106-114`) and `HITLOperator` (`hitl.py:199-244`) forward
`**kwargs` to BaseOperator, which raises `TypeError("Invalid arguments were
passed ...")` on the leftover key (`operator.py:1103-1107`); `TimeSensorAsync`
hard-codes `super().__init__(deferrable=True, **kwargs)` (`time.py:128`), so
passing it again is a duplicate-keyword TypeError. Something like "Pass
deferrable=True where the operator exposes it; some defer unconditionally"
covers both, and the same phrase sits on the filter tooltip at :402.
##########
registry/src/css/tokens.css:
##########
@@ -96,6 +97,8 @@
--color-rose-600: #e11d48;
--color-teal-400: #2dd4bf;
--color-teal-500: #14b8a6;
+ --color-teal-600: #0d9488;
Review Comment:
`--color-teal-600` was added for the previous round and nothing references
it now that the badge moved to `teal-700`; it can go.
--
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]