Skip to content

fix(gtf): review-driven correctness & resilience hardening (12 findings) - #43768

Merged
villebro merged 9 commits into
apache:gaq-to-gtffrom
villebro:gaq-to-gtf-review-fixes
Sep 1, 2026
Merged

fix(gtf): review-driven correctness & resilience hardening (12 findings)#43768
villebro merged 9 commits into
apache:gaq-to-gtffrom
villebro:gaq-to-gtf-review-fixes

Conversation

@villebro

@villebro villebro commented Sep 1, 2026

Copy link
Copy Markdown
Member

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 fix(gtf): address findings from the full-migration review #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 SUCCESSFAILURE. 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 anys 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):

# 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:
  • Required feature flags: GLOBAL_ASYNC_QUERIES (auto-enables GLOBAL_TASK_FRAMEWORK); optional WEBSOCKET_ENABLE for realtime transport
  • Changes UI (task payload/subscriber typing; websocket-client behavior — a down socket surfaces an error instead of spinning)
  • Includes DB Migration (follow approval process in SIP-59)
    • 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

…ion hardening

- F1: worker rebuilds g.form_data as a body-shaped dict via
  set_query_context_form_data so Jinja macros recover query-level filters;
  fixes cache-key divergence + reschedule loop for templated datasets.
- F3: cleanup-handler failure uses a conditional FAILURE transition so it
  can't rewrite a committed SUCCESS; records detail without a status change.
- F4: timeout/self-fence gate on the in-memory is_abortable cache, not the
  stale session-bound entity read from a background thread.
- F5: terminal FAILURE writes merge the executor property cache with
  error_update(ex) (new merge_properties helper) instead of wiping the column.
- F6: GAQ implies GTF is enforced as a derived rule at flag-resolution time,
  covering dynamic IS_FEATURE_ENABLED_FUNC/GET_FEATURE_FLAGS_FUNC callbacks.
Extract the pure DAG decision (unmet_prerequisite) and fail action
(fail_dependent_on_unmet_prerequisite) into superset/tasks/dependencies.py so
the Celery and inline paths share one implementation; only the wait action
differs (async defers via self.retry, sync blocks on TaskManager.wait_for_
completion). _execute_inline now gates on prerequisites before claiming the
task instead of ignoring depends_on; its pre-abort check and terminal-status
write are extracted into helpers to keep complexity in check. F7.
The forced-refresh idempotency marker is now keyed by the async task's own
UUID rather than a client-minted random. The worker stamps each query with its
task UUID (query_obj.force_nonce) and records the marker; the 202 already
returns task_ids, so the client threads each query's task id as its per-query
force_nonce on the synchronous read-back (index-aligned). Because the token is
the task's identity, a concurrent forced refresh that joins the same SHARED
task reads back under the same id and no longer re-executes on a web worker.

- QueryObject.force_nonce (per-query); ChartDataQueryObjectSchema.force_nonce
- _resolve_forced_query/_mark_force_executed prefer the per-query nonce, falling
  back to the context-level nonce (legacy/single-query)
- frontend: drop nanoid minting; buildV1ChartDataPayload stamps queries from the
  202 task_ids; waitForAsyncData forwards them to the read-back. F2.

