Skip to content

fix(gtf): async chart-data robustness + accurate DB errors; retire async_events - #43473

Merged
villebro merged 2 commits into
gaq-to-gtffrom
villebro/gtf-async-robustness
Aug 24, 2026
Merged

fix(gtf): async chart-data robustness + accurate DB errors; retire async_events#43473
villebro merged 2 commits into
gaq-to-gtffrom
villebro/gtf-async-robustness

Conversation

@villebro

Copy link
Copy Markdown
Member

SUMMARY

Targets gaq-to-gtf. Post-cutover robustness fixes for the GTF async chart-data
flow, addressing review findings on the merged child PRs, plus a small cleanup.

Async chart-data can no longer silently hang or throw when the result isn't cached.
Async delivery is cache-then-read-back, so a result that never lands in the DATA
cache (a NullCache backend, an oversized value, or a per-query disabled timeout)
would previously leave the client re-requesting an uncacheable result — a repeat
202 the refetch handler couldn't handle. Now:

  • /chart/data refuses async under a NullCache DATA backend and runs
    synchronously (the 202→loop can't even start); and
  • if a post-completion re-request still returns 202, the client falls back to a
    synchronous fetch
    (which returns the payload inline) instead of throwing.

Distributed-lock release is now atomic. The ownership-checked release did a
separate GET then DEL; if the lock expired and was re-acquired between them, the
stale holder could delete the new holder's lock. Replaced with a single Lua
compare-and-delete on the coordination backend.

A dependent task cancelled while waiting on prerequisites no longer publishes a
phantom FAILURE.
execute_task now only publishes FAILURE when the conditional
transition actually committed; if the task was concurrently aborted, it reports the
status that actually landed.

Accurate DB error surfacing. A transient metadata-DB error during a task
create/update is surfaced with its true cause (e.g. "database is locked") instead of
the generic "Task could not be created/updated." (opt-in preserve_message on the
shared on_error).

Cleanup — retire the async_events package. The GAQ cutover deleted everything
in it except cache_backend.py, whose classes are now the coordination service's
Redis client wrappers (not async-events/GAQ-specific). Moved to
superset/coordination/cache_backend.py and deleted the package.

TESTING INSTRUCTIONS

Unit tests cover each fix:

  • pytest tests/unit_tests/charts/test_chart_data_api.py -k should_run_async — async
    refused under NullCache.
  • pytest tests/unit_tests/distributed_lock/distributed_lock_tests.py — atomic
    compare-and-delete release; a stale token doesn't drop a newer holder's lock.
  • pytest tests/unit_tests/coordination/test_cache_backend.pycompare_and_delete
    delegates to the Lua script.
  • pytest tests/unit_tests/tasks/test_dependencies.py — a cancelled dependent reports
    the committed status, not a phantom FAILURE.
  • pytest tests/unit_tests/utils/test_decorators.pypreserve_message surfaces the
    DBAPI cause.
  • npm run test -- src/components/Chart/chartActions.test.ts — client sync fallback on
    a repeat 202.

End-to-end: with GLOBAL_ASYNC_QUERIES=on, a persistent DATA_CACHE_CONFIG, and a
Celery worker, load a dashboard and confirm charts resolve; point DATA_CACHE_CONFIG
at NullCache and confirm requests run synchronously (no tasks scheduled).

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags: GLOBAL_ASYNC_QUERIES (async path only)
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

…async_events

Bundles the post-cutover review follow-ups:

- Surface the true DB error on task create/update. on_error gains an opt-in
  preserve_message that carries the DBAPI cause (e.g. "database is locked")
  into TaskCreateFailedError / TaskUpdateFailedError instead of the generic
  message, so a transient contention failure is no longer reported as a flat
  "Task could not be created."

- Don't run async when the result can't be read back. /chart/data refuses async
  under a NullCache DATA backend (falls back to sync 200), and the client falls
  back to a synchronous fetch if a post-completion re-request still returns 202
  (oversized result / per-query disabled timeout), instead of looping/throwing.

- Ownership-checked lock release is now atomic. A Lua compare-and-delete on the
  coordination backend replaces the get-then-delete in _release_redis, closing
  the window where an expired-then-reacquired lock could be dropped.

- A dependent cancelled while waiting on prerequisites no longer publishes a
  FAILURE that never committed: execute_task only publishes FAILURE when the
  conditional transition actually landed, else reports the committed status.

- Retire the vestigial async_events package: its only survivor, cache_backend
  (the coordination service's Redis client wrappers), moves to
  superset/coordination/cache_backend.py and the package is deleted.
@dosubot dosubot Bot added change:backend Requires changing the backend global:async-query Related to Async Queries feature labels Aug 24, 2026
@bito-code-review

bito-code-review Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped - Branch Excluded

Bito didn't auto-review because the source or target branch is excluded from automatic reviews.
No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.
You can change the branch exclusion settings here, or contact your Bito workspace admin at evan@preset.io.

@github-actions github-actions Bot added the api Related to the REST API label Aug 24, 2026
@villebro

Copy link
Copy Markdown
Member Author

/review

@netlify

netlify Bot commented Aug 24, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit a4a4ce7
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a8c7c65cf107f000818fb6b
😎 Deploy Preview https://deploy-preview-43473--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@bito-code-review

bito-code-review Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #8e9ef6

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset-frontend/src/components/Chart/chartAction.ts - 1
    • Missing sync fallback in DrillByModal · Line 751-751
      `DrillByModal.tsx` implements the same async/202 pattern as `chartAction.ts` but lacks the sync-fallback guard added at lines 770–785. When async tasks complete but the result is not cached (oversized value or per-query timeout disabled), the refetch inside `handleChartDataResponse` calls `requestDrillData()` again with `enableAsyncMode: true`, which returns another 202 — causing an infinite loop or a thrown error. The two call sites (lines 405 and 504) need the same conditional fallback that this diff introduces in `chartAction.ts`.
  • superset-frontend/src/components/Chart/chartActions.test.ts - 1
    • Test assertions incomplete · Line 614-617
      The test verifies the chart succeeds but does not assert the actual payload data ({ data: [1, 2, 3] }) or the queriesResponse passed to the action. Adding these assertions ensures the fallback path returns the correct data and makes the test more robust against regression.
Review Details
  • Files reviewed - 17 · Commit Range: a4a4ce7..a4a4ce7
    • superset-frontend/src/components/Chart/chartAction.ts
    • superset-frontend/src/components/Chart/chartActions.test.ts
    • superset/async_events/__init__.py
    • superset/charts/data/api.py
    • superset/commands/distributed_lock/release.py
    • superset/commands/tasks/internal_update.py
    • superset/commands/tasks/submit.py
    • superset/coordination/base.py
    • superset/coordination/types.py
    • superset/tasks/scheduler.py
    • superset/utils/cache_manager.py
    • superset/utils/decorators.py
    • tests/unit_tests/async_events/__init__.py
    • tests/unit_tests/charts/test_chart_data_api.py
    • tests/unit_tests/distributed_lock/distributed_lock_tests.py
    • tests/unit_tests/tasks/test_dependencies.py
    • tests/unit_tests/utils/test_decorators.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment thread superset/utils/decorators.py Outdated
Comment on lines +249 to +250
cause = getattr(ex, "orig", None) or ex
raise reraise(str(cause), ex) from ex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The new preserve_message path copies the raw DBAPI exception text into the SupersetException message. Because task create/update exceptions serialize this message through their API error response, backend-specific errors can disclose schema, table, connection, or other database details to clients. Preserve only a sanitized/allowlisted operational message, or keep the detailed cause in server-side logs while returning the generic task error externally. [security]

Severity Level: Major ⚠️
- ⚠️ GTF task creation errors can reveal backend database details.
- ⚠️ Chart/task API clients may receive driver-specific exception text.
- ⚠️ Error messages can expose SQL, schema, host, or table metadata.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/utils/decorators.py
**Line:** 249:250
**Comment:**
	*Security: The new `preserve_message` path copies the raw DBAPI exception text into the `SupersetException` message. Because task create/update exceptions serialize this message through their API error response, backend-specific errors can disclose schema, table, connection, or other database details to clients. Preserve only a sanitized/allowlisted operational message, or keep the detailed cause in server-side logs while returning the generic task error externally.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in e63a05f by removing the preserve_message path entirely. Task create/update now reraise the generic TaskCreateFailedError / TaskUpdateFailedError to the client; the detailed DBAPI cause stays server-side (Celery task traceback / request log), which is exactly the sanitized-external / detailed-in-logs split you recommend.

Context: preserve_message was added to surface SQLite's "database is locked" during local dev. We've since moved local dev to Postgres and reverted the SQLite-lock workarounds, so its motivation is gone — dropping it is the right call on both counts.

@bito-code-review

Copy link
Copy Markdown
Contributor

The security concern regarding the exposure of backend-specific database details in SupersetException messages is valid. To resolve this, you should sanitize the exception message before it is passed to the SupersetException constructor, ensuring that only generic, safe information is returned to the client while logging the detailed cause server-side.

In superset/utils/decorators.py, you can modify the on_error function to handle the exception message more securely:

# Example of sanitizing the exception message
if preserve_message:
    # Log the full exception for debugging
    logger.exception("Task operation failed: %s", str(source))
    # Return a sanitized message to the client
    message = "An internal error occurred while processing the task."
    # ... proceed with raising the exception using the sanitized message

Would you like me to fetch all other comments on this PR to validate them and implement a minimal fix for the rest as well?

superset/utils/decorators.py

# Example of sanitizing the exception message
if preserve_message:
    # Log the full exception for debugging
    logger.exception("Task operation failed: %s", str(source))
    # Return a sanitized message to the client
    message = "An internal error occurred while processing the task."
    # ... proceed with raising the exception using the sanitized message

requestChartData(true).then(({ response: r, json: j }) =>
handleChartDataResponse(r, j),
) as Promise<QueryData[]>,
requestChartData(true).then(({ response: r, json: j }) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The cache-read retry still sends enableAsyncMode: true; when the completed result is not in the cache, the server submits another background task and returns 202 rather than merely probing the cache. The subsequent synchronous fallback therefore causes every uncacheable completion to execute the query twice and leaves the duplicate task running. Avoid scheduling another async task during the retry, or go directly to a synchronous request after task completion. [performance]

Severity Level: Major ⚠️
- ⚠️ Uncacheable chart results execute duplicate background tasks.
- ⚠️ Extra GTF work increases query and worker load.
- ⚠️ Duplicate tasks can compete for database resources.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/components/Chart/chartAction.ts
**Line:** 772:772
**Comment:**
	*Performance: The cache-read retry still sends `enableAsyncMode: true`; when the completed result is not in the cache, the server submits another background task and returns 202 rather than merely probing the cache. The subsequent synchronous fallback therefore causes every uncacheable completion to execute the query twice and leaves the duplicate task running. Avoid scheduling another async task during the retry, or go directly to a synchronous request after task completion.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e63a05f. The post-completion re-issue now runs synchronously (enableAsyncMode: false), so it never schedules a second background task — it reads the warm per-query cache, or computes inline once if the result wasn't cached. The repeat-202 branch (and its duplicate-task path) is gone entirely.

Added a regression test asserting the re-issue carries no async_mode in its body and that exactly two requests are made (async submit + one sync re-issue, no third call).

// skips the write). Fall back to a synchronous fetch, which returns
// the payload inline, instead of looping on an uncacheable request.
if (r.status === 202) {
return requestChartData(true, true).then(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The synchronous fallback changes only the async mode and keeps force disabled through requestChartData(true, true). If the original request used force: true and an older result is already cached while the newly completed task failed to write its result, this fallback can return that older cached result instead of the result requested by the user. Preserve the original force semantics for the fallback, or explicitly bypass the cache. [logic error]

Severity Level: Major ⚠️
- ⚠️ Forced chart refreshes can display stale DATA-cache results.
- ⚠️ Dashboard refreshes may show outdated query results.
- ⚠️ User-requested cache bypass semantics are lost.

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/src/components/Chart/chartAction.ts
**Line:** 779:779
**Comment:**
	*Logic Error: The synchronous fallback changes only the async mode and keeps `force` disabled through `requestChartData(true, true)`. If the original request used `force: true` and an older result is already cached while the newly completed task failed to write its result, this fallback can return that older cached result instead of the result requested by the user. Preserve the original force semantics for the fallback, or explicitly bypass the cache.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e63a05f. The re-issue now uses the caller's original force (no longer forced to false), so a forced refresh bypasses any stale cached entry instead of returning it. Combined with the sync change above, the post-completion request is a single synchronous call that preserves force — reading the warm cache on a normal request, or recomputing fresh when the user forced a refresh.

The regression test drives a force: true request and asserts the re-issue's URL carries force=true.

…rving re-issue

Review follow-ups on #43473:

- Security: remove the on_error `preserve_message` path. It copied the raw DBAPI
  exception text into the task create/update error, which serializes to API
  clients and could disclose schema/table/connection details. Revert to the
  generic task error; the detailed cause stays in server logs (Celery traceback
  / request log). Its original motivation (surfacing SQLite "database is locked")
  is moot now that local dev uses Postgres.

- Performance + correctness: the post-completion chart-data re-issue now runs
  synchronously and preserves the caller's `force`. Previously the re-issue ran
  async again, so an uncacheable result scheduled a *second* background task
  before falling back; and it dropped `force`, so a forced refresh could return a
  stale cached entry. A single synchronous re-issue reads the warm per-query
  cache (or computes once), never schedules a duplicate task, and — with `force`
  preserved — never serves stale data on a forced refresh. Removes the repeat-202
  branch entirely.
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.00000% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (gaq-to-gtf@29af4ec). Learn more about missing BASE report.

Files with missing lines Patch % Lines
superset/coordination/base.py 66.66% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             gaq-to-gtf   #43473   +/-   ##
=============================================
  Coverage              ?   78.86%           
=============================================
  Files                 ?     2883           
  Lines                 ?   164809           
  Branches              ?    38090           
=============================================
  Hits                  ?   129975           
  Misses                ?    32388           
  Partials              ?     2446           
Flag Coverage Δ
hive 38.13% <42.10%> (?)
javascript 74.23% <100.00%> (?)
mysql 57.69% <42.10%> (?)
postgres 57.72% <42.10%> (?)
presto 40.06% <42.10%> (?)
python 83.53% <94.73%> (?)
sqlite 57.41% <42.10%> (?)
unit 73.63% <94.73%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@villebro
villebro merged commit 33cf0c4 into gaq-to-gtf Aug 24, 2026
97 of 99 checks passed
@villebro
villebro deleted the villebro/gtf-async-robustness branch August 24, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API change:backend Requires changing the backend global:async-query Related to Async Queries feature size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant