rusackas opened a new pull request, #44552:
URL: https://github.com/apache/superset/pull/44552
### SUMMARY
Adopted from #36350 by @X-arshiya-X, with @Vab-170, @ssadras and
@Infernashwin. That PR could not be rebased in place — its head branch lives on
an org-owned fork with `maintainerCanModify: false`, which GitHub does not let
maintainers override — and it had drifted ~1,300 commits behind `master`. The
idea is theirs; this re-derives it against current `master`, which has grown
machinery that did not exist when it was written. Original authors credited as
co-authors on the commit.
Refs #35347 — see **Engine coverage** below; this fixes the reported
behavior for most engines but *not yet* for Trino, so I've deliberately not
written `Fixes`.
**The bug.** Clicking **Stop** on a running chart query in Explore aborts
the in-flight HTTP request, but the database keeps executing the query. A heavy
chart query keeps burning compute long after nobody is waiting for the result.
**The fix.** Each chart run now carries a client-generated `client_id`.
While the query runs, the existing chart-data cancellation seam in
`superset/tasks/query_cancel.py` captures the engine cancel id off the live
cursor and publishes it to the cache. A new `POST /api/v1/chart/data/stop`
resolves that handle and kills the backend session over a fresh connection —
the same `db_engine_spec.cancel_query` contract SQL Lab uses.
That seam already existed: it was built for the async (GTF) path, where a
task abort needs to kill the warehouse query. It was only ever wired to Celery
tasks. All this PR does is let the *synchronous* Explore path reach it, so the
Stop button gets the cancellation that async queries already had.
### Engine coverage — please read before approving
The capture happens on the seam that runs *before* the blocking execute, so
only engines that can produce a cancel handle at that point participate:
| | Engines | Stop now cancels? |
|---|---|---|
| `get_cancel_query_id` before execute | Postgres, MySQL, Redshift,
Snowflake, SingleStore, Ocient | **Yes** |
| id only available *during* execution | Trino, Presto, Hive, Impala | Not
yet |
So the engine named in #35347 (Trino) is **not** covered by this PR. Trino's
query id only appears on the cursor once execution has started, and it is
captured by `handle_cursor`, which only runs under `execute_with_cursor` — a
threaded poll that `Database._execute_sql_with_mutation_and_logging` does not
currently use (it calls `db_engine_spec.execute` directly). Covering Trino
means routing chart execution through `execute_with_cursor` when a cancel sink
is active, and publishing the handle from the callback as soon as
`handle_cursor` sets it.
I left that out on purpose: it changes the code path that executes *every*
chart query in Superset, and I had no Trino instance to validate the threaded
path against. It is a well-defined follow-up rather than something to guess at
here. (#36350 did attempt this, in `models/core.py`.) Happy to do it as a
follow-up PR, or fold it in here if you'd rather it land together — say which
you prefer.
### Notes for reviewers
**Authorization.** Cancelling someone else's query is a real authorization
surface, and `client_id` is untrusted, client-supplied input. The cache key
embeds the requesting user's id (`chart-query-cancel:{user_id}:{client_id}`)
rather than storing the owner as a field compared after lookup, so a
`client_id` is only ever resolvable inside the namespace of the user who
registered it. Passing another user's `client_id` simply misses — it cannot
read, cancel, or overwrite their entry. There are dedicated regression tests
for both directions of that
(`test_cancel_chart_query_for_user_cannot_reach_another_users_query`,
`test_cancellable_chart_query_cannot_overwrite_another_users_handle`).
Anonymous requests register nothing at all: an anonymous viewer of a public
dashboard has no user id to scope a handle to, and an unscoped handle would be
cancellable by any other anonymous visitor. They keep the existing client-side
abort.
The endpoint maps to `can_read on Chart`, the same permission that running a
chart query needs — cancelling a query you started requires no privilege beyond
starting it, and it needs no permission migration. Deliberately *not* reusing
SQL Lab's `/api/v1/query/stop`, which is gated on `can_stop_query on Query`: a
Gamma user with chart access but no SQL Lab access would have been 403'd on
their own chart's Stop button.
**Divergence from #36350.** The original made chart queries cancellable by
inserting a real SQL Lab `Query` row per chart render, keyed by the
client-supplied `client_id`, and reusing `/api/v1/query/stop`. I did not carry
that forward, for three reasons:
1. `query.client_id` is `unique=True` **globally, across all users**, and
the original's reuse lookup (`filter_by(client_id=client_id).one_or_none()`)
was not scoped by user. A request naming another user's `client_id` would have
resolved to *their* `Query` row and then handed it to the execution path, where
`handle_cursor` overwrites `extra_json[cancel_query]` — clobbering a running
SQL Lab query's cancel handle, so its owner's Stop would cancel the wrong
query. That is the blocker this rewrite removes.
2. It wrote a metadata-DB row plus a commit for **every chart render**,
forever. The follow-on `Query.sql IS NOT NULL` filters it added to
`queries/filters.py` and `daos/query.py` existed only to hide those rows from
Query History.
3. It threaded a `query=` parameter through `get_df` → `ExploreMixin.query`
→ `get_query_result` → the `Explorable` protocol, which needed an
`inspect.signature` guard because `AnnotationDatasource.query()` has a
different arity.
Routing through `query_cancel.py` instead needs no `Query` rows, no
migration, no history filtering, and no signature threading — so
`models/core.py`, `models/helpers.py`, `explorables/base.py`,
`connectors/sqla/models.py`, `daos/query.py`, `queries/filters.py`,
`sql_lab.py` and `sql/execution/executor.py` are all untouched here. I also
dropped the original's broad `except Exception: return False` wrappers around
`sql_lab.cancel_query` / `executor._cancel_query`, which silently converted
engine errors into "could not cancel".
Also dropped as unrelated drift from the stale branch: `CLAUDE.md`,
`GEMINI.md`, `GPT.md`, `.github/copilot-instructions.md`,
`.storybook/main.mjs`, `number-format/README.md`,
`playwright/embedded-app/index.html`,
`playwright/generators/docs/screenshot-manifest.yaml`, and a `DatabaseSelector`
null-guard.
**Scope.** `client_id` is sent for dashboard chart renders too, so their
queries register handles; there is just no per-chart Stop control in the
dashboard UI today to call the endpoint. Engines that cannot produce a cancel
id before execution capture nothing and stay non-cancellable, exactly as
before. Async (GTF) chart queries keep using their own abort path.
The one-line annotation in `superset/charts/api.py` widens
`openapi_spec_component_schemas` to `tuple[type[Schema], ...]` — matching how
`BaseSupersetApi` already declares it — so a subclass can extend it without
mypy flagging the override against an inferred fixed-length tuple.
### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
No visual change. The Stop button looks and behaves the same in the UI; what
changes is that the query actually dies in the database.
### TESTING INSTRUCTIONS
Automated:
```bash
pytest tests/unit_tests/tasks/test_query_cancel.py
tests/unit_tests/charts/test_chart_data_api.py
cd superset-frontend && npm run test --
src/components/Chart/chartReducers.test.ts
src/explore/exploreUtils/exploreUtils.test.tsx
```
Manual, against a covered engine (Postgres, MySQL, Redshift, Snowflake,
SingleStore, Ocient):
1. Point Superset at the database and open its query monitoring UI.
2. In Explore, build a chart whose query runs for a while (a heavy
aggregation, or `SELECT ... FROM huge_table` via a virtual dataset).
3. Run the chart and confirm the query shows as running in the database.
4. Click **Stop**.
5. The query should transition to cancelled/killed in the database, not keep
running to completion. (Use Postgres/MySQL/Snowflake/Redshift here — per
**Engine coverage** above, Trino and Presto are not covered yet.)
6. Regression check: SQL Lab's own Stop button still works unchanged.
To verify the authorization boundary by hand: note the `client_id` in a
running chart's `/api/v1/chart/data` request body, then as a *different* user
`POST /api/v1/chart/data/stop` with that `client_id`. It returns `{"result":
{"stopped": false}}` and the first user's query keeps running.
### ADDITIONAL INFORMATION
- [x] Has associated issue: #35347 (partially — see Engine coverage)
- [ ] Required feature flags:
- [x] Changes UI
- [ ] Includes DB Migration (follow approval process in
[SIP-59](https://github.com/apache/superset/issues/13351))
- [ ] Migration is atomic, supports rollback & is backwards-compatible
- [ ] Confirm DB migration upgrade and downgrade tested
- [ ] Runtime estimates and downtime expectations provided
- [x] Introduces new feature or API
- [ ] Removes existing feature or API
Supersedes #36350.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
--
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]