villebro opened a new pull request, #43689:
URL: https://github.com/apache/superset/pull/43689

   ### SUMMARY
   
   Delivers per-tab realtime `task-status` fanout for async chart-data, against 
the
   umbrella branch (#43407). Builds on the per-tab subscription refcount from 
#43685
   (`private.task.consumers`).
   
   **Problem.** Realtime `task-status` is delivered at principal grain:
   `publish_task_status` names the task's subscriber principals and the
   `superset-websocket` server fans each out to `realtime:user:<id>`, so 
**every tab
   a user has open receives status for every task any of their tabs is 
watching**,
   and the browser discards the irrelevant ones by `task_id`. Now that each 
tab's
   subscription is tracked per-tab, we can route `task-status` to only the tab 
that
   is watching a given task.
   
   This realizes the three channel tiers:
   
   1. **tab-specific** → `realtime:user:<id>:<tabId>` (new: per-tab 
`task-status`)
   2. **user-specific** → `realtime:user:<id>` (all of a user's tabs — kept for
      non-per-tab tasks and future per-user notices)
   3. **all users+tabs** → the existing `entity-changes:*` broadcast tier
   
   ### DESIGN
   
   - **Auth unchanged: one user-specific cookie, one ws connection per tab.** 
The
     cookie authorizes the principal (`channel: user:<id>`, identity-bound — the
     token integrity check is kept); each tab opens its own `WebSocket` and
     advertises its `getTabId()` on the **connect URL** (`?tab_id=…`), *not* in 
the
     token. The server derives the per-tab channel by prefixing the authorized
     principal channel, so a client-supplied `tab_id` can never address another
     principal's sockets. No per-tab cookies, no bearer token.
   - **Socket dual-registration.** On connect the socket is registered under
     `user:<id>` (principal — all-tabs/broadcast delivery + the per-principal
     connection cap) and, when a `tab_id` is present, also `user:<id>:<tabId>`. 
One
     socket id, two channel keys.
   - **Fanout routes by channel-key strings.** The `task-status` wire payload
     changes from `subscribers: [{principal_type, sub}]` to `channels: [str]` — 
the
     producer computes the exact routing keys and the ws server just delivers to
     `realtime:<key>` (this also simplifies the server: no 
re-derivation/validation
     of principal keys in the fanout path).
   - **Routing keys come from the task type's subscription policy.** New
     `TaskSubscriptionPolicy.routing_channels(task) -> list[str] | None` 
(concrete
     default `None`). Chart-data returns its `consumers` list (already the 
per-tab
     keys); any task type without a policy — or a chart task with no recorded 
tab —
     falls back to principal-grain keys from `get_subscriber_principals` 
(**unchanged
     behavior for non-chart tasks**).
   - **Broadcast dedupe fix.** `broadcastToAll` now iterates the unique socket
     registry (via an extracted `sendToSocket` helper) instead of per-channel, 
so a
     dual-registered socket receives each entity-change nudge exactly once.
   
   ### BEFORE/AFTER
   
   - **Before:** two tabs of one user watching different charts each receive 
every
     chart's `task-status`; the client filters by `task_id`.
   - **After:** each tab receives realtime `task-status` only for its own 
tasks, on
     `realtime:user:<id>:<tabId>`. Entity-change list nudges still reach every 
tab.
   
   ### TESTING INSTRUCTIONS
   
   - `pytest tests/unit_tests/tasks/test_manager.py 
tests/unit_tests/tasks/test_async_queries.py`
   - `npm --prefix superset-websocket run test && npm --prefix 
superset-websocket run lint`
   - `npm run test -- realtime`
   - Manual (worker + Redis coordination + ws server): open the same chart in 
two
     tabs; confirm each tab's `task-status` arrives only on its own
     `realtime:user:<id>:<tabId>` channel (inspect ws frames) while 
entity-change
     list nudges reach both tabs exactly once; stop the ws server and confirm
     polling still resolves charts.
   
   ### ADDITIONAL INFORMATION
   
   Wire-contract change (`subscribers` → `channels`) is a coordinated 
Python↔Node
   change with no version field, so deploy the Flask app and the 
`superset-websocket`
   server together. During a mismatched rolling upgrade each side drops the 
other's
   `task-status` payload and degrades to client polling (no crash, no leak).
   
   - [ ] Has associated issue:
   - [x] Required feature flags: `GLOBAL_ASYNC_QUERIES` / 
`GLOBAL_TASK_FRAMEWORK`; optional `WEBSOCKET_ENABLE`
   - [ ] Changes UI
   - [ ] Includes DB Migration
   - [x] Introduces new feature or API
   - [ ] Removes existing feature or API
   


-- 
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]

Reply via email to