Skip to content

Commit 3d5c516

Browse files
committed
feat: enhance rate limit handling with Octokit throttling and remove manual retry logic
1 parent 72c220b commit 3d5c516

File tree

1 file changed

+38
-53
lines changed

1 file changed

+38
-53
lines changed

scripts/copy-discussions.js

Lines changed: 38 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@
3636
const INCLUDE_POLL_MERMAID_CHART = true; // Set to false to disable Mermaid pie chart for polls
3737
const RATE_LIMIT_SLEEP_SECONDS = 0.5; // Default sleep duration between API calls to avoid rate limiting
3838
const DISCUSSION_PROCESSING_DELAY_SECONDS = 5; // Delay between processing discussions
39-
const RATE_LIMIT_RETRY_DELAY_SECONDS = 60; // Delay when hitting rate limits before retrying
40-
const MAX_RETRIES = 3; // Maximum number of retries for failed operations
39+
const MAX_RETRIES = 3; // Maximum number of retries for failed operations (rate limits handled automatically by Octokit)
4140

4241
const { Octokit } = require("octokit");
4342

@@ -134,15 +133,39 @@ if (!process.env.TARGET_TOKEN) {
134133
const SOURCE_API_URL = process.env.SOURCE_API_URL || 'https://api.github.com';
135134
const TARGET_API_URL = process.env.TARGET_API_URL || 'https://api.github.com';
136135

137-
// Initialize Octokit instances
136+
// Configure throttling for rate limit handling
137+
const throttleOptions = {
138+
onRateLimit: (retryAfter, options, octokit) => {
139+
warn(`Primary rate limit exhausted for request ${options.method} ${options.url}`);
140+
if (options.request.retryCount <= 2) {
141+
warn(`Retrying after ${retryAfter} seconds (retry ${options.request.retryCount + 1}/3)`);
142+
return true;
143+
}
144+
error(`Max retries reached for rate limit`);
145+
return false;
146+
},
147+
onSecondaryRateLimit: (retryAfter, options, octokit) => {
148+
warn(`Secondary rate limit detected for request ${options.method} ${options.url}`);
149+
if (options.request.retryCount <= 2) {
150+
warn(`Retrying after ${retryAfter} seconds (retry ${options.request.retryCount + 1}/3)`);
151+
return true;
152+
}
153+
error(`Max retries reached for secondary rate limit`);
154+
return false;
155+
}
156+
};
157+
158+
// Initialize Octokit instances with throttling enabled
138159
const sourceOctokit = new Octokit({
139160
auth: process.env.SOURCE_TOKEN,
140-
baseUrl: SOURCE_API_URL
161+
baseUrl: SOURCE_API_URL,
162+
throttle: throttleOptions
141163
});
142164

143165
const targetOctokit = new Octokit({
144166
auth: process.env.TARGET_TOKEN,
145-
baseUrl: TARGET_API_URL
167+
baseUrl: TARGET_API_URL,
168+
throttle: throttleOptions
146169
});
147170

148171
// Tracking variables
@@ -178,39 +201,6 @@ async function rateLimitSleep(seconds = RATE_LIMIT_SLEEP_SECONDS) {
178201
await sleep(seconds);
179202
}
180203

181-
function isRateLimitError(err) {
182-
const message = err.message?.toLowerCase() || '';
183-
const status = err.status || 0;
184-
185-
// Check for primary rate limit (403 with rate limit message)
186-
if (status === 403 && (message.includes('rate limit') || message.includes('api rate limit'))) {
187-
return true;
188-
}
189-
190-
// Check for secondary rate limit (403 with abuse/secondary message)
191-
if (status === 403 && (message.includes('secondary') || message.includes('abuse'))) {
192-
return true;
193-
}
194-
195-
// Check for retry-after header indication
196-
if (message.includes('retry after') || message.includes('try again later')) {
197-
return true;
198-
}
199-
200-
return false;
201-
}
202-
203-
async function handleRateLimitError(err, attemptNumber) {
204-
if (isRateLimitError(err)) {
205-
const waitTime = RATE_LIMIT_RETRY_DELAY_SECONDS * attemptNumber; // Exponential-ish backoff
206-
warn(`Rate limit detected (attempt ${attemptNumber}). Waiting ${waitTime}s before retry...`);
207-
warn(`Error details: ${err.message}`);
208-
await sleep(waitTime);
209-
return true;
210-
}
211-
return false;
212-
}
213-
214204
function formatPollData(poll) {
215205
if (!poll || !poll.options || poll.options.nodes.length === 0) {
216206
return '';
@@ -1039,7 +1029,7 @@ async function processDiscussionsPage(sourceOctokit, targetOctokit, owner, repo,
10391029
// Check if discussion is pinned
10401030
const isPinned = pinnedDiscussionIds.has(discussion.id);
10411031

1042-
// Create discussion with retry logic
1032+
// Create discussion (Octokit throttling plugin handles rate limits automatically)
10431033
let newDiscussion = null;
10441034
let createSuccess = false;
10451035

@@ -1065,22 +1055,17 @@ async function processDiscussionsPage(sourceOctokit, targetOctokit, owner, repo,
10651055
log(`✓ Created discussion #${discussion.number}: '${discussion.title}'`);
10661056

10671057
} catch (err) {
1068-
// Handle rate limit errors with retry
1069-
const shouldRetry = await handleRateLimitError(err, attempt);
1058+
// Octokit throttling handles rate limits; this catches other errors
1059+
error(`Failed to create discussion #${discussion.number}: '${discussion.title}' - ${err.message}`);
10701060

1071-
if (!shouldRetry) {
1072-
// Not a rate limit error, or unrecoverable error
1073-
error(`Failed to create discussion #${discussion.number}: '${discussion.title}' - ${err.message}`);
1074-
if (attempt < MAX_RETRIES) {
1075-
warn(`Retrying (attempt ${attempt + 1}/${MAX_RETRIES})...`);
1076-
await sleep(5); // Brief pause before retry
1077-
} else {
1078-
error(`Max retries (${MAX_RETRIES}) reached. Skipping discussion #${discussion.number}.`);
1079-
skippedDiscussions++;
1080-
break;
1081-
}
1061+
if (attempt < MAX_RETRIES) {
1062+
warn(`Retrying (attempt ${attempt + 1}/${MAX_RETRIES}) in 5 seconds...`);
1063+
await sleep(5);
1064+
} else {
1065+
error(`Max retries (${MAX_RETRIES}) reached. Skipping discussion #${discussion.number}.`);
1066+
skippedDiscussions++;
1067+
break;
10821068
}
1083-
// If shouldRetry is true, loop will continue to next attempt
10841069
}
10851070
}
10861071

0 commit comments

Comments
 (0)