Files
release-cleanup/app/delete-utils.js
T
JefferyandClaude Opus 4.8 7d34ceb439 feat(delete-utils): 新增刪除重試、併發與 best-effort 共用工具
新增 deleteWithRetry(暫時性錯誤指數退避重試、永久性錯誤即止)、
runWithConcurrency(有上限併發)與 isRetryableStatus,供清理流程強化錯誤處理。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 15:28:40 +08:00

80 lines
3.2 KiB
JavaScript

// 刪除作業共用工具:暫時性錯誤的指數退避重試、有上限併發,以及 best-effort 續行所需的結果型別。
// 對應 AI review 對 release/tag 刪除「重試、錯誤分流、併發、部分失敗續行」的多項建議。
// 視為可重試的暫時性 HTTP 狀態碼(限流與閘道層暫時性錯誤)。
// 刻意不含 500:500 多為確定性錯誤,重試通常無益。
const RETRYABLE_STATUS = new Set([429, 502, 503, 504])
// 單筆刪除的最大嘗試次數與退避基準。
const MAX_ATTEMPTS = 3
const BASE_BACKOFF_MS = 200
// 預設併發上限:加速大量刪除,同時避免對 Gitea API 造成過大壓力或觸發速率限制。
const DEFAULT_CONCURRENCY = 4
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
/**
* 判斷 HTTP 狀態碼是否為可重試的暫時性錯誤(429/502/503/504)。
* @param {number} status HTTP 狀態碼
* @returns {boolean}
*/
export function isRetryableStatus(status) {
return RETRYABLE_STATUS.has(status)
}
/**
* 帶指數退避的刪除:204 視為成功;暫時性錯誤(網路例外或 429/502/503/504)會重試;
* 永久性錯誤(如 401/403/404)立即停止重試。本函式不對外拋例外,失敗以回傳值表示。
* @param {{ deleteResource: (url: string) => Promise<number> }} client 具 deleteResource 的客戶端
* @param {string} url 目標資源位址
* @param {{ attempts?: number, sleep?: (ms: number) => Promise<void> }} [options] 可注入嘗試次數與 sleep(測試用)
* @returns {Promise<{ status: number, error?: Error, attempts: number }>} 最終狀態(204 成功);網路例外時 status 為 0 並帶 error
*/
export async function deleteWithRetry(client, url, options = {}) {
const { attempts = MAX_ATTEMPTS, sleep = wait } = options
let last = { status: 0, attempts: 0 }
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
const status = await client.deleteResource(url)
if (status === 204 || !isRetryableStatus(status)) {
return { status, attempts: attempt } // 成功或永久性錯誤,不再重試
}
last = { status, attempts: attempt }
} catch (error) {
last = { status: 0, error, attempts: attempt }
}
if (attempt < attempts) {
await sleep(BASE_BACKOFF_MS * 2 ** (attempt - 1)) // 200ms、400ms…
}
}
return last
}
/**
* 以有上限的併發逐項執行非同步 worker,回傳對應輸入順序的結果陣列。
* 控制併發數可加速處理,同時避免一次送出過多請求。worker 不應拋例外(由呼叫端自行吞例外)。
* @template T, R
* @param {T[]} items 待處理項目
* @param {(item: T, index: number) => Promise<R>} worker 處理單一項目
* @param {number} [concurrency] 併發上限,預設 4
* @returns {Promise<R[]>}
*/
export async function runWithConcurrency(items, worker, concurrency = DEFAULT_CONCURRENCY) {
const results = new Array(items.length)
let next = 0
async function runner() {
while (next < items.length) {
const index = next
next += 1
results[index] = await worker(items[index], index)
}
}
const size = Math.max(1, Math.min(concurrency, items.length))
await Promise.all(Array.from({ length: size }, () => runner()))
return results
}