Discord: harden gateway resilience

This commit is contained in:
Shadow
2026-01-08 16:52:34 -06:00
parent 769b76cd40
commit 9f8336c92b
3 changed files with 86 additions and 14 deletions

View File

@@ -47,6 +47,31 @@ describe("waitForDiscordGatewayStop", () => {
expect(disconnect).toHaveBeenCalledTimes(1); expect(disconnect).toHaveBeenCalledTimes(1);
}); });
it("ignores gateway errors when instructed", async () => {
const emitter = new EventEmitter();
const disconnect = vi.fn();
const onGatewayError = vi.fn();
const abort = new AbortController();
const err = new Error("transient");
const promise = waitForDiscordGatewayStop({
gateway: { emitter, disconnect },
abortSignal: abort.signal,
onGatewayError,
shouldStopOnError: () => false,
});
emitter.emit("error", err);
expect(onGatewayError).toHaveBeenCalledWith(err);
expect(disconnect).toHaveBeenCalledTimes(0);
expect(emitter.listenerCount("error")).toBe(1);
abort.abort();
await expect(promise).resolves.toBeUndefined();
expect(disconnect).toHaveBeenCalledTimes(1);
expect(emitter.listenerCount("error")).toBe(0);
});
it("resolves on abort without a gateway", async () => { it("resolves on abort without a gateway", async () => {
const abort = new AbortController(); const abort = new AbortController();

View File

@@ -15,8 +15,9 @@ export async function waitForDiscordGatewayStop(params: {
gateway?: DiscordGatewayHandle; gateway?: DiscordGatewayHandle;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
onGatewayError?: (err: unknown) => void; onGatewayError?: (err: unknown) => void;
shouldStopOnError?: (err: unknown) => boolean;
}): Promise<void> { }): Promise<void> {
const { gateway, abortSignal, onGatewayError } = params; const { gateway, abortSignal, onGatewayError, shouldStopOnError } = params;
const emitter = gateway?.emitter; const emitter = gateway?.emitter;
return await new Promise<void>((resolve, reject) => { return await new Promise<void>((resolve, reject) => {
let settled = false; let settled = false;
@@ -49,7 +50,10 @@ export async function waitForDiscordGatewayStop(params: {
}; };
const onGatewayErrorEvent = (err: unknown) => { const onGatewayErrorEvent = (err: unknown) => {
onGatewayError?.(err); onGatewayError?.(err);
const shouldStop = shouldStopOnError?.(err) ?? true;
if (shouldStop) {
finishReject(err); finishReject(err);
}
}; };
if (abortSignal?.aborted) { if (abortSignal?.aborted) {

View File

@@ -324,6 +324,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
publicKey: "a", publicKey: "a",
token, token,
autoDeploy: nativeEnabled, autoDeploy: nativeEnabled,
eventQueue: {
listenerTimeout: 120_000,
},
}, },
{ {
commands, commands,
@@ -331,6 +334,9 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
}, },
[ [
new GatewayPlugin({ new GatewayPlugin({
reconnect: {
maxAttempts: Number.POSITIVE_INFINITY,
},
intents: intents:
GatewayIntents.Guilds | GatewayIntents.Guilds |
GatewayIntents.GuildMessages | GatewayIntents.GuildMessages |
@@ -409,6 +415,30 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
const gateway = client.getPlugin<GatewayPlugin>("gateway"); const gateway = client.getPlugin<GatewayPlugin>("gateway");
const gatewayEmitter = getDiscordGatewayEmitter(gateway); const gatewayEmitter = getDiscordGatewayEmitter(gateway);
let queueMetricsTimer: ReturnType<typeof setInterval> | undefined;
let lastQueueTimeouts = 0;
let lastQueueDropped = 0;
const onGatewayWarning = (warning: unknown) => {
logVerbose(`discord gateway warning: ${String(warning)}`);
};
if (shouldLogVerbose()) {
gatewayEmitter?.on("warning", onGatewayWarning);
queueMetricsTimer = setInterval(() => {
const metrics = client.eventHandler?.getMetrics?.();
if (!metrics) return;
const nearCapacity =
metrics.maxQueueSize > 0 &&
metrics.queueSize / metrics.maxQueueSize >= 0.8;
const hasNewTimeouts = metrics.timeouts > lastQueueTimeouts;
const hasNewDrops = metrics.dropped > lastQueueDropped;
if (nearCapacity || hasNewTimeouts || hasNewDrops) {
logVerbose(`discord event queue metrics: ${JSON.stringify(metrics)}`);
}
lastQueueTimeouts = metrics.timeouts;
lastQueueDropped = metrics.dropped;
}, 60000);
}
try {
await waitForDiscordGatewayStop({ await waitForDiscordGatewayStop({
gateway: gateway gateway: gateway
? { ? {
@@ -420,7 +450,20 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
onGatewayError: (err) => { onGatewayError: (err) => {
runtime.error?.(danger(`discord gateway error: ${String(err)}`)); runtime.error?.(danger(`discord gateway error: ${String(err)}`));
}, },
shouldStopOnError: (err) => {
const message = String(err);
return (
message.includes("Max reconnect attempts") ||
message.includes("Fatal Gateway error")
);
},
}); });
} finally {
if (queueMetricsTimer) {
clearInterval(queueMetricsTimer);
}
gatewayEmitter?.removeListener("warning", onGatewayWarning);
}
} }
async function clearDiscordNativeCommands(params: { async function clearDiscordNativeCommands(params: {