From 4d05faa6cbf6f9007a7f753bf92cd8aea01a0f51 Mon Sep 17 00:00:00 2001 From: martin-mfg <2026226+martin-mfg@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:33:50 +0100 Subject: [PATCH] use PATs evenly --- backend/src/common/retryer.js | 120 ++++++++++++++++------------------ 1 file changed, 58 insertions(+), 62 deletions(-) diff --git a/backend/src/common/retryer.js b/backend/src/common/retryer.js index feecb4bf..83366a94 100644 --- a/backend/src/common/retryer.js +++ b/backend/src/common/retryer.js @@ -3,6 +3,10 @@ import { CustomError } from "./error.js"; import { logger } from "./log.js"; +function getRandomInt(max) { + return Math.floor(Math.random() * max); +} + // Script variables. // Count the number of GitHub API tokens available. @@ -21,76 +25,68 @@ const RETRIES = process.env.NODE_ENV === "test" ? 7 : PATs; * * @param {FetcherFunction} fetcher The fetcher function. * @param {any} variables Object with arguments to pass to the fetcher function. - * @param {number} retries How many times to retry. * @returns {Promise} The response from the fetcher function. */ -const retryer = async (fetcher, variables, retries = 0) => { +const retryer = async (fetcher, variables) => { if (!RETRIES) { throw new CustomError("No GitHub API tokens found", CustomError.NO_TOKENS); } + const startPAT = getRandomInt(PATs); - if (retries > RETRIES) { - throw new CustomError( - "Downtime due to GitHub API rate limiting", - CustomError.MAX_RETRY, - ); + for (let retries = 0; retries < RETRIES; retries++) { + const currentPAT = ((startPAT + retries) % PATs) + 1; + try { + let response = await fetcher( + variables, + // @ts-ignore + process.env[`PAT_${currentPAT}`], + // used in tests for faking rate limit + retries, + ); + + // react on both type and message-based rate-limit signals. + // https://github.com/anuraghazra/github-readme-stats/issues/4425 + const errors = response?.data?.errors; + const errorType = errors?.[0]?.type; + const errorMsg = errors?.[0]?.message || ""; + const isRateLimited = + (errors && errorType === "RATE_LIMITED") || + /rate limit/i.test(errorMsg); + + if (isRateLimited) { + logger.log(`PAT_${currentPAT} Failed due to rate limiting`); + } else { + return response; + } + } catch (err) { + /** @type {any} */ + const e = err; + + // network/unexpected error → let caller treat as failure + if (!e?.response) { + throw e; + } + + // prettier-ignore + // also checking for bad credentials if any tokens gets invalidated + const isBadCredential = + e?.response?.data?.message === "Bad credentials"; + const isAccountSuspended = + e?.response?.data?.message === "Sorry. Your account was suspended."; + + if (isBadCredential || isAccountSuspended) { + logger.log(`PAT_${currentPAT} Failed due to bad credentials`); + } else { + // HTTP error with a response → return it for caller-side handling + return e.response; + } + } } - try { - // try to fetch with the first token since RETRIES is 0 index i'm adding +1 - let response = await fetcher( - variables, - // @ts-ignore - process.env[`PAT_${retries + 1}`], - // used in tests for faking rate limit - retries, - ); - - // react on both type and message-based rate-limit signals. - // https://github.com/anuraghazra/github-readme-stats/issues/4425 - const errors = response?.data?.errors; - const errorType = errors?.[0]?.type; - const errorMsg = errors?.[0]?.message || ""; - const isRateLimited = - (errors && errorType === "RATE_LIMITED") || /rate limit/i.test(errorMsg); - - // if rate limit is hit increase the RETRIES and recursively call the retryer - // with username, and current RETRIES - if (isRateLimited) { - logger.log(`PAT_${retries + 1} Failed due to rate limiting`); - retries++; - // directly return from the function - return retryer(fetcher, variables, retries); - } - - // finally return the response - return response; - } catch (err) { - /** @type {any} */ - const e = err; - - // network/unexpected error → let caller treat as failure - if (!e?.response) { - throw e; - } - - // prettier-ignore - // also checking for bad credentials if any tokens gets invalidated - const isBadCredential = - e?.response?.data?.message === "Bad credentials"; - const isAccountSuspended = - e?.response?.data?.message === "Sorry. Your account was suspended."; - - if (isBadCredential || isAccountSuspended) { - logger.log(`PAT_${retries + 1} Failed due to bad credentials`); - retries++; - // directly return from the function - return retryer(fetcher, variables, retries); - } - - // HTTP error with a response → return it for caller-side handling - return e.response; - } + throw new CustomError( + "Downtime due to GitHub API rate limiting", + CustomError.MAX_RETRY, + ); }; export { retryer, RETRIES };