perf(gateway): retry transient attachment + wikipedia failures
- attachmentUploader: downloadDiscordAttachment retries CDN timeouts via retryWithBackoff (ATTACHMENT_RETRY_ATTEMPTS, 1s-8s backoff); AbortError normalized so 403/404 refresh path never fires on timeouts - attachmentUploader: uploadAttachmentToTele uses ATTACHMENT_RETRY_ATTEMPTS instead of retries:0 (Tele 5xx under load was failing outright) - wikipediaClient: wikipediaSummary retries once on abort/timeout (100% of prod summary errors were aborts); termGlossary drops its redundant second call (was up to 4 reqs/term under miss+retry) Verified: typecheck + lint clean, 210/210 tests pass
This commit is contained in:
1 parent
90d4c2d0b1
commit
9685fa02be
3 files changed
+79
-50
No files matched your search
@@ -1,4 +1,5 @@
|
||||
import { createChildLogger } from "@/shared/logger/index";
|
||||
import { retryWithBackoff } from "@/shared/utils/index";
|
||||
import { config } from "../../shared/config/config.js";
|
||||
import { uploadToTele } from "../../shared/uploader.js";
|
||||
import { messageStore } from "../message-capture/messageStore.js";
|
||||
@@ -44,7 +45,9 @@ export async function uploadAttachmentToTele(
|
||||
contentType,
|
||||
uploadUrl: config.TELE_UPLOAD_URL,
|
||||
timeoutMs: config.ATTACHMENT_UPLOAD_TIMEOUT_MS,
|
||||
retries: 0,
|
||||
// Tele 5xx under load is transient (prod: 12x Status 500) — retry
|
||||
// inside uploadToTele instead of failing the attachment outright.
|
||||
retries: config.ATTACHMENT_RETRY_ATTEMPTS,
|
||||
});
|
||||
|
||||
logger.info(
|
||||
@@ -64,12 +67,28 @@ export async function uploadAttachmentToTele(
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadDiscordAttachment(url: string): Promise<Buffer> {
|
||||
export async function downloadDiscordAttachment(
|
||||
url: string,
|
||||
timeoutMs?: number,
|
||||
): Promise<Buffer> {
|
||||
const timeout = timeoutMs ?? config.ATTACHMENT_UPLOAD_TIMEOUT_MS;
|
||||
logger.debug({ url }, "Starting Discord attachment download");
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(config.ATTACHMENT_UPLOAD_TIMEOUT_MS),
|
||||
});
|
||||
// Timeout-only retry: a CDN abort mid-download is transient (the prod
|
||||
// failure signature is "The operation was aborted due to timeout").
|
||||
// 403/404 = expired or purged URL — not retried here; the caller
|
||||
// refreshes via refreshDiscordUrl instead.
|
||||
const response = await retryWithBackoff(
|
||||
() =>
|
||||
fetch(url, {
|
||||
signal: AbortSignal.timeout(timeout),
|
||||
}),
|
||||
{
|
||||
retries: config.ATTACHMENT_RETRY_ATTEMPTS,
|
||||
minTimeout: 1000,
|
||||
maxTimeout: 8000,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AttachmentDownloadError(
|
||||
@@ -86,6 +105,13 @@ export async function downloadDiscordAttachment(url: string): Promise<Buffer> {
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof AttachmentDownloadError) throw error;
|
||||
// retryWithBackoff rethrows AbortError unwrapped — normalize it to a
|
||||
// plain Error so shouldRefreshDiscordUrl() never mistakes a timeout
|
||||
// for an expired URL (which would trigger a useless refresh fetch).
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
throw new Error(`Download timed out: ${toErrorMessage(error)}`);
|
||||
}
|
||||
logger.error(
|
||||
{ url, error: toErrorMessage(error) },
|
||||
"Failed to download Discord attachment",
|
||||
|
||||
Reference in new issue
Block a user