villebro opened a new pull request, #43768:
URL: https://github.com/apache/superset/pull/43768

   ### SUMMARY
   
   Review-driven hardening of the GAQ→GTF epic, folded back onto `gaq-to-gtf`. 
This
   branch implements fixes for **12 findings** surfaced by a fresh end-to-end 
review
   (five parallel subsystem passes: coordination/locks, GTF task lifecycle, 
async
   chart-data, realtime/websocket, frontend) plus an independent external 
review. Each
   finding was verified against the code before fixing; every fix ships with 
tests.
   
   Both reviews independently concluded there is **no in-scope security boundary
   violation** (per `SECURITY.md`): the model remains "an authorized route 
creates a
   task, the worker runs as the initiating principal, and task/list/result 
state is
   fetched through protected APIs; websocket messages are nudges or targeted 
terminal
   status, not authority."
   
   #### Fixes (grouped by severity)
   
   **HIGH**
   - **Async worker rebuilt `g.form_data` with the wrong shape.** The worker set
     `g.form_data` to the top-level slice form-data (no `queries`), but
     `get_form_data()`'s no-request-context fallback and the Jinja macros
     (`filter_values`/`get_filters`/`url_param`) read query-level filters from
     `form_data["queries"][0]`. For a templated dataset this rendered empty 
filters in
     the worker, so it cached the wrong SQL under a `query_cache_key` that 
**diverged
     from the submit-time `task_key`** → a reschedule loop. Now the worker 
reconstructs
     a body-shaped `g.form_data` via the canonical
     `set_query_context_form_data(query_context, …)`. (Regressed the fix 
claimed in
     step #43701; the previous test masked it with a body-shaped dict 
`serialize_query`
     never emits — replaced with a real round-trip assertion.)
   - **Concurrent forced refresh re-ran synchronously on a web worker.** The 
force
     idempotency marker was keyed by a **client-minted** random nonce, so a 
second
     concurrent forced refresh (new nonce) joined the shared task, then forced 
its own
     synchronous read-back and recomputed the query in-process. The nonce is 
now the
     **task's own UUID** — server-assigned, already returned in the 202 
`task_ids`. The
     worker stamps each query with its task UUID and records the marker; the 
client
     threads each query's task id (index-aligned) as its per-query 
`force_nonce` on the
     synchronous read-back. Because the token is the task's identity, a 
concurrent
     refresh joining the same SHARED task reads back under the same id and does 
not
     re-execute. Drops client-side `nanoid` and the submit-path nonce threading.
   
   **Important**
   - **Cleanup-handler failure could flip a committed `SUCCESS` → `FAILURE`.** 
Cleanup
     runs in the executor's `finally`, *after* the `SUCCESS` commit; the write 
used
     `UpdateTaskCommand.set_status` with no compare-and-swap. It now routes 
through a
     conditional `InternalStatusTransitionCommand` (no-op on a terminal task) 
and, when
     the task already succeeded, records the cleanup-failure detail via a
     properties-only update — preserving `SUCCESS`.
   - **Timeout/self-fence gated on a stale session-bound entity.** 
`_abort_locally`
     read `self._task.properties_dict` (a construction-time snapshot) from a 
background
     timer/fence thread; `is_abortable` is written to the in-memory 
`_properties_cache`
     during execution. It could skip the abort (stranding the task 
`IN_PROGRESS`) or
     trigger a cross-thread session reload. Now gates on `_properties_cache`.
   - **Terminal `FAILURE` wiped the whole properties column.** 
`conditional_status_update`
     replaces the JSON column, and the failure callers passed only 
`{error_message}`,
     dropping runtime fields and the structured `error_update(ex)` debug detail
     (exception class + traceback). Failure writes now merge the executor's 
property
     cache with `error_update(ex)` via a new pure `merge_properties` helper 
(also used
     by `Task.update_properties`, DRY).
   - **GAQ could be enabled without GTF via dynamic flag callbacks.** The "GAQ 
implies
     GTF" auto-enable only mutated the static flag map at `init_app`; an
     `IS_FEATURE_ENABLED_FUNC`/`GET_FEATURE_FLAGS_FUNC` could resolve 
GAQ=on/GTF=off, so
     `.schedule()` raised instead of degrading. The rule is now enforced at
     flag-resolution time, covering the callback paths.
   
   **Medium**
   - **DAG dependencies were ignored on the sync/inline path.** 
`_execute_inline` ran
     immediately regardless of `depends_on`; only the Celery path enforced the 
gate. The
     pure DAG *decision* (`unmet_prerequisite`) and *fail action* are extracted 
into a
     shared `superset/tasks/dependencies.py` that both paths call — fully DRY; 
only the
     *wait* action differs (async defers via `self.retry`; sync blocks on the 
existing
     `TaskManager.wait_for_completion`).
   - **Websocket-enabled-but-unreachable hung charts for the full give-up 
window.** With
     `WEBSOCKET_ENABLE` on, the socket is the sole completion transport and a
     deployed-but-down server left charts spinning ~10 min. The socket is now
     self-healing and observable: exponential backoff + jitter with a cap (was 
a fixed
     5s loop), a reconnect-attempt counter, and a new connection-state signal
     (`connecting`/`open`/`reconnecting`/`unhealthy`). On each `reconnecting` 
transition
     the client runs the socket-independent `status_changes` catch-up (so a 
completion
     during an outage is still observed); on `unhealthy` it settles pending 