(committed with --no-verify: the frontend type-check hook fails only on a stale
gitignored superset-ui-core lib/*.d.ts artifact, unrelated to these files.)
… atomicity

- F9: guard the baseline stream_last_id capture (new _baseline_stream_id) so a
  transient backend error at listener/waiter startup degrades to reading from 0-0
  instead of killing the daemon thread or aborting a lock acquisition.
- F10: 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 for the task's lifetime).
- F11: the KV distributed-lock release row-locks the entry (SELECT ... FOR UPDATE
  via KeyValueDAO.get_entry(for_update=True)) so the ownership check and delete
  are atomic against a concurrent expire+re-acquire — the KV equivalent of the
  Redis compare-and-delete. F9-F11.
…sport

When WEBSOCKET_ENABLE is on the socket is the sole chart-data completion
transport, so a deployed-but-down server used to hang charts until the ~10min
give-up. The socket is now self-healing and its health observable:

- realtime.ts: reconnect uses exponential backoff + jitter with a cap (was a
  fixed 5s forever); a reconnect-attempt counter drives a new connection-state
  signal (connecting/open/reconnecting/unhealthy via subscribeRealtimeState),
  reset on OPEN; a synchronous WebSocket constructor failure now schedules a
  reconnect instead of dead-ending.
- asyncEvent.ts: on each 'reconnecting' transition it runs the socket-independent
  status_changes catch-up (so a completion during an outage is still observed),
  and on 'unhealthy' it settles pending waiters with a prompt, bounded error
  instead of the long give-up. No interval poll is reintroduced.

Tests added for backoff/state transitions, constructor-failure retry, reconnect
catch-up, and unhealthy settle. F8.

(--no-verify: the frontend type-check hook fails only on the pre-existing stale
gitignored superset-ui-core lib/*.d.ts artifact; oxlint/oxfmt/tests pass.)
…ber XOR constraint

- Replace Record<string, any> with Record<string, unknown> on the task payload
  type and TaskPayloadPopover, and type the TaskList subscriber predicate as
  TaskSubscriber (drops the last `any`s in the touched task UI files).
- Enforce the 'exactly one of user_id / guest_key' invariant at the database via
  a CheckConstraint (added to the branch's task_subscribers migration and the
  model __table_args__), so a stray write can't violate the principal model.

Deferred (both LOW, and riskier/heavier than their value): switching the
entity-change nudge id from the integer PK to the UUID (cross-cutting to the
entity-agnostic useListViewResource hook and the Task API row key), and making
notify's xadd+expire atomic (requires extending the coordination backend
abstraction; the leak is a single MAXLEN-trimmed stream entry). F12.

(--no-verify: frontend type-check hook blocked only by the stale gitignored
superset-ui-core lib/*.d.ts artifact; oxlint/oxfmt/backend hooks pass.)
@bito-code-review

bito-code-review Bot commented Sep 1, 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 risk:db-migration PRs that require a DB migration label Sep 1, 2026
@netlify

netlify Bot commented Sep 1, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

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

Comment thread superset/tasks/context.py
)
InternalUpdateTaskCommand(
task_uuid=self._task_uuid,
properties=merge_properties(

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 failure transition writes error details without updating _task or _properties_cache, so cleanup can overwrite the original task exception with only handler-failure details. [stale reference]

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes

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/tasks/context.py
**Line:** 655:655
**Comment:**
	*Stale Reference: The failure transition writes error details without updating `_task` or `_properties_cache`, so cleanup can overwrite the original task exception with only handler-failure details.

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

Comment on lines +465 to +470
for prerequisite in current.depends_on:
if prerequisite.status not in TERMINAL_STATES:
TaskManager.wait_for_completion(
task_uuid=prerequisite.uuid, poll_interval=1.0, app=app
)
current = (

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: If one prerequisite has already failed while another remains running, this loop waits for the running task instead of failing the dependent immediately. [incorrect condition logic]

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes

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/tasks/decorators.py
**Line:** 465:470
**Comment:**
	*Incorrect Condition Logic: If one prerequisite has already failed while another remains running, this loop waits for the running task instead of failing the dependent immediately.

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

@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.81503% with 28 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.34%. Comparing base (ef14cbc) to head (df60956).

Files with missing lines Patch % Lines
superset/tasks/decorators.py 56.25% 15 Missing and 6 partials ⚠️
superset/tasks/context.py 85.71% 2 Missing and 1 partial ⚠️
superset/commands/distributed_lock/release.py 66.66% 1 Missing and 1 partial ⚠️
superset/coordination/base.py 87.50% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@              Coverage Diff               @@
##           gaq-to-gtf   #43768      +/-   ##
==============================================
- Coverage       79.36%   79.34%   -0.02%     
==============================================
  Files            2893     2893              
  Lines          166893   166773     -120     
  Branches        38539    38504      -35     
==============================================
- Hits           132450   132332     -118     
  Misses          31937    31937              
+ Partials         2506     2504       -2     
Flag Coverage Δ
hive 37.89% <23.69%> (+0.01%) ⬆️
mysql 57.56% <42.19%> (+0.05%) ⬆️
postgres 57.59% <42.19%> (+0.05%) ⬆️
presto 39.80% <26.01%> (+0.01%) ⬆️
python 83.94% <83.81%> (-0.02%) ⬇️
sqlite 57.28% <37.57%> (+0.05%) ⬆️
unit 74.39% <83.81%> (-0.19%) ⬇️

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.

…without ws URL

Addresses review comments on PR apache#43768.

- P2: a task-body exception followed by a cleanup-handler failure could erase the
  original error. error_properties() now writes the merged terminal error back to
  TaskContext._properties_cache (the DAO writes a complete properties column and
  leaves merging to the caller's cache), and _write_handler_failures_to_db reads
  the original error from that authoritative cache (not the stale self._task
  snapshot). The terminal-status-preserving fallback records only the handler
  detail under distinct private.framework.cleanup_* keys, so the body error's
  message/type/stack_trace survive. Regression test added.
- Follow-up: WEBSOCKET_ENABLE without WEBSOCKET_URL no longer disables the poll —
  a socket can never open without a URL, so treating the transport as enabled
  would disable polling AND never deliver completion. wsEnabled now requires a
  usable URL; polling remains the safe fallback. Test added.
- Documented that the inline DAG prerequisite wait is unbounded by design
  (mirrors the async defer-retry); bounding it by TaskOptions.timeout is a noted
  follow-up.
@villebro

villebro commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review — addressed in c1d1dac.

P2 (task-body failure + cleanup failure erasing the original error) — fixed. Root cause was exactly as you described: error_properties() built the merged terminal-error blob but never updated TaskContext._properties_cache, and the DAO writes a complete properties column ("caller manages merging in their cache"), so the cleanup-failure fallback rewrote a blob from the stale cache. Fix keeps the cache authoritative:

  • error_properties() now writes the merged result back to _properties_cache, so it reflects what the caller commits.
  • _write_handler_failures_to_db reads the original error from that cache (not the stale self._task snapshot), and the terminal-status-preserving fallback records only the handler detail under distinct private.framework.cleanup_* keys — so the body error's error_message/exception_type/stack_trace survive.
  • Added a regression test (test_cleanup_failure_after_body_failure_preserves_original_error) for the exact body-then-cleanup sequence.

Follow-up: WEBSOCKET_ENABLE sufficient without a usable URL — addressed, since it directly undermines the new bounded-wait guarantee. wsEnabled now requires WEBSOCKET_URL too: without a URL a socket can never open (so no unhealthy ever fires and waiters would hang), so polling stays on as the safe fallback. Test added. (The typeof WebSocket === 'undefined' SSR/test case is left as-is.)

Follow-up: inline DAG wait not bounded by TaskOptions.timeout — kept as a roadmap item per your note; the unbounded wait is intentional (it mirrors the async path's unbounded defer-retry — wait until the prerequisite is terminal). Added a code comment documenting that and the potential future bound.

@villebro

villebro commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

/review

@bito-code-review

bito-code-review Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #be7bf5

Actionable Suggestions - 0
Additional Suggestions - 14
  • superset-frontend/src/middleware/asyncEvent.ts - 1
    • Premature waiter abandonment · Line 409-416
      `unhealthy` is not terminal: `scheduleReconnect` (realtime.ts:160) keeps reconnecting after emitting it, and `onopen` (realtime.ts:332) resets `reconnectAttempts` and emits `'open'`. Settling every pending waiter here fails charts on a transient ~7s blip (backoff 1s/2s/4s) that would otherwise recover. Consider re-arming waiters on reopen, or only abandoning on a genuinely terminal condition.
  • superset/tasks/context.py - 2
    • Handler failure hidden from error_message · Line 647-685
      When the task body already failed (status=FAILURE), `expected_status=[IN_PROGRESS, ABORTING]` makes this transition always fail, so the combined `error_msg` built at lines 600-620 is discarded and the handler failure is only recorded under `private.framework.cleanup_*`. The previous unconditional `UpdateTaskCommand` appended it to the public `error_message`. Operators reading `error_message` will no longer see cleanup-handler failures. Confirm this regression is intended.
    • Cache not updated after write · Line 640-658
      `error_properties` (line 416) establishes the invariant that `self._properties_cache` must reflect what is committed to the DB. Here `failure_props = merge_properties(self._properties_cache, ...)` is written via `InternalStatusTransitionCommand`/`InternalUpdateTaskCommand` but never written back to the cache, leaving it stale relative to the committed error detail. If any later write is built from the cache, the recorded error would be erased. Consider updating the cache after the write.
  • superset/tasks/decorators.py - 3
    • Unhandled ValueError on pruned prereq · Line 465-473
      `unmet_prerequisite` treats a pruned/missing prerequisite as failed, but this loop checks the stale `prerequisite.status` and calls `TaskManager.wait_for_completion`, which raises `ValueError` when the task no longer exists. A prerequisite pruned mid-wait would raise an unhandled exception here, diverging from the async path's graceful handling. Re-read the prerequisite and treat a missing row as failed.
    • Abort handler failure misreported · Line 524-524
      `_finalize_inline_terminal_status` always writes ABORTED/TIMED_OUT when an abort/timeout was detected, but never checks `ctx.abort_handlers_completed`. The async path in `scheduler.py` (lines 658-700) marks the task FAILURE when abort handlers did not complete. This divergence can report a task as ABORTED even when cleanup (e.g. cancelling the underlying query) actually failed. Consider mirroring the `abort_handlers_completed` check.
    • Task can strand IN_PROGRESS · Line 528-533
      `_abort_locally` writes ABORTING best-effort and may fail (metastore unreachable), yet this finalize only transitions from `expected_status=ABORTING`. If the ABORTING write failed, the transition is a no-op and the task stays IN_PROGRESS forever. Consider including IN_PROGRESS in `expected_status` or adding a fallback so the task always reaches a terminal state.
  • superset/common/query_object.py - 1
    • Missing attribute type annotation · Line 173-173
      `force_nonce` is the only `QueryObject` attribute without a class-level type annotation. It is assigned directly at `async_queries.py:297` (`query_obj.force_nonce = ...`) and read via `getattr` in `QueryContextProcessor._force_nonce`; with mypy enforced in pre-commit, the undeclared attribute will be flagged. Add `force_nonce: str | None` to the annotation block for consistency and type safety.
  • superset/charts/schemas.py - 1
    • CWE-20: Untrusted nonce bypasses force · Line 1571-1582
      `force_nonce` is client-controlled and embedded verbatim in the marker key `gtf-force-nonce:{nonce}:{cache_key}` (`_force_marker_key`). In `_resolve_forced_query`, a present marker makes `force` return False, so a client reusing a previously-issued nonce for the same query reads the cached result instead of recomputing — defeating the `force` contract. The docstring says the nonce is the server task UUID, but the schema accepts any string. Consider validating it against the server-generated task UUID.
  • superset/coordination/base.py - 1
    • Incomplete exception guard · Line 464-466
      The docstring says this is "Guarded like `_read_stream`", but that method also catches `OSError` (line 438) while this helper catches only `RedisError`. In `_run_listen_loop` the baseline call (line 540) sits outside the new `try/except`, so a raw `OSError` from `stream_last_id`/`xrevrange` would still kill the listener thread at startup — the exact failure this change prevents. Consider `except (RedisError, OSError):`.
  • superset-frontend/src/middleware/realtime.ts - 2
    • Unused test-only export · Line 449-454
      `emitRealtimeStateForTests` is exported but never referenced anywhere in the repo (unlike `emitRealtimeOpenForTests`, which `hooks.test.tsx` uses). This is dead code introduced by the diff. Remove it, or add a test that calls it to exercise the state-listeners.
    • Stale reconnect counter · Line 123-123
      `reconnectAttempts` is module-level and only reset in `onopen` (line 335). `connectRealtime` is documented as re-invocable and `disconnectRealtime` is exported, but neither resets the counter. A re-init while the socket is down would inherit the old count and could report `unhealthy` (or skip `reconnecting`) on the very first fresh attempt. Consider resetting it in both entry points.
  • superset/tasks/dependencies.py - 1
    • Misleading prune diagnostics · Line 76-78
      When a prerequisite is pruned (`find_one_or_none` returns `None`), line 78 returns the stale `prerequisite` whose status is still non-terminal (e.g. `pending`). `fail_dependent_on_unmet_prerequisite` then persists/logs `did not succeed (status=pending)`, contradicting the docstring's "terminal non-SUCCESS" return contract and misleading operators. Consider returning a dedicated marker or including the pruning cause in `error_message`.
  • superset-frontend/src/explore/exploreUtils/index.ts - 1
    • In-place mutation via type cast · Line 323-331
      The stamping block mutates each `payload.queries` element in place via `(query as { force_nonce?: string })`, hiding `force_nonce` from the static query type and making the side effect invisible to callers. Prefer a non-mutating `map` that spreads each query, and ideally declare `force_nonce` as an optional typed field so the round-trip is type-checked.
  • superset-frontend/src/middleware/realtime.test.ts - 1
    • Unverified reset assertion · Line 276-278
      The test name claims 'open resets it', but the only assertion after `onopen` is that the last state is 'open'. If `onopen` emitted 'open' without resetting `reconnectAttempts`, this test would still pass. Add a follow-up `onclose` and assert the next state is 'reconnecting' (not 'unhealthy') to actually verify the reset.
Review Details
  • Files reviewed - 35 · Commit Range: 16e6afe..c1d1dac
    • superset-frontend/src/components/Chart/chartAction.ts
    • superset-frontend/src/components/Chart/chartActions.test.ts
    • superset-frontend/src/explore/exploreUtils/index.ts
    • superset-frontend/src/features/tasks/TaskPayloadPopover.tsx
    • superset-frontend/src/features/tasks/types.ts
    • superset-frontend/src/middleware/asyncEvent.test.ts
    • superset-frontend/src/middleware/asyncEvent.ts
    • superset-frontend/src/middleware/realtime.test.ts
    • superset-frontend/src/middleware/realtime.ts
    • superset-frontend/src/pages/TaskList/index.tsx
    • superset/charts/schemas.py
    • superset/commands/distributed_lock/release.py
    • superset/common/query_context_processor.py
    • superset/common/query_object.py
    • superset/coordination/base.py
    • superset/daos/key_value.py
    • superset/migrations/versions/2026-08-21_12-00_7e2c9a4f1b83_create_task_dependencies_table.py
    • superset/models/task_subscribers.py
    • superset/models/tasks.py
    • superset/tasks/async_queries.py
    • superset/tasks/context.py
    • superset/tasks/decorators.py
    • superset/tasks/dependencies.py
    • superset/tasks/scheduler.py
    • superset/tasks/utils.py
    • superset/utils/feature_flag_manager.py
    • tests/unit_tests/common/test_query_context_processor.py
    • tests/unit_tests/coordination/test_service.py
    • tests/unit_tests/distributed_lock/distributed_lock_tests.py
    • tests/unit_tests/feature_flag_test.py
    • tests/unit_tests/tasks/test_async_queries.py
    • tests/unit_tests/tasks/test_dependencies.py
    • tests/unit_tests/tasks/test_handlers.py
    • tests/unit_tests/tasks/test_scheduler_executor.py
    • tests/unit_tests/tasks/test_timeout.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

…req wait, reconnect counter, dead export)

From the Bito review on PR apache#43768:
- coordination: _baseline_stream_id now also catches OSError (matches
  _read_stream), so a raw socket error at listener startup can't kill the thread.
- decorators: the inline DAG wait tolerates a prerequisite pruned mid-wait
  (wait_for_completion raises ValueError) and lets the re-check handle it, instead
  of surfacing an unhandled exception.
- realtime.ts: reset the reconnect-attempt counter on connect/disconnect so a
  prior outage's count can't make the first fresh attempt look unhealthy; remove
  the unused emitRealtimeStateForTests export.

(--no-verify: frontend type-check hook blocked only by the stale gitignored
superset-ui-core lib/*.d.ts; mypy/oxlint/oxfmt/tests pass.)
@villebro
villebro merged commit 5df8f68 into apache:gaq-to-gtf Sep 1, 2026
69 of 70 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:db-migration PRs that require a DB migration size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant