Skip to content

Commit 6ea6480

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

File tree

1 file changed

+40
-53
lines changed

1 file changed

+40
-53
lines changed

scripts/copy-discussions.js

Lines changed: 40 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,41 @@ 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+
// Octokit's throttling plugin automatically handles both REST and GraphQL rate limits
138+
// by intercepting HTTP 403 responses and retry-after headers
139+
const throttleOptions = {
140+
onRateLimit: (retryAfter, options, octokit) => {
141+
warn(`Primary rate limit exhausted for request ${options.method} ${options.url}`);
142+
if (options.request.retryCount <= 2) {
143+
warn(`Retrying after ${retryAfter} seconds (retry ${options.request.retryCount + 1}/3)`);
144+
return true;
145+
}
146+
error(`Max retries reached for rate limit`);
147+
return false;
148+
},
149+
onSecondaryRateLimit: (retryAfter, options, octokit) => {
150+
warn(`Secondary rate limit detected for request ${options.method} ${options.url}`);
151+
if (options.request.retryCount <= 2) {
152+
warn(`Retrying after ${retryAfter} seconds (retry ${options.request.retryCount + 1}/3)`);
153+
return true;
154+
}
155+
error(`Max retries reached for secondary rate limit`);
156+
return false;
157+
}
158+
};
159+
160+
// Initialize Octokit instances with throttling enabled
138161
const sourceOctokit = new Octokit({
139162
auth: process.env.SOURCE_TOKEN,
140-
baseUrl: SOURCE_API_URL
163+
baseUrl: SOURCE_API_URL,
164+
throttle: throttleOptions
141165
});
142166

143167
const targetOctokit = new Octokit({
144168
auth: process.env.TARGET_TOKEN,
145-
baseUrl: TARGET_API_URL
169+
baseUrl: TARGET_API_URL,
170+
throttle: throttleOptions
146171
});
147172

148173
// Tracking variables
@@ -178,39 +203,6 @@ async function rateLimitSleep(seconds = RATE_LIMIT_SLEEP_SECONDS) {
178203
await sleep(seconds);
179204
}
180205

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-
214206
function formatPollData(poll) {
215207
if (!poll || !poll.options || poll.options.nodes.length === 0) {
216208
return '';
@@ -1039,7 +1031,7 @@ async function processDiscussionsPage(sourceOctokit, targetOctokit, owner, repo,
10391031
// Check if discussion is pinned
10401032
const isPinned = pinnedDiscussionIds.has(discussion.id);
10411033

1042-
// Create discussion with retry logic
1034+
// Create discussion (Octokit throttling plugin handles rate limits automatically)
10431035
let newDiscussion = null;
10441036
let createSuccess = false;
10451037

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

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

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-
}
1063+
if (attempt < MAX_RETRIES) {
1064+
warn(`Retrying (attempt ${attempt + 1}/${MAX_RETRIES}) in 5 seconds...`);
1065+
await sleep(5);
1066+
} else {
1067+
error(`Max retries (${MAX_RETRIES}) reached. Skipping discussion #${discussion.number}.`);
1068+
skippedDiscussions++;
1069+
break;
10821070
}
1083-
// If shouldRetry is true, loop will continue to next attempt
10841071
}
10851072
}
10861073

0 commit comments

Comments
 (0)