feat: migrate Global Async Queries onto the Global Task Framework - #43407
Draft
villebro wants to merge 6 commits into
Draft
feat: migrate Global Async Queries onto the Global Task Framework#43407villebro wants to merge 6 commits into
villebro wants to merge 6 commits into
Conversation
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #43407 +/- ##
==========================================
+ Coverage 78.82% 78.83% +0.01%
==========================================
Files 2876 2879 +3
Lines 164459 164342 -117
Branches 37956 37985 +29
==========================================
- Hits 129634 129561 -73
+ Misses 32378 32341 -37
+ Partials 2447 2440 -7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
6 tasks
Merged
6 tasks
…at-most-once pub/sub) (#43409)
5 tasks
9 tasks
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SUMMARY
Epic feature branch. This PR is the integration target for the multi-step migration of Global Async Queries (GAQ) onto the Global Task Framework (GTF). Individual step PRs are reviewed and merged into
gaq-to-gtf; this PR accumulates them and merges intomasterin one go once the epic is complete. It is kept as a draft tracker until then.Superset currently has two overlapping background-execution systems:
GLOBAL_ASYNC_QUERIES) — runs chart-data queries in a dedicated Celery task (load_chart_data_into_cache) and notifies the browser through a bespoke Redis Streams transport (AsyncQueryManager+ the/api/v1/async_event/polling endpoint, or the externalsuperset-websocketserver). Results are handed back via a cachedqc-<hash>query-context descriptor and aresult_url.GLOBAL_TASK_FRAMEWORK) — a newer unified background-task abstraction: a@task/.schedule()API, ataskstable, dedup bytask_key, progress/timeouts/cancellation, abort handlers,wait_for_completion, a Task List UI, and a REST API.This epic deprecates GAQ's internal plumbing and re-implements async chart data on top of GTF, while keeping
GLOBAL_ASYNC_QUERIESas the operator-facing switch for whether chart queries run asynchronously. Two secondary goals: consolidate coordination primitives (locks, pub/sub, streams) behind one service + one config (DISTRIBUTED_COORDINATION_CONFIG), and harmonizeQueryObjectserialization on one canonical JSON-safe representation.Key architectural insight. Per-query results are already the atomic cached unit:
QueryContextProcessor.get_df_payload_result(query_obj)executes and caches exactly oneQueryObjectunder its ownquery_cache_key(which folds in datasource,extra_cache_keys, RLS, and impersonation). Theqc-<hash>entry holds no results — it is only a descriptor. So the atomic async unit is theQueryObjectkeyed byquery_cache_key, and per-query dedup by that key is safe across users.Progress tracker
Every step PR targets
gaq-to-gtf.GLOBAL_ASYNC_QUERIES_CACHE_BACKENDdeprecation)task_dependenciesjunction table (+ chain-icon dependency display in the Task List)QueryObjectserializationQueryObjecttasks (superset.query_object_v1) +GET /api/v1/task/status_changescursor poll + client re-request; removes theqc-<hash>wrapper /result_url//data/<cache_key>replay AND rips outAsyncQueryManager//api/v1/async_event//GLOBAL_ASYNC_QUERIES_CACHE_BACKEND; embedded-guest task visibility. Async is polling-only (WS transport deferred to step 6).async_modeper-request opt-in +GLOBAL_ASYNC_QUERIES→GLOBAL_TASK_FRAMEWORKauto-enable +GLOBAL_ASYNC_QUERIES_DEFAULT/ per-dashboard overridesuperset-websocketinto a general-purpose push transportvillebro/gtf-websocketsuperset-websocketin the main multi-stage Dockerfile and ship it on the official image (launchable from the main image via an alternate entrypoint), so the realtime transport needs no self-built imageShipped so far (merged into
gaq-to-gtf)superset/coordination/service consolidating distributed locks, pub/sub, streams, and key/value over oneDISTRIBUTED_COORDINATION_CONFIGconnection, withwait_for_signal/listen_for_signalawait-notify helpers. Coordinator resolvesDISTRIBUTED_COORDINATION_CONFIGonly;GLOBAL_ASYNC_QUERIES_CACHE_BACKENDis deprecated (GAQ-only fallback). GTFTaskManager, the distributed lock, andAsyncQueryManagerroute through it. Non-breaking.CoordinationService.notify()(XADD,MAXLEN 1+ TTL) plus stream-blockingwait_for_signal/listen_for_signal(event-driven, no polling) whenDISTRIBUTED_COORDINATION_CONFIGis set, metastore polling otherwise. GTF task completion/abort emit vianotify(). Pub/sub (publish) retained only as an explicit best-effort nudge. New backend primitivesxread/stream_last_id/expire; new configDISTRIBUTED_COORDINATION_SIGNAL_TTL(default 24h) bounds signal-stream retention. Removes the ~1s DB-poll-with-backend that the lossy pub/sub design required.task_dependenciesjunction table + migration (FKON DELETE CASCADE);Task.dependenciesself-referential M2M exposing the prerequisiteTaskentities in oneselectin.TaskOptions.depends_onacceptsTaskentities / UUIDs / strings; block-and-wait scheduler gate withall_successsemantics (fails fast, cascades transitively); chain-icon dependency column + "waiting on N prerequisites" indicator in the Task List;depends_onin the REST API; superset-core abstractTaskDependencymodel. Submit persists edges in O(1) round-trips; the gate does zero extra reads when prerequisites are already terminal.superset/common/query_serialization.py:serialize_query(query_context, index)→ JSON-safe payload of the raw query dict + datasource/form_data/result_type/result_format/force/custom_cache_timeout;load_serialized_query(payload)rebuilds a single-queryQueryContextviaQueryContextFactory(the same path that produced it), so the reconstructed query hashes to an identicalquery_cache_key.force/custom_cache_timeout(context-level) carried explicitly so they survive per query. The atomic unit Step 4 runs as an async task./chart/datanow fans out into one SHARED GTF task perQueryObject(superset.query_object_v1, keyed byquery_cache_key); the 202 returns{task_ids}and the client pollsGET /api/v1/task/status_changes(cursor-based{uuid: {status, progress}},TaskFilter-scoped,task_typefilter) then re-issues the request from the warm per-query cache. Contribution queriesdepends_onthe totals task. No coordinator task, noqc-<hash>wrapper. Embedded guests get task visibility + cancellation via a token-derivedguest_key(task_subscribers.guest_key, folded into the task-dependencies migration). Rips outAsyncQueryManager, the/api/v1/async_event/REST API, the legacyload_chart_data_into_cachejob, and the GAQ JWT/transport config + dedicatedGLOBAL_ASYNC_QUERIES_CACHE_BACKEND— coordination isDISTRIBUTED_COORDINATION_CONFIG-only. Frontend transport rewritten (asyncEvent.ts) to the cursor poll (multi-waiter-safe for deduplicated tasks). KeptGLOBAL_ASYNC_QUERIESflag +GLOBAL_ASYNC_QUERIES_POLLING_DELAY. Real-time WS push retired (polling-only) until step 6./chart/dataruns async only when the request setsasync_mode(absent = synchronous 200, so programmatic API clients are unaffected); gated additionally on the flag + full-JSON + caching (ChartDataRestApi._should_run_async).GLOBAL_ASYNC_QUERIESforce-enablesGLOBAL_TASK_FRAMEWORKat startup. New frontend-only configGLOBAL_ASYNC_QUERIES_DEFAULT(defaulttrue). FrontendresolveAsyncMode()policy chain (feature flag → per-dashboard override → deployment default) injectsasync_modeon full-JSON renders (via an explicitenableAsyncModeopt-in so directresponse.json.resultreaders stay sync);StatefulChartopts in via an injected hook; native-filter requests carry the override too. Per-dashboard override UI in the dashboard Properties modal (json_metadata.async_mode).Dependency graph
Design decisions (locked with product owner)
{task_ids}, and the client pollsGET /api/v1/task/status_changes(a cursor-based{uuid: {status, progress}}batch,TaskFilter-scoped, filtered tosuperset.query_object_v1), aggregating the query tasks' own statuses itself — allSUCCESS→ re-request (served from the warm per-query cache); any terminal non-success → error. Cancel viaPOST /api/v1/task/<uuid>/cancel. This lets us remove the bespoke/api/v1/async_event/REST API and the entire GAQ firehose/AsyncQueryManager. GTF owns its own completion emission (per-task, via the coordination service); the WebSocket transport (Step 6) will subscribe to GTF, not to any GAQ stream. See Realtime transport & channel topology below.GLOBAL_ASYNC_QUERIES=onforce-enablesGLOBAL_TASK_FRAMEWORKat startup (with a log line), mirroring theDASHBOARD_RBACauto-migration precedent.QueryObject,task_key = query_cache_key,TaskScope.SHARED(safe cross-user dedup — the key encodes RLS/impersonation).qc-<hash>descriptor. On completion the client re-issues the same chart-data POST, which now hits the per-query DATA cache and returns synchronously.depends_oncapability to GTF; the chart-data orchestrator sets edges only where real coupling exists (contribution dependents → the totals query). Independent queries still run in parallel.QueryObjectrepresentation (to_dict()+json_int_dttm_ser+ datasource ref +result_type/result_format/force).GLOBAL_ASYNC_QUERIES_CACHE_BACKENDis deprecated, not removed — non-breaking.🔗) popover column mirroring the existingDetailspopover pattern — the popover lists each prerequisite with itsTaskStatusIcon; a "waiting on N prerequisites" indicator surfaces blocked (block-and-wait) tasks. The list stays flat (no tree/expandable rows — server-side pagination + arbitrary sort make subtree grouping fragile); a richer status-colored DAG graph is deferred to a detail drawer once multi-node DAGs exist.GLOBAL_ASYNC_QUERIESonly makes async available; a request-levelasync_modeflag (endpoint defaultfalse) decides per request. The server treats an absentasync_modeas sync, so programmatic/chart/dataconsumers keep the synchronous 200 flow. The frontend resolves theasync_modeit sends via a policy chain — per-dashboard override (Default / Force enabled / Force disabled, in the dashboard properties editor) → deployment defaultGLOBAL_ASYNC_QUERIES_DEFAULT(frontend-only policy input, defaulttrue) → feature-flag gate. Defaulttruekeeps the UI's existing async behavior (non-breaking for the UI) while API clients are sync-by-default; operators dial it down globally or per dashboard for gradual rollout.Follow-on phase — generalize the WebSocket server (PR 7+, distributed into the stepping stones)
Beyond the core migration, generalize
superset-websocketfrom a GAQ-specific event tail into a general server→browser push transport any feature can use (real-time GTF task/progress updates in the Task List, report/alert notifications, extension events), all riding the coordination-service streams. This depends only on PR 0 (already merged), so the work is threaded into the stepping-stone PRs where each already touches the relevant code: 7a + 7c → PR 3 (emit the generic type-tagged envelope via apush_to_channelhelper, keeping the GAQ shape as onetype), 7b → PR 4 (extract a shared, feature-agnostic channel-token service), 7e groundwork → PR 5 (topic-routableasyncEvent.ts), 7f → PR 6 (Node consumes the generic envelope; drop the legacytype, last). The remaining dedicated step is 7d — real-time GTF task updates in the Task List (push instead of poll), which pairs with PR 1's dependency display. Related: expose coordination to extensions via asuperset_core.coordinationabstract surface (injected likesuperset_core.tasks).Follow-up — Streams-based coordination (cleanup, after PR 1)
Redis Pub/Sub is documented at-most-once / fire-and-forget (a message is "forever lost" on subscriber disconnect), so
CoordinationService.wait_for_signaltoday wakes on a pub/sub nudge but re-checks the DB predicate every_PUBSUB_TICK_SECONDS(=1.0s) as a delivery backstop — i.e. it degrades to ~1s polling even when Redis is up, and each poll currently loads the fullTaskORM (twoselectinrelations) when onlystatusis needed. A dedicated follow-up will movepublish_completion/publish_abortonto Redis Streams (persisted, at-least-once — Redis's own recommendation for stronger delivery) with a blockingXREADfrom a captured id: genuinely event-driven, race-free, and no busy-poll when Redis is available; the no-backend fallback becomes a status-only batched read (get_status, always-fresh scalar select, also detecting self-cancellation mid-wait); stream growth bounded withMAXLEN/EXPIRE+ prune. It touches the shared PR 0 primitive (GTF completion/abort + sync-join), so it gets its own PR + CI. PR 1 deliberately does not block on it (the 1s poll is correct, just not optimal).Realtime transport & channel topology (strategic direction)
This epic's coordination work sets up a general realtime transport for the whole app, not just GAQ. Two distinct needs, two mechanisms — deliberately kept separate:
wait_for_completionblocks on a per-task signal; Step 1). Point-addressed, replayable, at-least-once, no firehose scan. Correctness depends on delivery, so it must be a stream.entity-changes:task, later:dashboard,:chart,:dataset). A mounted list view holds exactly one subscription and filters client-side to the uuids it currently renders. Best-effort is fine: a dropped event just leaves a row briefly stale until the next update or refresh.Why per-entity-type is the right granularity (rejecting both extremes):
async-events-fullpattern — we move away from it, not extend it.Firehose disposition. The GAQ
async-events-fullfirehose andAsyncQueryManagerwere removed in Step 4; async chart-data is polling-only until Step 6 adds the GTF-native WebSocket transport. New realtime uses the per-entity-type Pub/Sub above. Step 7 is the first concrete consumer of this pattern (the Task List), paving the way to make every list view in Superset realtime.Step 7 — Realtime list views via entity-change pub/sub (capstone)
Make list views reflect entity state in real time instead of requiring a manual refresh, starting with the Task List (live status / progress / completion rate on currently-displayed rows). Semantics (locked): lossy (per-entity-type Pub/Sub, best-effort); update-only — only rows currently displayed are patched in place; no new-row insertion (too noisy — new entities appear on the next refresh/navigation). Backend emits a change event at the existing
Taskstatus-transition /publish_completionchokepoints ontoentity-changes:task; the frontend Task List subscribes (via the Step 6 generalized transport), filtering to the uuids on the current page. Establishes the reusable pattern later extended to Dashboards, Charts, Datasets, etc.Security
No change to the role/capability matrix. Per-query dedup by
query_cache_keyis safe across users because the key encodes RLS + impersonation + datasource; workers run underoverride_user. Guest/embedded channels remain HMAC-derived. Each step PR is reviewed againstSECURITY.md.Backward compatibility
The operator switch stays
GLOBAL_ASYNC_QUERIES. Async now runs on the Global Task Framework overDISTRIBUTED_COORDINATION_CONFIG. Breaking changes (the whole branch merges tomasteras a unit, so these land together): Step 4 removes theqc-<hash>result descriptor +/api/v1/chart/data/<cache_key>replay, the bespoke/api/v1/async_event/REST API,AsyncQueryManager+ the GAQ firehose, the GAQ JWT/cookie/transport config, and the dedicatedGLOBAL_ASYNC_QUERIES_CACHE_BACKEND(coordination isDISTRIBUTED_COORDINATION_CONFIG-only). Clients poll/cancel via the GTF task API; polling auth is the normal session, embedded-guest visibility isSECRET_KEY-derived.UPDATING.mdwill carry the operator migration notes.async_modeper-request opt-in (withGLOBAL_ASYNC_QUERIES_DEFAULT/ per-dashboard override) lands in Step 5.TESTING INSTRUCTIONS
Per step PR (see each PR for specifics). End-to-end for the epic: with
GLOBAL_ASYNC_QUERIES=on(GTF auto-enabled) andDISTRIBUTED_COORDINATION_CONFIGset, load a dashboard with mixed multi-query + contribution charts; confirm tasks appear in the Task List UI assuperset.query_object_v1, the charts resolve via thestatus_changespoll, cancellation works, and a cached second load short-circuits to 200.ADDITIONAL INFORMATION
GLOBAL_ASYNC_QUERIES(auto-enablesGLOBAL_TASK_FRAMEWORK);GLOBAL_ASYNC_QUERIES_DEFAULT(frontend async default,true)task_dependencies)async_moderequest flag; taskdepends_on)qc-<hash>path +/api/v1/async_event/REST API)