|
48 | 48 | ) |
49 | 49 | from .backends import get_backend |
50 | 50 | from .version import __version__ |
51 | | -from ._configs import get_combined_config, pathname_configs, pages_folder_config |
| 51 | +from ._configs import ( |
| 52 | + get_combined_config, |
| 53 | + load_dash_env_vars, |
| 54 | + pathname_configs, |
| 55 | + pages_folder_config, |
| 56 | +) |
52 | 57 | from ._utils import ( |
53 | 58 | AttributeDict, |
54 | 59 | format_tag, |
|
76 | 81 | from . import backends |
77 | 82 |
|
78 | 83 | from ._get_app import with_app_context, with_app_context_factory |
79 | | -from ._shared_storage import ( |
80 | | - BaseSharedStorage, |
81 | | - LocalSharedStorage, |
82 | | - SharedStorageError, |
83 | | -) |
| 84 | +from ._shared_storage import BaseSharedStorage, SharedStorageError |
| 85 | +from ._shared_storage._env import storage_from_env |
84 | 86 | from ._grouping import map_grouping, grouping_len, update_args_group |
85 | 87 | from ._obsolete import ObsoleteChecker |
86 | 88 | from ._callback_context import callback_context |
|
154 | 156 | _ID_DUMMY = "_pages_dummy" |
155 | 157 |
|
156 | 158 | _UNINITIALIZED = object() # Sentinel for tracking init_app state |
| 159 | +# Default for ``shared_storage`` so "not passed" (DASH_SHARED_STORAGE may pick |
| 160 | +# the backend) can be told apart from an explicit argument. |
| 161 | +_SHARED_STORAGE_DEFAULT: Any = object() |
157 | 162 |
|
158 | 163 | DASH_VERSION_URL = "https://dash-version.plotly.com:8080/current_version" |
159 | 164 |
|
@@ -470,6 +475,20 @@ class Dash(ObsoleteChecker): |
470 | 475 | takes a thread for milliseconds. ASGI backends (Quart, FastAPI) keep |
471 | 476 | one open connection per browser instead and ignore this. |
472 | 477 | :type stream_poll_interval: int |
| 478 | +
|
| 479 | + :param shared_storage: Backend for ``dash.ctx.shared_storage`` and the |
| 480 | + streaming-callback transport: a ``BaseSharedStorage`` subclass or |
| 481 | + instance, or ``None`` to disable. Default ``LocalSharedStorage`` (one |
| 482 | + machine or pod). When not passed, the ``DASH_SHARED_STORAGE`` |
| 483 | + environment variable can choose it: ``local``, ``none``, |
| 484 | + ``diskcache:///absolute/path``, or a ``redis://`` / ``rediss://`` URL. |
| 485 | + An explicit argument, including ``None``, always wins. |
| 486 | +
|
| 487 | + Stream requests are signed per page load, so with several workers or |
| 488 | + pods they all need the same signing secret: set ``server.secret_key`` |
| 489 | + or the ``DASH_SECRET_KEY`` environment variable. ``DASH_SECRET_KEY`` is |
| 490 | + used for Dash's own signing only and does not set ``server.secret_key``. |
| 491 | + :type shared_storage: BaseSharedStorage subclass or instance, or None |
473 | 492 | """ |
474 | 493 |
|
475 | 494 | _plotlyjs_url: str |
@@ -535,7 +554,7 @@ def __init__( # pylint: disable=too-many-statements, too-many-branches |
535 | 554 | stream_poll_interval: int = 100, |
536 | 555 | shared_storage: Optional[ |
537 | 556 | Union[Type[BaseSharedStorage], BaseSharedStorage] |
538 | | - ] = LocalSharedStorage, |
| 557 | + ] = _SHARED_STORAGE_DEFAULT, |
539 | 558 | enable_mcp: Optional[bool] = None, |
540 | 559 | mcp_path: Optional[str] = None, |
541 | 560 | **obsolete, |
@@ -724,6 +743,10 @@ def __init__( # pylint: disable=too-many-statements, too-many-branches |
724 | 743 | # access so it costs nothing until used and never binds in a gunicorn |
725 | 744 | # preload master or the Flask reloader parent -- only in the worker that |
726 | 745 | # actually touches it. |
| 746 | + if shared_storage is _SHARED_STORAGE_DEFAULT: |
| 747 | + shared_storage = storage_from_env( |
| 748 | + load_dash_env_vars().get("DASH_SHARED_STORAGE") |
| 749 | + ) |
727 | 750 | self._shared_storage_arg = shared_storage |
728 | 751 | self._shared_storage_instance: Optional[BaseSharedStorage] = None |
729 | 752 | self._shared_storage_lock = threading.Lock() |
@@ -753,9 +776,8 @@ def __init__( # pylint: disable=too-many-statements, too-many-branches |
753 | 776 | self._pages_lock = threading.Lock() |
754 | 777 | self._pages_async_lock: Optional[asyncio.Lock] = None |
755 | 778 |
|
756 | | - # Secret used to sign background-callback handles (see _callback_signing). |
757 | | - # Prefer the Flask/Quart secret_key (shared across workers when the |
758 | | - # operator sets one); otherwise fall back to a per-process random secret. |
| 779 | + # Fallback signing secret, used when neither server.secret_key nor |
| 780 | + # DASH_SECRET_KEY is set. See _get_signing_secret. |
759 | 781 | self._generated_signing_secret: Optional[bytes] = None |
760 | 782 |
|
761 | 783 | if server: |
@@ -1022,7 +1044,7 @@ def shared_storage(self) -> BaseSharedStorage: |
1022 | 1044 | with self._shared_storage_lock: |
1023 | 1045 | if self._shared_storage_instance is None: |
1024 | 1046 | storage = self._shared_storage_arg |
1025 | | - if isinstance(storage, type): |
| 1047 | + if isinstance(storage, (type, functools.partial)): |
1026 | 1048 | storage = storage() |
1027 | 1049 | storage.start() |
1028 | 1050 | self._shared_storage_instance = storage |
@@ -1071,21 +1093,29 @@ def serve_layout(self): |
1071 | 1093 | ) |
1072 | 1094 |
|
1073 | 1095 | def _get_signing_secret(self) -> bytes: |
1074 | | - """Return the secret used to sign background-callback handles. |
| 1096 | + """Return the secret used to sign page tokens (``end_id``), background |
| 1097 | + callback handles and stream connections. |
1075 | 1098 |
|
1076 | 1099 | Resolution order: |
1077 | 1100 |
|
1078 | 1101 | 1. The server's ``secret_key`` if set (shared across workers when the |
1079 | 1102 | operator configures one, e.g. for Flask-Login). |
1080 | | - 2. Otherwise a random secret persisted in the background-callback result |
| 1103 | + 2. ``DASH_SECRET_KEY`` from the environment, so a hosting platform can |
| 1104 | + give every worker and pod the same key without editing the app. Used |
| 1105 | + for Dash's own signing only, never assigned to ``server.secret_key`` |
| 1106 | + (that would change Flask session behavior). |
| 1107 | + 3. Otherwise a random secret persisted in the background-callback result |
1081 | 1108 | store, so every worker reads back the same value. This is exactly as |
1082 | 1109 | shared as the callback results themselves, so it works cross-worker |
1083 | 1110 | whenever the deployment is set up for multi-worker background |
1084 | 1111 | callbacks (an explicitly shared cache / broker). |
1085 | | - 3. Finally, if no background manager is available, a per-process random |
1086 | | - secret (there are no background handles to verify in that case). |
| 1112 | + 4. Finally, if no background manager is available, a per-process random |
| 1113 | + secret. Stream tokens then only verify on the worker that issued |
| 1114 | + them, so multi-worker streaming apps need 1 or 2. |
1087 | 1115 | """ |
1088 | | - key = getattr(self.server, "secret_key", None) |
| 1116 | + key = getattr(self.server, "secret_key", None) or load_dash_env_vars().get( |
| 1117 | + "DASH_SECRET_KEY" |
| 1118 | + ) |
1089 | 1119 | if key: |
1090 | 1120 | return key.encode("utf-8") if isinstance(key, str) else key |
1091 | 1121 | if self._generated_signing_secret is None: |
|
0 commit comments