Skip to content

Commit a510107

Browse files
thomasballingerConvex, Inc.
authored and
Convex, Inc.
committed
Longer backoffs for specific WebSocket disconnect reasons (#37778)
Explicit longer timeouts for certain kinds of WebSocket disconnect-causing server errors GitOrigin-RevId: b99c88d312e36eedf44979e7692b228ce6cade53
1 parent bdaad1e commit a510107

File tree

1 file changed

+55
-16
lines changed

1 file changed

+55
-16
lines changed

npm-packages/convex/src/browser/sync/web_socket_manager.ts

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,42 @@ export type OnMessageResponse = {
9393
hasSyncedPastLastReconnect: boolean;
9494
};
9595

96+
const serverDisconnectErrors = {
97+
// A known error, e.g. during a restart or push
98+
InternalServerError: { timeout: 100 },
99+
// ErrorMetadata::overloaded() messages that we realy should back off
100+
SubscriptionsWorkerFullError: { timeout: 3000 },
101+
TooManyConcurrentRequests: { timeout: 3000 },
102+
CommitterFullError: { timeout: 3000 },
103+
AwsTooManyRequestsException: { timeout: 3000 },
104+
ExecuteFullError: { timeout: 3000 },
105+
SystemTimeoutError: { timeout: 3000 },
106+
ExpiredInQueue: { timeout: 3000 },
107+
// More ErrorMetadata::overloaded() that typically indicate a deploy just happened
108+
VectorIndexesUnavailable: { timeout: 1000 },
109+
SearchIndexesUnavailable: { timeout: 1000 },
110+
// More ErrorMeatadata::overloaded()
111+
VectorIndexTooLarge: { timeout: 3000 },
112+
SearchIndexTooLarge: { timeout: 3000 },
113+
TooManyWritesInTimePeriod: { timeout: 3000 },
114+
} as const satisfies Record<string, { timeout: number }>;
115+
116+
type ServerDisconnectError = keyof typeof serverDisconnectErrors | "Unknown";
117+
118+
function classifyDisconnectError(s?: string): ServerDisconnectError {
119+
if (s === undefined) return "Unknown";
120+
// startsWith so more info could be at the end (although currently there isn't)
121+
122+
for (const prefix of Object.keys(
123+
serverDisconnectErrors,
124+
) as ServerDisconnectError[]) {
125+
if (s.startsWith(prefix)) {
126+
return prefix;
127+
}
128+
}
129+
return "Unknown";
130+
}
131+
96132
/**
97133
* A wrapper around a websocket that handles errors, reconnection, and message
98134
* parsing.
@@ -102,10 +138,14 @@ export class WebSocketManager {
102138

103139
private connectionCount: number;
104140
private _hasEverConnected: boolean = false;
105-
private lastCloseReason: string | null;
141+
private lastCloseReason:
142+
| "InitialConnect"
143+
| "OnCloseInvoked"
144+
| (string & {}) // a full serverErrorReason (not just the prefix) or a new one
145+
| null;
106146

107147
/** Upon HTTPS/WSS failure, the first jittered backoff duration, in ms. */
108-
private readonly initialBackoff: number;
148+
private readonly defaultInitialBackoff: number;
109149

110150
/** We backoff exponentially, but we need to cap that--this is the jittered max. */
111151
private readonly maxBackoff: number;
@@ -143,7 +183,8 @@ export class WebSocketManager {
143183
this.connectionCount = 0;
144184
this.lastCloseReason = "InitialConnect";
145185

146-
this.initialBackoff = 100;
186+
// backoff for unknown errors
187+
this.defaultInitialBackoff = 100;
147188
this.maxBackoff = 16000;
148189
this.retries = 0;
149190

@@ -253,11 +294,8 @@ export class WebSocketManager {
253294
}
254295
this.logger.log(msg);
255296
}
256-
if (event.reason?.includes("SubscriptionsWorkerFullError")) {
257-
this.scheduleReconnect("SubscriptionsWorkerFullError");
258-
} else {
259-
this.scheduleReconnect("unknown");
260-
}
297+
const reason = classifyDisconnectError(event.reason);
298+
this.scheduleReconnect(reason);
261299
return;
262300
};
263301
}
@@ -324,9 +362,7 @@ export class WebSocketManager {
324362
}, this.serverInactivityThreshold);
325363
}
326364

327-
private scheduleReconnect(
328-
reason: "client" | "unknown" | "SubscriptionsWorkerFullError",
329-
) {
365+
private scheduleReconnect(reason: "client" | ServerDisconnectError) {
330366
this.socket = { state: "disconnected" };
331367
const backoff = this.nextBackoff(reason);
332368
this.logger.log(`Attempting reconnect in ${backoff}ms`);
@@ -549,11 +585,14 @@ export class WebSocketManager {
549585
this.logger.logVerbose(message);
550586
}
551587

552-
private nextBackoff(
553-
reason: "client" | "unknown" | "SubscriptionsWorkerFullError",
554-
): number {
555-
const initialBackoff =
556-
reason === "SubscriptionsWorkerFullError" ? 3000 : this.initialBackoff;
588+
private nextBackoff(reason: "client" | ServerDisconnectError): number {
589+
const initialBackoff: number =
590+
reason === "client"
591+
? this.defaultInitialBackoff
592+
: reason === "Unknown"
593+
? this.defaultInitialBackoff
594+
: serverDisconnectErrors[reason].timeout;
595+
557596
const baseBackoff = initialBackoff * Math.pow(2, this.retries);
558597
this.retries += 1;
559598
const actualBackoff = Math.min(baseBackoff, this.maxBackoff);

0 commit comments

Comments
 (0)