Skip to content

Commit 67f8cda

Browse files
committed
feat: implement retry logic for GitHub API requests and add sleep utility
1 parent b61f9e6 commit 67f8cda

3 files changed

Lines changed: 42 additions & 4 deletions

File tree

src/scraper/client.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
1-
import { GraphQLClient } from 'graphql-request'
1+
import { ClientError, GraphQLClient } from 'graphql-request'
22
import 'cross-fetch/polyfill' // https://github.com/prisma-labs/graphql-request/issues/206#issuecomment-693304073
3+
import { sleep } from './utils/sleep'
34

4-
export const client = new GraphQLClient('https://api.github.com/graphql', {
5+
const graphqlClient = new GraphQLClient('https://api.github.com/graphql', {
56
headers: {
67
Authorization: `bearer ${process.env.OSD_GH_TOKEN}`,
78
},
89
method: 'POST',
910
})
11+
12+
// Delays for retrying after a secondary rate limit hit: 1min, 2min, 5min.
13+
const SECONDARY_RATE_LIMIT_RETRY_DELAYS_MS = [60_000, 120_000, 300_000]
14+
15+
// Space requests 1 second apart to stay within GitHub's secondary rate limits.
16+
const REQUEST_INTERVAL_MS = 1_000
17+
18+
export const client = {
19+
async request<T>(query: string, variables?: Record<string, unknown>): Promise<T> {
20+
for (let attempt = 0; attempt <= SECONDARY_RATE_LIMIT_RETRY_DELAYS_MS.length; attempt++) {
21+
try {
22+
const result = await graphqlClient.request<T>(query, variables)
23+
await sleep(REQUEST_INTERVAL_MS)
24+
return result
25+
} catch (error) {
26+
const isSecondaryRateLimit =
27+
error instanceof ClientError &&
28+
error.response.status === 403 &&
29+
JSON.stringify(error.response).toLowerCase().includes('secondary rate limit')
30+
31+
if (isSecondaryRateLimit && attempt < SECONDARY_RATE_LIMIT_RETRY_DELAYS_MS.length) {
32+
const delay = SECONDARY_RATE_LIMIT_RETRY_DELAYS_MS[attempt]
33+
console.log(
34+
`Secondary rate limit hit. Retrying in ${delay / 1000}s (attempt ${attempt + 1}/${SECONDARY_RATE_LIMIT_RETRY_DELAYS_MS.length})...`
35+
)
36+
await sleep(delay)
37+
continue
38+
}
39+
40+
throw error
41+
}
42+
}
43+
44+
throw new Error('Exceeded maximum retries after secondary rate limit.')
45+
},
46+
}

src/scraper/utils/sleep.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms))

src/scripts/scrape.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ const scrape = async (): Promise<void> => {
1515
try {
1616
const scrapeAll = !('only' in argv)
1717
const scrapeOnlyUsers = 'only' in argv && (!argv.only || (argv.only && argv.only === 'users'))
18-
if (scrapeAll && scrapeOnlyUsers) {
18+
if (scrapeAll || scrapeOnlyUsers) {
1919
await scrapeUsers()
2020
console.log('Finished scraping users.')
2121
}
2222

2323
const scrapeOnlyRepos = 'only' in argv && (!argv.only || (argv.only && argv.only === 'repos'))
24-
if (scrapeAll && scrapeOnlyRepos) {
24+
if (scrapeAll || scrapeOnlyRepos) {
2525
await scrapeRepos()
2626
console.log('Finished scraping repos.')
2727
}

0 commit comments

Comments
 (0)