Skip to content

fix(WebSocketShard): Add RESUMED and READY timeout #8759

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
93 changes: 91 additions & 2 deletions packages/discord.js/src/client/websocket/WebSocketShard.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ class WebSocketShard extends EventEmitter {
*/
Object.defineProperty(this, 'helloTimeout', { value: null, writable: true });

/**
* The RESUMED dispatch timeout
* @name WebSocketShard#resumedDispatchTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'resumedDispatchTimeout', { value: null, writable: true });

/**
* The WebSocket timeout.
* @name WebSocketShard#wsCloseTimeout
Expand Down Expand Up @@ -174,13 +182,29 @@ class WebSocketShard extends EventEmitter {
*/
Object.defineProperty(this, 'readyTimeout', { value: null, writable: true });

/**
* The READY dispatch event timeout
* @name WebSocketShard#readyDispatchTimeout
* @type {?NodeJS.Timeout}
* @private
*/
Object.defineProperty(this, 'readyDispatchTimeout', { value: null, writable: true });

/**
* Time when the WebSocket connection was opened
* @name WebSocketShard#connectedAt
* @type {number}
* @private
*/
Object.defineProperty(this, 'connectedAt', { value: 0, writable: true });

/**
* Time when the last replayed event was received
* @name WebSocketShard#lastReplayedAt
* @type {number}
* @private
*/
Object.defineProperty(this, 'lastReplayedAt', { value: 0, writable: true });
}

/**
Expand Down Expand Up @@ -428,6 +452,7 @@ class WebSocketShard extends EventEmitter {
this.expectedGuilds = new Set(packet.d.guilds.map(d => d.id));
this.status = Status.WaitingForGuilds;
this.debug(`[READY] Session ${this.sessionId} | Resume url ${this.resumeURL}.`);
this.setReadyDispatchTimeout(-1);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ReadyHeartbeat');
break;
Expand All @@ -441,10 +466,15 @@ class WebSocketShard extends EventEmitter {
this.status = Status.Ready;
const replayed = packet.s - this.closeSequence;
this.debug(`[RESUMED] Session ${this.sessionId} | Replayed ${replayed} events.`);
this.setResumedDispatchTimeout(-1);
this.lastHeartbeatAcked = true;
this.sendHeartbeat('ResumeHeartbeat');
break;
}
default: {
if (this.status === Status.Resuming) this.lastReplayedAt = Date.now();
break;
}
}

if (packet.s > this.sequence) this.sequence = packet.s;
Expand All @@ -461,6 +491,12 @@ class WebSocketShard extends EventEmitter {
break;
case GatewayOpcodes.InvalidSession:
this.debug(`[INVALID SESSION] Resumable: ${packet.d}.`);

// Clear the timers
this.setReadyDispatchTimeout(-1);
this.setResumedDispatchTimeout(-1);
clearTimeout(this.readyTimeout);

// If we can resume the session, do so immediately
if (packet.d) {
this.identifyResume();
Expand Down Expand Up @@ -565,7 +601,53 @@ class WebSocketShard extends EventEmitter {
this.helloTimeout = setTimeout(() => {
this.debug('Did not receive HELLO in time. Destroying and connecting again.');
this.destroy({ reset: true, closeCode: 4009 });
}, 20_000).unref();
}, 20_000);
}

/**
* Sets the RESUMED dispatch timeout.
* @param {number} [time] If set to -1, it will clear the resumed timeout
* @private
*/
setResumedDispatchTimeout(time) {
if (time === -1) {
if (this.resumedDispatchTimeout) {
this.debug('Clearing the RESUMED dispatch timeout.');
clearTimeout(this.resumedDispatchTimeout);
this.resumedDispatchTimeout = null;
}
return;
}
this.debug('Setting a RESUMED dispatch timeout for 20s.');
this.resumedDispatchTimeout = setTimeout(() => {
if ((Date.now() - this.lastReplayedAt) < 20_000) {
this.debug('Received a message within the last 20s. Delaying RESUMED timeout.');
return this.setResumedDispatchTimeout();
}
this.debug('Did not receive RESUMED in time. Destroying and connecting again.');
this.destroy({ reset: false, closeCode: 4009 });
}, 20_000);
}

/**
* Sets the READY dispatch timeout.
* @param {number} [time] If set to -1, it will clear the ready timeout
* @private
*/
setReadyDispatchTimeout(time) {
if (time === -1) {
if (this.readyDispatchTimeout) {
this.debug('Clearing the READY dispatch timeout.');
clearTimeout(this.readyDispatchTimeout);
this.readyDispatchTimeout = null;
}
return;
}
this.debug('Setting a READY dispatch timeout for 20s.');
this.readyDispatchTimeout = setTimeout(() => {
this.debug('Did not receive READY in time. Destroying and connecting again.');
this.destroy({ reset: true, closeCode: 4009 });
}, 20_000);
}

/**
Expand Down Expand Up @@ -605,7 +687,7 @@ class WebSocketShard extends EventEmitter {
this.emitClose();
// Setting the variable false to check for zombie connections.
this.closeEmitted = false;
}, time).unref();
}, time);
}

/**
Expand Down Expand Up @@ -702,6 +784,8 @@ class WebSocketShard extends EventEmitter {

this.debug(`[IDENTIFY] Shard ${this.id}/${client.options.shardCount} with intents: ${d.intents}`);
this.send({ op: GatewayOpcodes.Identify, d }, true);

this.setReadyDispatchTimeout();
}

/**
Expand All @@ -726,6 +810,8 @@ class WebSocketShard extends EventEmitter {
};

this.send({ op: GatewayOpcodes.Resume, d }, true);

this.setResumedDispatchTimeout();
}

/**
Expand Down Expand Up @@ -802,6 +888,9 @@ class WebSocketShard extends EventEmitter {
// Step 0: Remove all timers
this.setHeartbeatTimer(-1);
this.setHelloTimeout(-1);
this.setReadyDispatchTimeout(-1);
this.setResumedDispatchTimeout(-1);
clearTimeout(this.readyTimeout);

this.debug(
`[WebSocket] Destroy: Attempting to close the WebSocket. | WS State: ${
Expand Down
5 changes: 5 additions & 0 deletions packages/discord.js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3277,11 +3277,14 @@ export class WebSocketShard extends EventEmitter {
};
private connection: WebSocket | null;
private helloTimeout: NodeJS.Timeout | null;
private resumedDispatchTimeout: NodeJS.Timeout | null;
private eventsAttached: boolean;
private expectedGuilds: Set<Snowflake> | null;
private readyTimeout: NodeJS.Timeout | null;
private readyDispatchTimeout: NodeJS.Timeout | null;
private closeEmitted: boolean;
private wsCloseTimeout: NodeJS.Timeout | null;
private lastReplayedAt: number;

public manager: WebSocketManager;
public id: number;
Expand All @@ -3297,6 +3300,8 @@ export class WebSocketShard extends EventEmitter {
private onPacket(packet: unknown): void;
private checkReady(): void;
private setHelloTimeout(time?: number): void;
private setResumedDispatchTimeout(time?: number): void;
private setReadyDispatchTimeout(time?: number): void;
private setWsCloseTimeout(time?: number): void;
private setHeartbeatTimer(time: number): void;
private sendHeartbeat(): void;
Expand Down