Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
cce590e
fix: handle None return from error handler in no-output background ca…
dajiaohuang Sep 25, 2026
ad21a5b
Merge branch 'dev' into fix/3628-background-callback-error-handler
dajiaohuang Sep 25, 2026
38bd0e7
fix: pass output_spec to MCP background task handler
dajiaohuang Sep 30, 2026
f267f58
style: format callback error handling
dajiaohuang Sep 30, 2026
c0be071
style: match pinned Black formatting
dajiaohuang Sep 30, 2026
054249f
test: cover no-output background callback errors
dajiaohuang Sep 30, 2026
ebf5f95
test: assert no-output callback error handling
dajiaohuang Sep 30, 2026
7a0bac1
Fix combined commit status API call
dajiaohuang Sep 30, 2026
dcbf640
Merge branch 'dev' into fix/post-test-status-combined-status
T4rk1n Oct 5, 2026
fc7f061
fix: apply output_spec guard to cancel branch and simplify tests
dajiaohuang Oct 7, 2026
47cd995
Merge remote-tracking branch 'origin/dev' into fix/3628-background-ca…
dajiaohuang Oct 15, 2026
f5cde05
Fix MCP task results for no-output background callbacks
dajiaohuang Oct 15, 2026
ba28a77
Decode the pathname in the pages router
b84330808 Oct 20, 2026
d62f731
Test WebSocket callbacks before the first HTTP request
wleeaf Oct 23, 2026
c142528
Merge remote-tracking branch 'origin/dev' into fix-pages-pathname-dec…
b84330808 Oct 24, 2026
ae8048f
Shorten the CHANGELOG entry and link the PR
b84330808 Oct 26, 2026
d092ea0
Merge branch 'dev' into fix/3628-background-callback-error-handler
T4rk1n Oct 26, 2026
40c6f7f
Merge pull request #4012 from dajiaohuang/fix/3628-background-callbac…
T4rk1n Oct 26, 2026
92d6121
Merge branch 'dev' into fix-pages-pathname-decoding
T4rk1n Oct 26, 2026
d74f615
Merge pull request #4074 from b84330808/fix-pages-pathname-decoding
T4rk1n Oct 26, 2026
1f97674
Merge branch 'dev' into fix-fastapi-websocket-setup
T4rk1n Oct 26, 2026
0d0fc1d
Remove the test callback from the global registry if the test fails
wleeaf Oct 27, 2026
70af4ca
Hold linters and @plotly majors in Dependabot, bump selenium cap to 4…
T4rk1n Oct 27, 2026
86e1662
Merge branch 'dev' into fix/post-test-status-combined-status
T4rk1n Oct 27, 2026
0f00a0a
Merge pull request #4025 from dajiaohuang/fix/post-test-status-combin…
T4rk1n Oct 27, 2026
210a489
Merge branch 'dev' into fix-fastapi-websocket-setup
T4rk1n Oct 27, 2026
d28563d
Merge branch 'dev' into dependabot-hold-linters-plotly-majors
T4rk1n Oct 27, 2026
cd5403b
Merge pull request #4085 from wleeaf/fix-fastapi-websocket-setup
camdecoster Oct 27, 2026
c2d94e3
Make shared storage opt-in, move msgspec to extras
T4rk1n Oct 28, 2026
7b397ae
Add PR number to CHANGELOG
T4rk1n Oct 28, 2026
945c004
Enable shared storage in the Windows stream worker URL test
T4rk1n Oct 28, 2026
712dc3c
Merge branch 'dev' into dependabot-hold-linters-plotly-majors
T4rk1n Oct 28, 2026
783a3dc
Merge pull request #4090 from plotly/dependabot-hold-linters-plotly-m…
T4rk1n Oct 28, 2026
1296357
Merge branch 'dev' into shared-storage-opt-in
T4rk1n Oct 28, 2026
5abb55c
Merge pull request #4095 from plotly/shared-storage-opt-in
T4rk1n Oct 29, 2026
076e5af
Require shared storage for HTTP streaming callbacks
T4rk1n Oct 29, 2026
acd5383
Add PR number to CHANGELOG
T4rk1n Oct 29, 2026
3bc84a4
Merge pull request #4099 from plotly/stream-requires-shared-storage
T4rk1n Oct 29, 2026
28b5375
version 4.5.0
T4rk1n Oct 29, 2026
3ba04eb
Backfill 4.5.0 CHANGELOG entries and PR links
T4rk1n Oct 29, 2026
00b8ed6
Merge pull request #4100 from plotly/release-4.5.0
T4rk1n Oct 29, 2026
fa4f243
Merge remote-tracking branch 'origin/dev' into master-4.5.0
T4rk1n Oct 29, 2026
f7ad523
Version 4.5.0 build artifacts
T4rk1n Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 44 additions & 26 deletions .ai/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ Special handling for Colab:
- `prevent_initial_callbacks` - Skip callbacks on load (default: `False`)
- `background_callback_manager` - DiskcacheManager or CeleryManager
- `on_error` - Global callback error handler
- `shared_storage` - Cross-process state + pub/sub backend (default: `LocalSharedStorage`). Pass `None` to disable, or a `BaseSharedStorage` subclass/instance to swap. See [Shared Storage](#shared-storage).
- `shared_storage` - Cross-process state + pub/sub backend (default: off). Pass `LocalSharedStorage` to enable, or a `BaseSharedStorage` subclass/instance to swap. See [Shared Storage](#shared-storage).

**WebSocket Callbacks:**
- `websocket_callbacks` - Enable WebSocket for all callbacks (default: `False`). Requires FastAPI backend.
Expand Down Expand Up @@ -820,7 +820,9 @@ channel. Unlike `dcc.Store` (which lives in the browser), shared storage lives
on the server and lets callbacks running in different workers see the same keys
and topics without standing up an external service like Redis.

Enabled by default on every app. It starts lazily on first use, so it costs
Opt-in: off unless the app passes `shared_storage=` or `DASH_SHARED_STORAGE`
is set, so existing deployments do not grow a socket owner or a streaming
downlink on upgrade. Once enabled it starts lazily on first use, so it costs
nothing until touched.

### Accessing It
Expand All @@ -836,13 +838,21 @@ def handler(n):
return store.get("clicks", 0)

# Or off the app directly:
app = dash.Dash()
app = dash.Dash(shared_storage=dash.LocalSharedStorage)
app.shared_storage.set("key", {"any": "json-compatible value"})
```

Values (and published messages) must be **JSON-compatible** — dict / list / str
/ int / float / bool / None — the same constraint as `dcc.Store` and callback
outputs. Messages are encoded with `msgspec` (msgpack), a hard dependency.
outputs. Messages are encoded with `msgspec` (msgpack) from the
`dash[shared_storage]` extra (also pulled in by `dash[diskcache]` and
`dash[redis]`). Without it, `LocalSharedStorage` in `"memory"` mode still works
and its socket transport falls back to stdlib JSON (both ends run the same
install, so they always agree). JSON turns non-string dict keys into strings and
cannot carry `bytes`, so keep values strictly JSON-shaped. Everything that
writes encoded bytes somewhere lasting (the persistent modes, Diskcache, Redis)
raises an `ImportError` naming the extra that brings it instead, so a stored
format never depends on what happens to be installed.

### API

Expand Down Expand Up @@ -895,7 +905,7 @@ more than one container/pod (see [Deployment Topology](#deployment-topology-and-

| Backend | Backing store | Reaches | Extra |
|---------|---------------|---------|-------|
| `LocalSharedStorage` *(default)* | in-memory, owner-elected socket | one **container** (all its workers) | none |
| `LocalSharedStorage` | in-memory, owner-elected socket | one **container** (all its workers) | none (`dash[shared_storage]` for the persistent modes) |
| `DiskcacheSharedStorage` | a `diskcache.Cache` | one **host** (all processes sharing the dir) | `dash[diskcache]` |
| `RedisSharedStorage` | Redis (Streams for pub/sub) | **any** process/container/pod | `dash[redis]` |

Expand All @@ -909,14 +919,21 @@ from dash._shared_storage import (
LocalSharedStorage, DiskcacheSharedStorage, RedisSharedStorage,
)

Dash(shared_storage=LocalSharedStorage) # default
Dash() # disabled (default)
Dash(shared_storage=LocalSharedStorage)
Dash(shared_storage=DiskcacheSharedStorage(directory="/tmp/ss"))
Dash(shared_storage=RedisSharedStorage(url="redis://host:6379"))
Dash(shared_storage=None) # disabled
```

When disabled, accessing `app.shared_storage` / `dash.ctx.shared_storage`
raises `SharedStorageError`.
raises `SharedStorageError`. Streaming callbacks over HTTP need it: server setup
raises `StreamCallbackError` naming them (`_validate.validate_stream_callbacks`),
so an app cannot stream on a fallback locally and then break once deployed.
WebSocket streaming callbacks (`websocket_callbacks=True` or `websocket=True`
on a backend with `websocket_capability`, i.e. FastAPI or Quart) send frames on
the socket and do not need it; on Flask those flags change nothing and the check
still applies. Browsers without `SharedWorker` do not use the WebSocket
transport and stream over HTTP, one connection per stream.

**`LocalSharedStorage`** — in-memory, no external service. On first use every
worker races to become the single **owner** for the machine by binding a stable
Expand Down Expand Up @@ -993,7 +1010,7 @@ subscriptions likewise poll without blocking and sleep on the loop.

### Deployment Topology and Shared Storage

The default `LocalSharedStorage` is **per-container**: its socket/loopback
`LocalSharedStorage` is **per-container**: its socket/loopback
election only reaches processes in the same network + filesystem namespace.

- **Single process / single pod** (e.g. Plotly Cloud apps): fine — one owner,
Expand All @@ -1009,16 +1026,16 @@ election only reaches processes in the same network + filesystem namespace.
A hosting platform can switch the backend without editing the app through
`DASH_SHARED_STORAGE` (`_shared_storage/_env.py`), read only when the app did
not pass `shared_storage=` (the default is a sentinel, so an explicit argument,
`None` included, always wins): `local`, `none`, `diskcache:///abs/path`, or a
`redis://` / `rediss://` URL. A `key_prefix` query parameter on the Redis URL
(`redis://host/0?key_prefix=myapp`) sets `RedisSharedStorage(key_prefix=)` and
is stripped before the URL reaches redis-py; the diskcache path is
percent-decoded. `cluster://` is reserved and raises; anything else raises
`InvalidConfig` at construction, without echoing the value (it can hold
credentials). The value becomes a zero-argument
factory, so nothing is built or connected until `app.shared_storage` is first
read, but a missing extra (`dash[redis]`, `dash[diskcache]`) fails at
construction with the backend's own `ImportError`.
`None` included, always wins): `local`, `none` (same as unset or empty),
`diskcache:///abs/path`, or a `redis://` / `rediss://` URL. A `key_prefix` query
parameter on the Redis URL (`redis://host/0?key_prefix=myapp`) sets
`RedisSharedStorage(key_prefix=)` and is stripped before the URL reaches
redis-py; the diskcache path is percent-decoded. `cluster://` is reserved and
raises; anything else raises `InvalidConfig` at construction, without echoing
the value (it can hold credentials). The value becomes a zero-argument factory,
so nothing is built or connected until `app.shared_storage` is first read, but a
missing extra (`dash[redis]`, `dash[diskcache]`, `dash[shared_storage]`) fails
at construction with the backend's own `ImportError`.

### Custom and Out-of-Tree Backends

Expand All @@ -1039,12 +1056,13 @@ and returns a `Subscription` (`__iter__` / `__aiter__` / `close`) that raises
top-level `dash`; the poll-loop helpers (`PollResult`, the polling subscription)
are private and not part of the contract.

Core only ships backends whose dependency is already a Dash extra (`msgspec`
base; `dash[diskcache]`; `dash[redis]`). Anything needing a heavier dependency
belongs **out of tree** behind this same interface — e.g. a Postgres backend
(`LISTEN`/`NOTIFY` for push pub/sub + a table for KV and replay) lives in its own
package so a `psycopg` connection is never pulled into Dash core. It plugs in the
same way as a built-in: `Dash(shared_storage=PostgresSharedStorage(...))`.
Core only ships backends whose dependency is already a Dash extra
(`dash[shared_storage]`; `dash[diskcache]`; `dash[redis]`). Anything needing a
heavier dependency belongs **out of tree** behind this same interface — e.g. a
Postgres backend (`LISTEN`/`NOTIFY` for push pub/sub + a table for KV and
replay) lives in its own package so a `psycopg` connection is never pulled into
Dash core. It plugs in the same way as a built-in:
`Dash(shared_storage=PostgresSharedStorage(...))`.

### Module Layout

Expand Down Expand Up @@ -1412,7 +1430,7 @@ with an empty result on the terminal frame.

### Multiplexed downlink and the stream SharedWorker

When the app has a shared-storage backend (the default `LocalSharedStorage`),
When the app has a shared-storage backend (opt-in, see Shared Storage),
HTTP streams do not each hold their own response. The callback's POST carries
`streamConnection: {requestId}` and returns a fast ack; a *pump*
(`dash/_stream_hub.py`) drives the generator as an asyncio task and publishes
Expand Down
21 changes: 14 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ updates:
patterns:
- "*"
ignore:
# Majors across these build helpers and fixtures (eslint 10, prettier 3,
# webpack-cli 7, react-jsx-parser 2, ...) break the build or the pinned
# eslint 8 / prettier 2 toolchain. Take minors and patches only.
- dependency-name: "*"
update-types: ["version-update:semver-major"]
# dash-jupyterlab supports JupyterLab 2, 3 and 4 via a wide range;
# Dependabot narrows it to the latest 4.x and the lockfile bump breaks tsc.
- dependency-name: "@jupyterlab/*"
# react-docgen is pinned for legacy metadata extraction (extract-meta.js uses
# the react-docgen 5 API) and must never be updated.
- dependency-name: "react-docgen"
Expand Down Expand Up @@ -260,15 +268,14 @@ updates:
# jupyterlab is pinned <4.0.0: jupyterlab 4.x ships jlpm as Yarn Berry, which
# cannot consume @plotly/dash-jupyterlab's Yarn 1 lockfile and breaks the build.
#
# black >=25.12.0 and pylint >=4.0.0 require Python >=3.10 and cannot install
# on the 3.9 CI jobs (Dash still supports Python 3.9). Upgrading either past
# its last 3.9-compatible release (black 25.11.0, pylint 3.3.9) also forces a
# repo-wide reformat / lint-fix pass, so linter bumps are done deliberately,
# not via Dependabot.
# Linters are held entirely. black >=25.12.0, pylint >=4.0.0 and flake8
# >=7.4.0 require Python >=3.10 and cannot install on the 3.9 CI jobs (Dash
# still supports Python 3.9). Even the last 3.9-compatible black / pylint
# force a repo-wide reformat / lint-fix pass, so linter bumps are done
# deliberately, not via Dependabot.
ignore:
- dependency-name: "jupyterlab"
versions: [">=4.0.0"]
- dependency-name: "black"
versions: [">=25.12.0"]
- dependency-name: "pylint"
versions: [">=4.0.0"]
- dependency-name: "flake8"
2 changes: 1 addition & 1 deletion .github/workflows/post-test-status.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ jobs:
const sha = context.payload.workflow_run.head_sha;
const statusContext = 'percy/dash';

const { data: { statuses } } = await github.rest.repos.getCombinedStatus({
const { data: { statuses } } = await github.rest.repos.getCombinedStatusForRef({
owner,
repo,
ref: sha,
Expand Down
Loading
Loading