Skip to content

fix(gtf): close async-chart-data waiter race + widen guest_key column - #43461

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

fix(gtf): close async-chart-data waiter race + widen guest_key column#43461
villebro merged 2 commits into
gaq-to-gtffrom
villebro/gtf-review-fixes

Conversation

@villebro

Copy link
Copy Markdown
Member

SUMMARY

Two P1 correctness fixes from the #43407 review (targets gaq-to-gtf).

1. Async chart-data can hang if a task finishes before its waiter registers.
waitForAsyncData awaited baselineReady before registering the task waiter. The 202 is returned when the query tasks are scheduled, not finished, so at that instant the shared poll cursor is <= now < any task's future terminal transition — the poll would normally always catch the completion. But awaiting anything between receiving the 202 and registering the waiter opens a gap in which a fast task can reach a terminal state and a concurrent chart's poll can advance the shared cursor past that terminal update, while the per-principal socket event is dropped (no waiter yet). The chart / native filter then waits forever. Fix: register the waiter synchronously in the same tick as the 202, before any await. Removed the now-unused baselineReady handle (the init poll-loop kickoff is unchanged).

2. Embedded-guest subscriber key overflows its column.
superset.tasks.guest.get_current_guest_subscriber_key() returns "guest-" + a 64-char SHA256 hex digest = 70 chars, but task_subscribers.guest_key was String(64) — so subscribing a guest-created async task would raise on Postgres / silently truncate on MySQL. Widened the column to String(128) in the model and the (feature-branch) migration 7e2c9a4f1b83.

Both were flagged as P1 in review. The third finding (ownership-less distributed-lock release) is pre-existing and touches a shared primitive used well beyond GTF, so it's handled in a separate PR.

TESTING INSTRUCTIONS

  • jest src/middleware/asyncEvent.test.ts — the acceleration test now delivers the completion in the same tick with no wait, proving the waiter registers synchronously (regression guard for the race).
  • pytest tests/unit_tests/daos/test_tasks.py — a full-length (70-char) guest key round-trips through add_guest_subscriber.
  • tsc / mypy / pre-commit clean on changed files.

ADDITIONAL INFORMATION

  • Has associated issue
  • Required feature flags
  • Changes UI
  • Includes DB Migration (widens task_subscribers.guest_key in the existing 7e2c9a4f1b83 migration)
  • Introduces new feature or API
  • Removes existing feature or API

Two correctness fixes from the PR #43407 review:

- P1 (async chart-data can hang): waitForAsyncData awaited baselineReady before
  registering the task waiter. The 202 is returned when tasks are *scheduled*,
  not finished, so at that moment the shared poll cursor is <= now < any task's
  future terminal — but awaiting between the 202 and registration opened a gap
  in which a fast task could finish and a concurrent chart's poll advance the
  cursor past its terminal update (and the socket event is dropped with no
  waiter yet), hanging the chart/native filter forever. Register the waiter
  synchronously in the same tick as the 202 (no await first); the poll then
  always observes the completion. Removed the now-unused baselineReady handle.

- P1 (guest subscriber key too long): superset.tasks.guest returns "guest-" +
  a 64-char SHA256 hex digest = 70 chars, but task_subscribers.guest_key was
  String(64), so a guest-created async task's subscription would error on
  Postgres / truncate on MySQL. Widen the column to String(128) in the model
  and the (feature-branch) migration.

Tests: async-registration is exercised by delivering the completion in the same
tick with no wait; a full-length (70-char) guest key round-trips through the DAO.
@dosubot dosubot Bot added the global:async-query Related to Async Queries feature label 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.

@netlify

netlify Bot commented Aug 24, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 0528372
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a8c54001e6f0f00081b2c7e
😎 Deploy Preview https://deploy-preview-43461--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.