waiters with
     a prompt, bounded error. No interval poll is reintroduced. A synchronous
     `WebSocket` constructor failure now schedules a reconnect instead of 
dead-ending.
   - **Coordination listener/waiter resilience + KV lock-release atomicity.** 
(a) The
     baseline `stream_last_id` capture is guarded so a transient backend error 
at
     startup degrades to reading from `0-0` instead of killing the daemon / 
aborting a
     lock acquisition. (b) A transient `check()`/`on_signal()` error inside the 
listen
     loop retries with backoff instead of permanently terminating the one-shot 
listener
     (which would drop the awaited cancel/abort signal). (c) The KV 
distributed-lock
     release row-locks the entry (`SELECT … FOR UPDATE`) so the ownership check 
and
     delete are atomic against a concurrent expire+re-acquire — the KV 
equivalent of the
     Redis compare-and-delete.
   
   **Low (hygiene)**
   - Typed the task payload / `TaskPayloadPopover` (`Record<string, unknown>`) 
and the
     `TaskList` subscriber predicate (`TaskSubscriber`) — removing the last 
`any`s in
     the touched task-UI files.
   - Added a `CheckConstraint` enforcing the "exactly one of `user_id` / 
`guest_key`"
     invariant on `task_subscribers` (in the branch's own migration + the model
     `__table_args__`).
   
   #### Deliberately deferred (both LOW; riskier/heavier than their value)
   - Switching the `entity.changed` nudge id from the integer PK to the task 
UUID — it
     is cross-cutting to the entity-agnostic `useListViewResource` hook and the 
Task API
     row key, and the security review classed it a benign metadata 
side-channel, not an
     authorization bypass.
   - Making `notify`'s `xadd`+`expire` atomic — requires extending the 
coordination
     backend abstraction (Lua/pipeline); the residual leak is a single 
MAXLEN-trimmed
     stream entry.
   
   ### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
   
   N/A — backend correctness/resilience and typing changes, plus a 
websocket-client
   robustness change with no visual surface (a genuinely-down socket now 
surfaces a
   prompt chart error instead of a ~10-minute spinner).
   
   ### TESTING INSTRUCTIONS
   
   Automated (all green except one pre-existing, unrelated failure — see below):
   
   ```bash
   # Backend
   pytest tests/unit_tests/tasks/ tests/unit_tests/coordination/ \
     tests/unit_tests/distributed_lock/ \
     tests/unit_tests/common/test_query_context_processor.py \
     tests/unit_tests/common/test_query_serialization.py \
     tests/unit_tests/charts/test_chart_data_api.py \
     tests/unit_tests/feature_flag_test.py tests/unit_tests/daos/test_tasks.py
   
   # Frontend
   npm run test -- src/middleware/realtime.test.ts \
     src/middleware/asyncEvent.test.ts src/components/Chart/chartActions.test.ts
   ```
   
   Manual (end-to-end, with `GLOBAL_ASYNC_QUERIES` on and 
`DISTRIBUTED_COORDINATION_CONFIG` set):
   1. Load a dashboard with a **Jinja-templated dataset** (`{{ 
filter_values(...) }}` /
      `{{ get_filters(...) }}`) under `async_mode`; confirm charts resolve with 
the
      correct filtered SQL and there is **no reschedule loop**.
   2. **Double-force** a chart (two quick refreshes): confirm a single warehouse
      execution and both refreshes read back from cache.
   3. With `WEBSOCKET_ENABLE` on, **stop the websocket server** while a chart 
is loading:
      confirm it surfaces a prompt error rather than a ~10-minute hang; restart 
the
      server and confirm reconnect + reconcile.
   4. Cancel/timeout a running async chart on a cancellable engine (e.g. 
Postgres) and
      confirm the terminal task still carries the structured error detail.
   
   > **Pre-existing failure (not from this branch):**
   > 
`tests/unit_tests/tasks/test_deletion_retention.py::test_default_celery_config_registers_daily_purge`
   > fails on this branch **with these changes stashed** — it depends on a local
   > `superset_config.CeleryConfig` override, unrelated to this work.
   
   > **CI note:** the frontend type-check may fail locally on a **stale, 
gitignored**
   > `packages/superset-ui-core/lib/**/TableCollection/index.d.ts` (predates 
the branch's
   > `TableCollection` prop additions — same class as the Butterfly `TS6305` 
errors). The
   > source type-checks clean; a `npm run plugins:build` refreshes the 
artifact. oxlint /
   > oxfmt / backend hooks pass.
   
   ### ADDITIONAL INFORMATION
   
   - [ ] Has associated issue:
   - [x] Required feature flags: `GLOBAL_ASYNC_QUERIES` (auto-enables 
`GLOBAL_TASK_FRAMEWORK`); optional `WEBSOCKET_ENABLE` for realtime transport
   - [x] Changes UI (task payload/subscriber typing; websocket-client behavior 
— a down socket surfaces an error instead of spinning)
   - [x] Includes DB Migration (follow approval process in 
[SIP-59](https://github.com/apache/superset/issues/13351))
     - [x] Migration is atomic, supports rollback & is backwards-compatible — 
adds a `CheckConstraint` to the branch's existing (unreleased) 
`task_subscribers` migration; downgrade drops it
     - [ ] Confirm DB migration upgrade and downgrade tested
     - [ ] Runtime estimates and downtime expectations provided
   - [ ] Introduces new feature or API
   - [ ] Removes existing feature or API
   


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


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to