kaxil commented on code in PR #70298:
URL: https://github.com/apache/airflow/pull/70298#discussion_r3768156006


##########
registry/src/provider-version.njk:
##########
@@ -383,6 +383,19 @@ eleventyComputed:
     <section class="modules">
       <div class="modules-header">
         <h2>Modules</h2>
+        {% if pv.isLatest %}
+        {# pv.versionData.modules (older versions) lacks these fields 
entirely. #}
+        <div class="capability-filter-toggles">
+          <label class="capability-filter-toggle" title="Reconnects on retry 
(ResumableJobMixin) or frees the worker slot while waiting (deferrable) instead 
of resubmitting or blocking.">

Review Comment:
   "Reconnects on retry (ResumableJobMixin)" names a mechanism neither 
durable-marked operator actually uses. `KubernetesPodOperator` is 
`(BaseOperator)` (`pod.py:142`) and `AgentOperator` is `(BaseOperator, 
HITLReviewMixin)` (`agent.py:120`); both carry the manual marker with a comment 
right above it saying "supports durable execution directly, without 
ResumableJobMixin" (`pod.py:286-288`, `agent.py:224-226`). `is_durable_capable` 
has the two paths precisely so those operators qualify without the mixin, so 
the mixin is the one thing the currently-badged modules have in common only by 
not using it. The shipped badge copy at :418 describes the behaviour without 
naming an SDK class, which reads better for a site visitor anyway.
   
   While you're in here, the Deferrable badge tooltip on :421 says "trigger 
state is persisted and reassigned automatically", which is a bit stronger than 
what happens: the row persists the classpath and the serialized kwargs, not 
progress, so a reassigned trigger re-runs `run()` from the start rather than 
resuming. `deferring.rst:170` tells trigger authors to assume exactly that ("a 
trigger instance can run *more than once*").



##########
registry/src/provider-version.njk:
##########
@@ -383,6 +383,19 @@ eleventyComputed:
     <section class="modules">
       <div class="modules-header">
         <h2>Modules</h2>
+        {% if pv.isLatest %}

Review Comment:
   The `isLatest` gate closes the older-version path, but there's a second 
route to the same dead toggle it doesn't cover, and this one hits the pages 
people actually land on.
   
   The live catalogue is rebuilt incrementally. `publish-docs-to-s3.yml:655` 
passes specific provider ids into `registry-build.yml`, which pulls the 
published `modules.json` down from S3, and `merge_registry_data.py:83-84` keeps 
every non-targeted provider's module dicts verbatim. `_validate` 
(`registry_contract_models.py:233-235`) is `model_validate(payload); return 
payload`, so it never writes the Pydantic defaults back into the dict. So after 
the next provider release, every provider that wasn't in that build still has 
`pv.isLatest` true and no `supports_deferrable` key at all, and ticking either 
box blanks its grid.
   
   It isn't only a stale-data problem either. From the current tree, 17 of 105 
provider packages contain a class declaring a `deferrable` parameter, and the 
durable marker appears in exactly 2 (`pod.py:288`, `agent.py:226`), so 87 
packages have neither. Amazon and Google alone account for most of the 
deferrable classes. Both toggles are dead controls on the large majority of 
provider pages even after a full rebuild.
   
   Counting in the template and rendering each box only when its count is 
non-zero fixes the stale-data case and the genuinely-empty case together, and 
self-heals when a rebuild lands. The accumulate-in-a-loop pattern already works 
in this file (`totalModules` at :38-39), and the type tabs 28 lines down 
already gate themselves the same way (`{% if moduleCounts[t.id] > 0 %}` at 
:358). Putting the count in the label saves a click too. Worth pairing with the 
empty state from `provider-filters.js:81-94`, which already hides the grid and 
writes a `#filter-status` count for the providers list; the module grid has no 
equivalent, and the "no modules" banner at :454-465 is keyed off `totalModules` 
so client-side filtering never reaches it.



##########
registry/src/css/main.css:
##########
@@ -3468,8 +3490,18 @@ main {
   font-size: var(--text-xs);
   font-weight: var(--font-medium);
   border-radius: var(--radius-lg);
+}
+
+.provider-detail-page .modules .module .content h3 .durable-badge {
   background: color-mix(in srgb, var(--color-teal-500) 15%, transparent);
-  color: var(--color-teal-400);
+  color: var(--color-teal-600);
+  color: light-dark(var(--color-teal-600), var(--color-teal-400));

Review Comment:
   This is a real improvement, the light-mode durable badge was effectively 
unreadable at 1.62:1 before. It just doesn't quite clear the line: compositing 
the 15% `teal-500` wash onto the card's own `--color-card-bg`, which resolves 
to `#ffffff` in light mode (`tokens.css:250`), `teal-600` comes out at 3.26:1, 
against the 4.5:1 that 12px `--font-medium` text needs. `teal-700` (#0f766e) 
gets it to 4.77:1 on the same composite. For what it's worth my last comment 
under-called the blue one too: `blue-600` is 4.35:1, just under the line, where 
`blue-700` (#1d4ed8) would be 5.64:1. Dark mode is comfortable either way, 
7.9:1 and 6.1:1.



##########
registry/src/js/provider-detail.js:
##########
@@ -25,6 +25,9 @@
   var extraDepsList = document.getElementById('extra-deps-list');
   var extrasDataEl = document.getElementById('extras-data');
   var moduleSearch = document.getElementById('module-search');
+  var durableOnlyFilter = document.getElementById('durable-only-filter');
+  var deferrableOnlyFilter = document.getElementById('deferrable-only-filter');
+  var moduleTabs = document.querySelectorAll('.module-tab');

Review Comment:
   This one looks like it came back in with the main merge rather than on 
purpose. #70190 narrowed the selector to `.module-tab[data-type]`, which is 
still there on line 33, so the branch's older copy landing above it leaves two 
`var moduleTabs` in the same scope. Nothing breaks today since the second 
assignment wins, but the dead line is the exact selector the comment on 31-32 
exists to warn against: the natural instinct on seeing two near-identical 
declarations is to delete the second one, and then the More button joins 
`moduleTabs` and clicking it resets `currentType` to `'all'`.



##########
dev/registry/extract_parameters.py:
##########
@@ -432,6 +433,11 @@ def is_durable_capable(cls: type, resumable_mixin: type | 
None) -> bool:
     return "execute_resumable" in source
 
 
+def supports_deferrable(cls: type) -> bool:
+    """Return True if a class exposes a `deferrable` constructor parameter."""

Review Comment:
   `get_params_from_class` unions every ancestor's `__init__` params, so this 
answers "does some ancestor name `deferrable`" rather than "can this defer". 
Because the union ignores who actually *reads* the parameter, it currently 
publishes at least one capability claim that isn't true.
   
   `DiscordWebhookOperator` is `class DiscordWebhookOperator(HttpOperator)` and 
is registered in the catalogue (`discord/provider.yaml:74-77`). Its own 
`__init__` declares no `deferrable` and forwards `**kwargs` up 
(`discord_webhook.py:57-70`), so the union picks the param up from 
`HttpOperator.__init__` (`http/operators/http.py:121`). But the only thing that 
reads it is `HttpOperator.execute`'s `if self.deferrable: 
self.execute_async(...)` (`http.py:164-168`), and 
`DiscordWebhookOperator.execute` overrides that outright with a synchronous 
`self.hook.execute()` (`discord_webhook.py:95-102`). So it gets the Deferrable 
badge, matches both new checkboxes, and ships `supports_deferrable: true` in 
`/api/modules.json`, while passing `deferrable=True` frees no worker slot at 
all. An inherited knob whose only reader has been overridden is dead.
   
   It misses in the other direction too, which is what makes this a limit of 
the proxy rather than a patchable edge case. An operator that defers 
*unconditionally* has no flag to expose, so there is no param to find. 
`DateTimeSensorAsync` is the clearest: its docstring is "Deferring itself to 
avoid taking up a worker slot", its `execute` body is a bare `self.defer(...)`, 
and `deferrable` appears nowhere in the file (`sensors/date_time.py:83-143`, 
registered at `standard/provider.yaml:102`). I ran the predicate against 
`DateTimeSensorAsync`, `MSGraphAsyncOperator`, `AwaitMessageSensor`, 
`SSHRemoteJobOperator` and `HITLOperator`: all five return False while each 
one's own `execute` or `poke` calls `self.defer(`. So "Deferrable only" hides 
exactly the always-deferrable operators, and "Requires deferrable=True" on the 
:421 tooltip is unfollowable for them since there is no such param to set. A 
sweep suggests a handful of other always-deferring classes worth a spot check, 
though I only co
 nfirmed these five end to end.
   
   `is_durable_capable` right above already takes the opposite position on 
inheritance, and says why: the marker is deliberately name-mangled so it is 
*not* inherited, because a subclass that overrides `execute()` "may not 
preserve the parent's task_state_store reconnect behavior, so the declaration 
must not be inherited". That reasoning applies verbatim here, and this 
predicate is the neighbour that ignores it.
   
   I don't have a clean bound to propose. I tried the three that look obvious 
and each has a named counter-example in the tree, so this is probably worth 
deciding deliberately rather than patching:
   
   - Grep the class's own `execute` for `self.defer(`, the way 
`is_durable_capable` greps for `execute_resumable`: false-negatives 
`TriggerDagRunOperator`. Its `execute` is `trigger_dagrun.py:233-302` and 
delegates, so the `if self.deferrable: self.defer(` sits in `_trigger_dag_af_2` 
at `:396-397`, one call away.
   - Grep the whole class body instead: false-negatives `TimeDeltaSensorAsync` 
(`sensors/time_delta.py:154-167`, registered at `provider.yaml:103`), which 
inherits the param *and* the deferring `execute` from `TimeDeltaSensor`, so its 
own body contains neither `self.defer(` nor `self.deferrable`.
   - Exclude classes that override `execute` at all: drops `HttpOperator` 
itself, which defines its own.
   
   A per-class opt-in marker mirroring `__supports_durable_execution` would be 
consistent with the durable path and immune to all three, at the cost of 
provider-side annotation. Whichever way you go, the Discord shape is worth a 
regression test: forwards `**kwargs` to a deferrable parent, overrides 
`execute` synchronously, must classify as non-deferrable.



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