From 8e2c6fea1b2f9f49eb2615f8dc75f1b029a429d5 Mon Sep 17 00:00:00 2001 From: 134 Date: Sat, 4 Jul 2026 20:20:21 +0800 Subject: [PATCH] fix: timeout stalled s3 uploads --- lib/storage/service.ts | 43 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/storage/service.ts b/lib/storage/service.ts index e8e9ce895..6e3b130c9 100644 --- a/lib/storage/service.ts +++ b/lib/storage/service.ts @@ -45,6 +45,38 @@ export type StorageUploadFunction = ( let lastKnownGoodDbS3Config: S3UploadConfig | null = null; const STORAGE_FAILOVER_CONFIG_KEY = "storage_failover_config"; +const DEFAULT_S3_REQUEST_TIMEOUT_MS = 30_000; + +function getStorageTimeoutMs(name: string, fallback: number, min: number, max: number) { + const parsed = Number(process.env[name]); + if (!Number.isFinite(parsed)) return fallback; + return Math.max(min, Math.min(max, Math.round(parsed))); +} + +async function withStorageOperationTimeout( + operation: Promise, + timeoutMs: number, + label: string +): Promise { + let timedOut = false; + let timeoutId: ReturnType | undefined; + const guardedOperation = operation.catch((error) => { + if (timedOut) { + logger.warn(`[Upload] ${label} failed after timeout:`, error); + return new Promise(() => undefined); + } + throw error; + }); + const timeout = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + timedOut = true; + reject(new Error(`${label} timed out after ${timeoutMs}ms`)); + }, timeoutMs); + }); + return Promise.race([guardedOperation, timeout]).finally(() => { + if (timeoutId) clearTimeout(timeoutId); + }); +} function normalizeEndpoint(endpoint: string): string { const trimmed = endpoint.trim().replace(/\/+$/, ""); @@ -224,7 +256,16 @@ async function uploadToS3( ACL: "public-read", }); - await s3Client.send(command); + await withStorageOperationTimeout( + s3Client.send(command), + getStorageTimeoutMs( + "S3_UPLOAD_REQUEST_TIMEOUT_MS", + DEFAULT_S3_REQUEST_TIMEOUT_MS, + 5_000, + 120_000 + ), + "S3 upload" + ); logger.debug(`[S3 Upload] Upload successful: ${key}`);