Skip to content

Commit 4b4319d

Browse files
committed
⚡️(frontend) add jitter to WS reconnection
When a massive simultaneous disconnection occurs (e.g. infra restart), all clients would reconnect and invalidate their queries at exactly the same time, causing a possible DB spike. Adding random jitter spreads these events over a time window so the load is absorbed gradually.
1 parent 8df86e6 commit 4b4319d

2 files changed

Lines changed: 40 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ and this project adheres to
1919

2020
### Fixed
2121

22+
- ⚡️(frontend) add jitter to WS reconnection #2162
2223
- 🐛(frontend) fix tree pagination #2145
2324
- 🐛(nginx) add page reconciliation on nginx #2154
2425

src/frontend/apps/impress/src/features/docs/doc-management/stores/useProviderStore.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,17 @@ const defaultValues = {
3030

3131
type ExtendedCloseEvent = CloseEvent & { wasClean: boolean };
3232

33+
/**
34+
* When a massive simultaneous disconnection occurs (e.g. infra restart), all
35+
* clients would reconnect and invalidate their queries at exactly the same
36+
* time, causing a possible DB spike. Adding random jitter spreads these events over a
37+
* time window so the load is absorbed gradually.
38+
*/
39+
const RECONNECT_BASE_DELAY_MS = 1000;
40+
const RECONNECT_JITTER_MAX_MS = 3000;
41+
3342
let reconnectTimeout: ReturnType<typeof setTimeout> | undefined;
43+
let lostConnectionTimeout: ReturnType<typeof setTimeout> | undefined;
3444

3545
export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
3646
...defaultValues,
@@ -63,7 +73,14 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
6373
}
6474

6575
clearTimeout(reconnectTimeout);
66-
reconnectTimeout = setTimeout(() => void provider.connect(), 1000);
76+
77+
// Jitter spreading for reconnection attempts
78+
// Math.random() generates a random delay to avoid all clients
79+
// reconnecting at the same time
80+
reconnectTimeout = setTimeout(
81+
() => void provider.connect(),
82+
RECONNECT_BASE_DELAY_MS + Math.random() * RECONNECT_JITTER_MAX_MS,
83+
);
6784
}
6885
},
6986
onAuthenticationFailed() {
@@ -73,13 +90,30 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
7390
set({ isReady: true, isConnected: true });
7491
},
7592
onStatus: ({ status }) => {
76-
set((state) => {
77-
const nextConnected = status === WebSocketStatus.Connected;
93+
const isConnected = status === WebSocketStatus.Connected;
94+
const wasConnected = get().isConnected;
7895

96+
if (isConnected) {
97+
clearTimeout(lostConnectionTimeout);
98+
}
99+
// If we were previously connected and now we're not,
100+
// we might have lost the connection
101+
else if (wasConnected) {
102+
clearTimeout(lostConnectionTimeout);
103+
// Jitter spreading for reconnection attempts
104+
// Math.random() generates a random delay to avoid all clients
105+
// reconnecting at the same time
106+
lostConnectionTimeout = setTimeout(
107+
() => set({ hasLostConnection: true }),
108+
Math.random() * RECONNECT_JITTER_MAX_MS,
109+
);
110+
}
111+
112+
set((state) => {
79113
/**
80114
* status === WebSocketStatus.Connected does not mean we are totally connected
81115
* because authentication can still be in progress and failed
82-
* So we only update isConnected when we loose the connection
116+
* So we only update isConnected when we lose the connection
83117
*/
84118
const connected =
85119
status !== WebSocketStatus.Connected
@@ -91,10 +125,6 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
91125
return {
92126
...connected,
93127
isReady: state.isReady || status === WebSocketStatus.Disconnected,
94-
hasLostConnection:
95-
state.isConnected && !nextConnected
96-
? true
97-
: state.hasLostConnection,
98128
};
99129
});
100130
},
@@ -123,6 +153,7 @@ export const useProviderStore = create<UseCollaborationStore>((set, get) => ({
123153
},
124154
destroyProvider: () => {
125155
clearTimeout(reconnectTimeout);
156+
clearTimeout(lostConnectionTimeout);
126157
const provider = get().provider;
127158
if (provider) {
128159
provider.destroy();

0 commit comments

Comments
 (0)