(
fetchFn: () => Promise<T>,
identifier: string,
logger: Logger,
maxAttempts: number = 3
)
| 66 | } |
| 67 | |
| 68 | export const fetchWithRetry = async <T>( |
| 69 | fetchFn: () => Promise<T>, |
| 70 | identifier: string, |
| 71 | logger: Logger, |
| 72 | maxAttempts: number = 3 |
| 73 | ): Promise<T> => { |
| 74 | let attempts = 0; |
| 75 | |
| 76 | while (true) { |
| 77 | try { |
| 78 | return await fetchFn(); |
| 79 | } catch (e: any) { |
| 80 | Sentry.captureException(e); |
| 81 | |
| 82 | attempts++; |
| 83 | if ( |
| 84 | ( |
| 85 | (e.status >= 500 && e.status < 600) || |
| 86 | e.status === StatusCodes.FORBIDDEN || |
| 87 | e.status === StatusCodes.TOO_MANY_REQUESTS |
| 88 | ) && attempts < maxAttempts |
| 89 | ) { |
| 90 | const resetDateMs = (() => { |
| 91 | // First, try to see if we have a reset date specified in the response headers |
| 92 | if (isOctokitRequestError(e) && e.response?.headers['x-ratelimit-reset']) { |
| 93 | return parseInt(e.response.headers['x-ratelimit-reset']) * 1000; |
| 94 | } |
| 95 | |
| 96 | // Default to a exponential backoff approach |
| 97 | const defaultWaitTime = 3000 * Math.pow(2, attempts - 1); |
| 98 | return Date.now() + defaultWaitTime; |
| 99 | })(); |
| 100 | |
| 101 | const waitTime = Math.max(0, resetDateMs - Date.now()); |
| 102 | logger.warn(`Rate limit exceeded for ${identifier}. Waiting ${waitTime}ms before retry ${attempts}/${maxAttempts}...`); |
| 103 | |
| 104 | await new Promise(resolve => setTimeout(resolve, waitTime)); |
| 105 | continue; |
| 106 | } |
| 107 | throw e; |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | // TODO: do this better? ex: try using the tokens from all the connections |
| 113 | // We can no longer use repo.cloneUrl directly since it doesn't contain the token for security reasons. As a result, we need to |
no test coverage detected