Skip to content

Commit 8c2329f

Browse files
committed
add configuration for heartbeat and proper te\sts for reconnect
1 parent e15ef05 commit 8c2329f

8 files changed

Lines changed: 33 additions & 6 deletions

File tree

.ai/ARCHITECTURE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,12 +905,14 @@ app = Dash(
905905
server=server,
906906
websocket_callbacks=True,
907907
websocket_inactivity_timeout=300000, # 5 minutes (default)
908+
websocket_heartbeat_interval=30000, # 30 seconds (default)
908909
websocket_allowed_origins=['https://example.com'],
909910
)
910911
```
911912

912913
- **`websocket_callbacks`** - Enable WebSocket for all callbacks (default: `False`)
913914
- **`websocket_inactivity_timeout`** - Close WebSocket after period of inactivity in milliseconds (default: `300000` = 5 minutes). Heartbeats do not count as activity. Set to `0` to disable timeout. Connection automatically reconnects when needed.
915+
- **`websocket_heartbeat_interval`** - Interval for heartbeat/keep-alive checks in milliseconds (default: `30000` = 30 seconds). Also determines how frequently inactivity timeout is checked.
914916
- **`websocket_allowed_origins`** - List of allowed origins for WebSocket connections (security)
915917

916918
### Architecture

@plotly/dash-websocket-worker/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export interface ConnectMessage extends WorkerMessage {
4040
payload: {
4141
serverUrl: string;
4242
inactivityTimeout?: number;
43+
heartbeatInterval?: number;
4344
};
4445
}
4546

@plotly/dash-websocket-worker/src/worker.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,16 +81,24 @@ self.onconnect = (event: MessageEvent) => {
8181
const rendererId = connectMsg.rendererId;
8282
const newServerUrl = connectMsg.payload.serverUrl;
8383
const inactivityTimeout = connectMsg.payload.inactivityTimeout;
84+
const heartbeatInterval = connectMsg.payload.heartbeatInterval;
8485

8586
// Register the renderer
8687
router.registerRenderer(rendererId, port);
8788
rendererIds.add(rendererId);
8889

89-
console.log(`[DashWSWorker] Renderer ${rendererId} connected, inactivityTimeout: ${inactivityTimeout}`);
90+
console.log(`[DashWSWorker] Renderer ${rendererId} connected, inactivityTimeout: ${inactivityTimeout}, heartbeatInterval: ${heartbeatInterval}`);
9091

91-
// Update inactivity timeout if provided
92+
// Update config if provided
93+
const configUpdate: {inactivityTimeout?: number; heartbeatInterval?: number} = {};
9294
if (typeof inactivityTimeout === 'number') {
93-
wsManager.setConfig({ inactivityTimeout });
95+
configUpdate.inactivityTimeout = inactivityTimeout;
96+
}
97+
if (typeof heartbeatInterval === 'number') {
98+
configUpdate.heartbeatInterval = heartbeatInterval;
99+
}
100+
if (Object.keys(configUpdate).length > 0) {
101+
wsManager.setConfig(configUpdate);
94102
}
95103

96104
// Connect to server if not already connected

dash/dash-renderer/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type DashConfig = {
2727
url: string;
2828
worker_url: string;
2929
inactivity_timeout?: number;
30+
heartbeat_interval?: number;
3031
};
3132
csrf_token_name?: string;
3233
csrf_header_name?: string;

dash/dash-renderer/src/utils/workerClient.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,13 @@ class WorkerClient {
8787
* @param workerUrl URL to the SharedWorker script
8888
* @param serverUrl WebSocket server URL
8989
* @param inactivityTimeout Optional inactivity timeout in ms
90+
* @param heartbeatInterval Optional heartbeat interval in ms
9091
*/
9192
public async connect(
9293
workerUrl: string,
9394
serverUrl: string,
94-
inactivityTimeout?: number
95+
inactivityTimeout?: number,
96+
heartbeatInterval?: number
9597
): Promise<void> {
9698
if (this.worker) {
9799
// Already connected
@@ -120,7 +122,8 @@ class WorkerClient {
120122
rendererId: this.rendererId,
121123
payload: {
122124
serverUrl,
123-
inactivityTimeout
125+
inactivityTimeout,
126+
heartbeatInterval
124127
}
125128
});
126129

@@ -160,6 +163,7 @@ class WorkerClient {
160163
url?: string;
161164
worker_url?: string;
162165
inactivity_timeout?: number;
166+
heartbeat_interval?: number;
163167
};
164168
}): Promise<void> {
165169
// Already connected
@@ -191,7 +195,8 @@ class WorkerClient {
191195
await this.connect(
192196
config.websocket.worker_url,
193197
wsUrl,
194-
config.websocket.inactivity_timeout
198+
config.websocket.inactivity_timeout,
199+
config.websocket.heartbeat_interval
195200
);
196201
}
197202

dash/dash.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def __init__( # pylint: disable=too-many-statements, too-many-branches
488488
websocket_callbacks: Optional[bool] = False,
489489
websocket_allowed_origins: Optional[List[str]] = None,
490490
websocket_inactivity_timeout: Optional[int] = 300000,
491+
websocket_heartbeat_interval: Optional[int] = 30000,
491492
websocket_batch_delay: Optional[float] = 0.005,
492493
**obsolete,
493494
):
@@ -648,6 +649,7 @@ def __init__( # pylint: disable=too-many-statements, too-many-branches
648649
self._websocket_callbacks = websocket_callbacks
649650
self._websocket_allowed_origins = websocket_allowed_origins or []
650651
self._websocket_inactivity_timeout = websocket_inactivity_timeout
652+
self._websocket_heartbeat_interval = websocket_heartbeat_interval
651653
self._websocket_batch_delay = websocket_batch_delay
652654

653655
self.logger = logging.getLogger(__name__)
@@ -998,6 +1000,7 @@ def _config(self):
9981000
"url": self.config.requests_pathname_prefix + "_dash-ws-callback",
9991001
"worker_url": self._get_worker_url(),
10001002
"inactivity_timeout": self._websocket_inactivity_timeout,
1003+
"heartbeat_interval": self._websocket_heartbeat_interval,
10011004
}
10021005

10031006
return config

tests/websocket/test_ws_inactivity.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def test_ws020_inactivity_timeout_closes(dash_duo):
1919
backend="fastapi",
2020
websocket_callbacks=True,
2121
websocket_inactivity_timeout=3000, # 3 seconds for testing
22+
websocket_heartbeat_interval=1000, # 1 second - check inactivity frequently
2223
)
2324

2425
app.layout = html.Div(
@@ -54,6 +55,7 @@ def test_ws021_activity_resets_timer(dash_duo):
5455
backend="fastapi",
5556
websocket_callbacks=True,
5657
websocket_inactivity_timeout=4000, # 4 seconds
58+
websocket_heartbeat_interval=1000, # 1 second - check inactivity frequently
5759
)
5860

5961
app.layout = html.Div(
@@ -88,6 +90,7 @@ def test_ws022_quick_successive_callbacks(dash_duo):
8890
backend="fastapi",
8991
websocket_callbacks=True,
9092
websocket_inactivity_timeout=5000,
93+
websocket_heartbeat_interval=1000, # 1 second - check inactivity frequently
9194
)
9295

9396
app.layout = html.Div(
@@ -121,6 +124,7 @@ def test_ws023_auto_reconnect_after_timeout(dash_duo):
121124
backend="fastapi",
122125
websocket_callbacks=True,
123126
websocket_inactivity_timeout=2000, # 2 seconds
127+
websocket_heartbeat_interval=1000, # 1 second - check inactivity frequently
124128
)
125129

126130
app.layout = html.Div(
@@ -164,6 +168,7 @@ def test_ws024_long_callback_doesnt_timeout(dash_duo):
164168
backend="fastapi",
165169
websocket_callbacks=True,
166170
websocket_inactivity_timeout=3000, # 3 seconds
171+
websocket_heartbeat_interval=1000, # 1 second - check inactivity frequently
167172
)
168173

169174
app.layout = html.Div(

tests/websocket/test_ws_reconnect.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def test_ws033_callback_after_reconnect(dash_duo):
118118
backend="fastapi",
119119
websocket_callbacks=True,
120120
websocket_inactivity_timeout=2000, # 2 seconds
121+
websocket_heartbeat_interval=1000, # 1 second - check inactivity frequently
121122
)
122123

123124
app.layout = html.Div(
@@ -211,6 +212,7 @@ def test_ws035_long_callback_with_shutdown_check(dash_duo):
211212
backend="fastapi",
212213
websocket_callbacks=True,
213214
websocket_inactivity_timeout=2000, # 2 seconds
215+
websocket_heartbeat_interval=1000, # 1 second - check inactivity frequently
214216
)
215217

216218
app.layout = html.Div(

0 commit comments

Comments
 (0)