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:
asepharyana committed 2026-09-14 00:13:56 +07:00
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",