Skip to content

Commit bb0356a

Browse files
authored
Merge pull request #3931 from plotly/feat/streaming
Streaming callbacks with multiplexed transport
2 parents d08abba + 9b807f1 commit bb0356a

62 files changed

Lines changed: 9795 additions & 47 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.ai/ARCHITECTURE.md‎

Lines changed: 409 additions & 0 deletions
Large diffs are not rendered by default.

‎.github/workflows/testing.yml‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
dcc_paths_changed: ${{ steps.filter.outputs.dcc_related_paths }}
2121
html_paths_changed: ${{ steps.filter.outputs.html_related_paths }}
2222
websocket_changed: ${{ steps.filter.outputs.websocket_paths }}
23+
streaming_changed: ${{ steps.filter.outputs.streaming_paths }}
2324
steps:
2425
- name: Checkout repository
2526
uses: actions/checkout@v4
@@ -79,6 +80,14 @@ jobs:
7980
- '@dash-websocket-worker/**'
8081
- 'dash/dash-renderer/src/**'
8182
- 'tests/websocket/**'
83+
streaming_paths:
84+
- *shared_paths
85+
- 'dash/_callback.py'
86+
- 'dash/_streaming.py'
87+
- 'dash/_callback_context.py'
88+
- 'dash/backends/**'
89+
- 'dash/dash-renderer/src/**'
90+
- 'tests/streaming/**'
8291
8392
lint-unit:
8493
name: Lint & Unit Tests (Python ${{ matrix.python-version }})
@@ -256,6 +265,77 @@ jobs:
256265
tests/compliance/test_typing.py \
257266
tests/compliance/test_callback_typing.py
258267
268+
shared-storage:
269+
name: Run Shared Storage Tests (Python ${{ matrix.python-version }})
270+
needs: build
271+
timeout-minutes: 20
272+
runs-on: ubuntu-latest
273+
strategy:
274+
fail-fast: false
275+
matrix:
276+
python-version: ["3.9", "3.12"]
277+
278+
# Redis service for the RedisSharedStorage backend.
279+
services:
280+
redis:
281+
image: redis:6
282+
ports:
283+
- 6379:6379
284+
options: >-
285+
--health-cmd "redis-cli ping"
286+
--health-interval 10s
287+
--health-timeout 5s
288+
--health-retries 5
289+
290+
env:
291+
REDIS_URL: redis://localhost:6379
292+
293+
steps:
294+
- name: Checkout repository
295+
uses: actions/checkout@v4
296+
297+
- name: Set up Node.js
298+
uses: actions/setup-node@v4
299+
with:
300+
node-version: '24'
301+
cache: 'npm'
302+
303+
- name: Install Node.js dependencies
304+
run: npm ci
305+
306+
- name: Set up Python ${{ matrix.python-version }}
307+
uses: actions/setup-python@v5
308+
with:
309+
python-version: ${{ matrix.python-version }}
310+
cache: 'pip'
311+
cache-dependency-path: requirements/*.txt
312+
313+
- name: Download built Dash packages
314+
uses: actions/download-artifact@v4
315+
with:
316+
name: dash-packages
317+
path: packages/
318+
319+
- name: Install Dash packages
320+
run: |
321+
python -m pip install --upgrade pip wheel
322+
python -m pip install "setuptools<80.0.0"
323+
find packages -name dash-*.whl -print -exec sh -c 'pip install "{}[ci,testing,dev,diskcache,redis]"' \;
324+
325+
- name: Setup Chrome and ChromeDriver
326+
uses: browser-actions/setup-chrome@v1
327+
with:
328+
chrome-version: stable
329+
330+
- name: Verify Redis connection
331+
run: python -c "import redis; redis.Redis(host='localhost', port=6379).ping(); print('Redis OK')"
332+
333+
- name: Build/Setup test components
334+
run: npm run setup-tests.py
335+
336+
- name: Run shared-storage tests
337+
run: pytest --headless tests/shared_storage -v
338+
259339
background-callbacks:
260340
name: Run Background & Async Callback Tests (Python ${{ matrix.python-version }})
261341
needs: [build, changes_filter]
@@ -625,6 +705,69 @@ jobs:
625705
touch __init__.py
626706
pytest --headless --nopercyfinalize tests/websocket -v -s
627707
708+
streaming-tests:
709+
name: Streaming Callback Tests (Python ${{ matrix.python-version }})
710+
needs: [build, changes_filter]
711+
if: |
712+
(github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev')) ||
713+
needs.changes_filter.outputs.streaming_changed == 'true'
714+
timeout-minutes: 30
715+
runs-on: ubuntu-latest
716+
strategy:
717+
fail-fast: false
718+
matrix:
719+
python-version: ["3.9", "3.12"]
720+
721+
steps:
722+
- name: Checkout repository
723+
uses: actions/checkout@v4
724+
725+
- name: Set up Node.js
726+
uses: actions/setup-node@v4
727+
with:
728+
node-version: '24'
729+
cache: 'npm'
730+
731+
- name: Install Node.js dependencies
732+
run: npm ci
733+
734+
- name: Set up Python ${{ matrix.python-version }}
735+
uses: actions/setup-python@v5
736+
with:
737+
python-version: ${{ matrix.python-version }}
738+
cache: 'pip'
739+
cache-dependency-path: requirements/*.txt
740+
741+
- name: Download built Dash packages
742+
uses: actions/download-artifact@v4
743+
with:
744+
name: dash-packages
745+
path: packages/
746+
747+
- name: Install Dash packages
748+
# Streaming callbacks are async generators; the async extra pulls in
749+
# flask[async], which they require on the Flask backend.
750+
run: |
751+
python -m pip install --upgrade pip wheel
752+
python -m pip install "setuptools<80.0.0"
753+
find packages -name dash-*.whl -print -exec sh -c 'pip install "{}[async,ci,testing,dev,fastapi,quart]"' \;
754+
755+
- name: Setup Chrome and ChromeDriver
756+
uses: browser-actions/setup-chrome@v1
757+
with:
758+
chrome-version: stable
759+
760+
- name: Build/Setup test components
761+
run: npm run setup-tests.py
762+
763+
- name: Run streaming tests
764+
run: |
765+
mkdir streamtests
766+
cp -r tests streamtests/tests
767+
cd streamtests
768+
touch __init__.py
769+
pytest --headless --nopercyfinalize tests/streaming -v -s
770+
628771
test-main:
629772
name: Main Dash Tests (Python ${{ matrix.python-version }}, React ${{ matrix.react-version }}, Group ${{ matrix.test-group }})
630773
needs: build

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ This project adheres to [Semantic Versioning](https://semver.org/).
55
## [Unreleased]
66

77
### Added
8+
- [#3930](https://github.com/plotly/dash/pull/3930) Shared storage: a backend-agnostic state manager available on every app via `dash.ctx.shared_storage` (and `app.shared_storage`), for sharing state between callbacks or across worker processes without an external service. Started lazily on first use, so it costs nothing until touched; pass `shared_storage=None` to `Dash(...)` to disable.
9+
- Cross-process key/value store (`get`/`set`/`delete`, with optional TTL) and ordered, replayable publish/subscribe (`publish`/`subscribe`). Subscriptions resume from the caller's last-seen sequence out of a bounded buffer, and a buffer overrun surfaces as an explicit gap rather than a silent loss. Values must be JSON-compatible (like `dcc.Store`).
10+
- Three backends ship, selected by passing a `BaseSharedStorage` instance to `shared_storage=`:
11+
- `LocalSharedStorage` (default): in-memory, elects one owner process per machine over an `AF_UNIX`/loopback socket, correct within a single container, including across multiple gunicorn workers.
12+
- `DiskcacheSharedStorage`: on a `diskcache.Cache`, shared by every process on one host, for single-machine deployments (not for multi-pod ones with ephemeral disks).
13+
- `RedisSharedStorage`: on Redis, using Redis Streams for the ordered pub/sub: the backend for horizontally-scaled deployments behind a load balancer, e.g. apps scaled across pods.
14+
- `LocalSharedStorage` additionally accepts `mode=` to make its key/value store durable: `"memory"` (default, in-memory only), `"persist"` (write-through to disk on every change), or `"persist-reset"` (in-memory speed with a periodic flush every `flush_interval` seconds and on clean exit). Persistent modes recover on start and on owner re-election, so state survives a process restart or a crashed owner. Data is stored in a chunked, atomically-written msgpack store (a per-namespace folder under the user cache directory by default, overridable via `path=`). TTLs are preserved across restarts; pub/sub remains transient.
15+
- [#3931](https://github.com/plotly/dash/pull/3931) Streaming callbacks: a callback defined as an `async def` generator streams its yields to the browser as they are produced (`dash.Patch` yields give incremental updates). All of a browser's streams share one connection hosted in a SharedWorker, so they don't count against the per-host connection limit; closing a tab cancels its streams.
816
- [#3977](https://github.com/plotly/dash/pull/3977) Add partial WebSocket prop reads with `get_prop(..., path=...)`. Closes [#3975](https://github.com/plotly/dash/issues/3975).
917
- [#3765](https://github.com/plotly/dash/pull/3765) Add opt-in partial pattern matching for callback `Input`, `Output`, and `State` dependencies via `partial_pattern=True`. Dictionary ID patterns can now match component IDs containing additional keys, and partial patterns can be combined with `ALL` and `MATCH` wildcards. Fixes [#3764](https://github.com/plotly/dash/issues/3764).
1018
- [#3646](https://github.com/plotly/dash/pull/3646) Experimental support for React 19. The default is still React 18.3.1; to use React 19 set the environment variable `REACT_VERSION=19.2.4` before running your app, or call `dash._dash_renderer._set_react_version("19.2.4")` inside the app. React 19 has no official UMD builds, so Dash serves the [`umd-react`](https://www.npmjs.com/package/umd-react) package, together with a compatibility shim loaded after react-dom and before any component package. The shim keeps component libraries built against React <=18 (e.g. dash-bootstrap-components, dash-mantine-components) working under React 19: it stubs the removed `ReactCurrentOwner` internals, redirects the legacy element `$$typeof` symbol so pre-bundled React 18 jsx-runtimes produce elements React 19 accepts (error #525), and exposes a global `react/jsx-runtime` (`window.ReactJSXRuntime`) that Dash's own component bundles externalize to. Component library authors adopting this convention should copy the defensive `jsxRuntimeExternal` webpack external from `components/dash-core-components/webpack.config.js` rather than a bare `'ReactJSXRuntime'` string: it falls back to a `React.createElement`-based runtime when the global is missing, so the same build also works on Dash versions older than this release.

‎dash/__init__.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
from ._remount import remount # noqa: F401,E402
4545
from ._jupyter import jupyter_dash # noqa: F401,E402
4646

47+
from ._shared_storage import ( # noqa: F401,E402
48+
BaseSharedStorage,
49+
DiskcacheSharedStorage,
50+
LocalSharedStorage,
51+
RedisSharedStorage,
52+
SharedStorageError,
53+
SharedStorageGap,
54+
Subscription,
55+
)
56+
4757
from ._hooks import hooks # noqa: F401,E402
4858

4959
ctx = callback_context
@@ -96,4 +106,11 @@ def _jupyter_nbextension_paths():
96106
"ctx",
97107
"hooks",
98108
"stringify_id",
109+
"BaseSharedStorage",
110+
"DiskcacheSharedStorage",
111+
"LocalSharedStorage",
112+
"RedisSharedStorage",
113+
"SharedStorageError",
114+
"SharedStorageGap",
115+
"Subscription",
99116
]

0 commit comments

Comments
 (0)