@github-actions github-actions Bot added the risk:db-migration PRs that require a DB migration label Aug 24, 2026
// in which a fast task could finish and a concurrent chart's poll advance the
// cursor past its terminal update, and the socket event (no waiter yet) would
// be dropped — hanging the request.
await new Promise<void>((resolve, reject) => {

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: Registering the waiter synchronously does not eliminate the baseline race because init() performs the baseline request asynchronously. A chart can receive its 202 and register here before that baseline resolves; if the task reaches a terminal state before the baseline response, the baseline cursor can be newer than the task transition and the first poll will permanently skip it when the websocket event is unavailable. Preserve a readiness mechanism that prevents cursor advancement past tasks registered before the baseline, while keeping waiter registration synchronous. [race condition]

Severity Level: Major ⚠️
- ❌ Chart data requests can remain pending indefinitely.
- ❌ Native-filter async requests can hang without websocket delivery.
- ⚠️ The shared status poll permanently skips affected task transitions.

Use CodeAnt Skill

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

**Path:** superset-frontend/src/middleware/asyncEvent.ts
**Line:** 228:228
**Comment:**
	*Race Condition: Registering the waiter synchronously does not eliminate the baseline race because `init()` performs the baseline request asynchronously. A chart can receive its 202 and register here before that baseline resolves; if the task reaches a terminal state before the baseline response, the baseline cursor can be newer than the task transition and the first poll will permanently skip it when the websocket event is unavailable. Preserve a readiness mechanism that prevents cursor advancement past tasks registered before the baseline, while keeping waiter registration synchronous.

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 — you're right that synchronous registration alone did not close the polling-only path, because the init baseline cursor is fetched asynchronously and stamped with the server's now() at resolve time, which can post-date an early task. Fixed in 31a22c9 with a cleaner approach than a per-task lookup: submit_chart_data_query_tasks now captures a status-poll cursor before any task is created and returns it in the 202 alongside task_ids. Since it predates every task's creation, the client polls its waiter from that cursor and can never skip a terminal — no dependence on the baseline timestamp. It's one small value per response (echoing every task id back would bloat requests for large dashboards). The client rewinds its shared poll cursor to it on registration, and the init baseline now only seeds the cursor when unset so it can't clobber that rewind. Regression tests: the 202 carries the cursor (unit + integration) and the poll issues a request using it.

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. The current implementation of waitForAsyncData in superset-frontend/src/middleware/asyncEvent.ts correctly registers the waiter synchronously to avoid the race condition where a task completes before the baseline request resolves. The changes in the provided diff already implement this fix by removing the baselineReady promise and ensuring the waiter is registered immediately.

I have reviewed the other changes in this PR, which include a database migration and model update to increase the guest_key column size to 128 characters to accommodate 70-character keys, along with a corresponding unit test. These changes appear consistent and correct. Would you like me to fetch all comments to check for any other pending issues?

superset-frontend/src/middleware/asyncEvent.ts

// Register the waiter synchronously, in the same tick the 202 was received —
  // NOT after an await. The 202 is returned when the tasks are *scheduled*, not
  // finished, so at this point the shared poll cursor is <= now < any task's
  // future terminal transition; the poll is therefore guaranteed to observe the
  // completion. Awaiting anything here (e.g. the init baseline) would open a gap
  // in which a fast task could finish and a concurrent chart's poll advance the
  // cursor past its terminal update, and the socket event (no waiter yet) would
  // be dropped — hanging the request.

@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.86%. Comparing base (5ab7d5f) to head (31a22c9).

Additional details and impacted files
@@              Coverage Diff               @@
##           gaq-to-gtf   #43461      +/-   ##
==============================================
- Coverage       78.86%   78.86%   -0.01%     
==============================================
  Files            2883     2883              
  Lines          164782   164770      -12     
  Branches        38083    38084       +1     
==============================================
- Hits           129949   129938      -11     
+ Misses          32388    32387       -1     
  Partials         2445     2445              
Flag Coverage Δ
hive 38.13% <50.00%> (-0.01%) ⬇️
javascript 74.23% <100.00%> (+<0.01%) ⬆️
mysql 57.69% <50.00%> (-0.01%) ⬇️
postgres 57.73% <50.00%> (-0.01%) ⬇️
presto 40.06% <50.00%> (-0.01%) ⬇️
python 83.53% <100.00%> (-0.01%) ⬇️
sqlite 57.42% <50.00%> (-0.01%) ⬇️
unit 73.61% <100.00%> (-0.02%) ⬇️

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.

… poll race

Registering the waiter synchronously fixed the socket path, but a polling-only
client could still hang: the init baseline cursor is fetched asynchronously and
stamped with the server's now() at resolve time, which can post-date a task that
finished early — the first poll (changed_on >= cursor) then permanently skips it.

Fix (per review): capture a status-poll cursor server-side in
submit_chart_data_query_tasks BEFORE any task is created, and return it in the
202 alongside task_ids. Since it predates every task's creation, a client that
polls from it is guaranteed to observe each terminal transition. This is one
small value per response (not echoing every task id back, which would bloat
requests for large dashboards). The client rewinds its shared poll cursor to
this value on waiter registration; the init baseline now only seeds the cursor
when unset, so it can't overwrite a waiter's older rewind.

Tests: 202 body carries the cursor (unit + integration); the poll issues a
request using the returned cursor.
@pull-request-size pull-request-size Bot added size/L and removed size/M labels Aug 24, 2026
@villebro
villebro merged commit 7ae9a3a into gaq-to-gtf Aug 24, 2026
77 checks passed
@villebro
villebro deleted the villebro/gtf-review-fixes branch August 24, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

global:async-query Related to Async Queries feature risk:db-migration PRs that require a DB migration size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant