Discord: harden gateway resilience
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
Reference in New Issue
Block a user