Skip to content

Catch and retry rate limited responses in read-only GraphQL queries#1531

Merged
offbyone merged 6 commits intomainfrom
handle-rate-limiting-in-graphql-queries
Apr 1, 2026
Merged

Catch and retry rate limited responses in read-only GraphQL queries#1531
offbyone merged 6 commits intomainfrom
handle-rate-limiting-in-graphql-queries

Conversation

@synthead
Copy link
Copy Markdown
Collaborator

@synthead synthead commented Mar 21, 2026

Closes https://github.ghe.com/github/octoshift/issues/11503!

This PR makes GraphQL queries include both general retries and rate limiting! This makes read-only GraphQL queries have the same robustness as other GET calls in the GEI CLI.

Demo

Here is a migration in progress, handling rate limiting:

[2026-03-20 17:09:48] [INFO] Creating Migration Source...
[2026-03-20 17:09:49] [INFO] Importing Archive...
[2026-03-20 17:09:49] [INFO] Migration in progress (ID: RM_kgDaACQ0M2E5ZGQzZS1kYTMwLTQ3MWItODU3Yi1mNTg5NmMyZmUyOGU). State: PENDING_VALIDATION. Waiting 60 seconds...
[2026-03-20 17:10:50] [WARNING] Secondary rate limit detected (attempt 1/3). Waiting 60 seconds before retrying...
[2026-03-20 17:11:50] [INFO] Migration in progress (ID: RM_kgDaACQ0M2E5ZGQzZS1kYTMwLTQ3MWItODU3Yi1mNTg5NmMyZmUyOGU). State: PENDING_VALIDATION. Waiting 60 seconds...
[2026-03-20 17:12:50] [INFO] Migration in progress (ID: RM_kgDaACQ0M2E5ZGQzZS1kYTMwLTQ3MWItODU3Yi1mNTg5NmMyZmUyOGU). State: PENDING_VALIDATION. Waiting 60 seconds...
[2026-03-20 17:13:50] [WARNING] Secondary rate limit detected (attempt 1/3). Waiting 60 seconds before retrying...
[2026-03-20 17:14:51] [INFO] Migration in progress (ID: RM_kgDaACQ0M2E5ZGQzZS1kYTMwLTQ3MWItODU3Yi1mNTg5NmMyZmUyOGU). State: PENDING_VALIDATION. Waiting 60 seconds...
[2026-03-20 17:15:51] [INFO] Migration in progress (ID: RM_kgDaACQ0M2E5ZGQzZS1kYTMwLTQ3MWItODU3Yi1mNTg5NmMyZmUyOGU). State: PENDING_VALIDATION. Waiting 60 seconds...
[2026-03-20 17:16:51] [WARNING] Secondary rate limit detected (attempt 1/3). Waiting 60 seconds before retrying...
[2026-03-20 17:17:51] [INFO] Migration in progress (ID: RM_kgDaACQ0M2E5ZGQzZS1kYTMwLTQ3MWItODU3Yi1mNTg5NmMyZmUyOGU). State: PENDING_VALIDATION. Waiting 60 seconds...
[2026-03-20 17:18:51] [INFO] Migration in progress (ID: RM_kgDaACQ0M2E5ZGQzZS1kYTMwLTQ3MWItODU3Yi1mNTg5NmMyZmUyOGU). State: PENDING_VALIDATION. Waiting 60 seconds...
  • Did you write/update appropriate tests
  • Release notes updated (if appropriate)
  • Appropriate logging output
  • Issue linked
  • Docs updated (or issue created)
  • New package licenses are added to ThirdPartyNotices.txt (if applicable)

@synthead synthead self-assigned this Mar 21, 2026
@synthead synthead changed the title Handle rate limiting in graphql queries Catch and retry rate limited responses in read-only GraphQL queries Mar 21, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 21, 2026

Unit Test Results

    1 files      1 suites   24s ⏱️
1 030 tests 1 030 ✅ 0 💤 0 ❌
1 031 runs  1 031 ✅ 0 💤 0 ❌

Results for commit 77daf4b.

♻️ This comment has been updated with latest results.

@gohana gohana assigned gohana and offbyone and unassigned gohana Mar 23, 2026
@offbyone offbyone force-pushed the handle-rate-limiting-in-graphql-queries branch from b2ffc2c to e56e2ec Compare March 26, 2026 21:07
Comment thread src/Octoshift/Services/GithubClient.cs Dismissed
@offbyone offbyone marked this pull request as ready for review March 26, 2026 21:19
Copilot AI review requested due to automatic review settings March 26, 2026 21:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves robustness of GEI’s read-only GraphQL polling by adding secondary rate-limit detection/retry handling to GraphQL POST calls, aligning it more closely with existing REST retry behavior.

Changes:

  • Added PostGraphQLWithRetryAsync to GithubClient to detect GraphQL secondary rate-limit responses and trigger the existing secondary-rate-limit retry path.
  • Updated GithubApi.GetMigration to use the new GraphQL-with-retry client method.
  • Updated unit tests/mocks to reflect the new client method and to reduce test runtime.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/Octoshift/Services/GithubClient.cs Adds GraphQL secondary rate-limit detection and a new GraphQL POST helper that integrates with retry logic.
src/Octoshift/Services/GithubApi.cs Switches migration state polling to use the new GraphQL-with-retry path.
src/OctoshiftCLI.Tests/Octoshift/Services/GithubApiTests.cs Updates mocks to expect PostGraphQLWithRetryAsync instead of PostGraphQLAsync.
src/OctoshiftCLI.Tests/Octoshift/Services/GithubClientTests.cs Adjusts tests to shorten delays by overriding the default backoff delay.
Comments suppressed due to low confidence (2)

src/Octoshift/Services/GithubClient.cs:377

  • HandleSecondaryRateLimit now uses _secondaryRateLimitMaxRetries for the stop condition, but the exception message and log message still print SECONDARY_RATE_LIMIT_MAX_RETRIES. If the max is overridden (tests or future config), the thrown/logged “(attempt x/3)” and “Maximum retries (3)” will be incorrect. Use the same variable for the messages as the check uses.
        if (retryCount >= _secondaryRateLimitMaxRetries)
        {
            throw new OctoshiftCliException($"Secondary rate limit exceeded. Maximum retries ({SECONDARY_RATE_LIMIT_MAX_RETRIES}) reached. Please wait before retrying your request.");
        }

        var delaySeconds = GetSecondaryRateLimitDelay(headers, retryCount);

        _log.LogWarning($"Secondary rate limit detected (attempt {retryCount + 1}/{SECONDARY_RATE_LIMIT_MAX_RETRIES}). Waiting {delaySeconds} seconds before retrying...");

src/OctoshiftCLI.Tests/Octoshift/Services/GithubClientTests.cs:2210

  • Same issue here: setting _secondaryRateLimitDefaultDelay to 0 makes the test assertions stop exercising the exponential backoff calculation across retries. A no-op delay hook (or scaled-down delays) would allow validating the backoff values without slowing the test suite.
        var githubClient = new GithubClient(_mockOctoLogger.Object, httpClient, null, _retryPolicy, _dateTimeProvider.Object, PERSONAL_ACCESS_TOKEN)
        {
            // Set short default delay to speed up the test. This is reflected in the logs below.
            _secondaryRateLimitDefaultDelay = 0
        };

        // Act & Assert
        await FluentActions
            .Invoking(async () => await githubClient.DeleteAsync("http://example.com"))
            .Should()
            .ThrowExactlyAsync<OctoshiftCliException>()
            .WithMessage("Secondary rate limit exceeded. Maximum retries (3) reached. Please wait before retrying your request.");

        // Verify all retry attempts were logged
        _mockOctoLogger.Verify(m => m.LogWarning("Secondary rate limit detected (attempt 1/3). Waiting 0 seconds before retrying..."), Times.Once);
        _mockOctoLogger.Verify(m => m.LogWarning("Secondary rate limit detected (attempt 2/3). Waiting 0 seconds before retrying..."), Times.Once);
        _mockOctoLogger.Verify(m => m.LogWarning("Secondary rate limit detected (attempt 3/3). Waiting 0 seconds before retrying..."), Times.Once);

Comment thread src/Octoshift/Services/GithubClient.cs Outdated
Comment on lines +105 to +115
var (response, headers) = await PostWithRetry(url, body, customHeaders);

if (IsGraphQLSecondaryRateLimit(response))
{
(response, _) = await HandleSecondaryRateLimit(HttpMethod.Post, url, body, HttpStatusCode.OK, customHeaders, headers, retryCount);
}

var data = JObject.Parse(response);
EnsureSuccessGraphQLResponse(data);

return data;
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostGraphQLWithRetryAsync only checks for GraphQL secondary rate limiting once. HandleSecondaryRateLimit retries via SendAsync, but SendAsync won’t detect GraphQL rate-limit responses (HTTP 200 with errors[].type == RATE_LIMIT), so a second rate-limited response will fall through to EnsureSuccessGraphQLResponse and throw instead of continuing to retry up to the max. Consider looping/re-invoking the GraphQL rate-limit check on every response (and using PostWithRetry for the subsequent attempts so transient exceptions still get retried).

Suggested change
var (response, headers) = await PostWithRetry(url, body, customHeaders);
if (IsGraphQLSecondaryRateLimit(response))
{
(response, _) = await HandleSecondaryRateLimit(HttpMethod.Post, url, body, HttpStatusCode.OK, customHeaders, headers, retryCount);
}
var data = JObject.Parse(response);
EnsureSuccessGraphQLResponse(data);
return data;
var currentRetryCount = retryCount;
while (true)
{
var (response, headers) = await PostWithRetry(url, body, customHeaders);
if (IsGraphQLSecondaryRateLimit(response))
{
(response, headers) = await HandleSecondaryRateLimit(HttpMethod.Post, url, body, HttpStatusCode.OK, customHeaders, headers, currentRetryCount);
if (IsGraphQLSecondaryRateLimit(response))
{
currentRetryCount++;
continue;
}
}
var data = JObject.Parse(response);
EnsureSuccessGraphQLResponse(data);
return data;
}

Copilot uses AI. Check for mistakes.
Comment thread src/Octoshift/Services/GithubApi.cs
Comment thread src/OctoshiftCLI.Tests/Octoshift/Services/GithubClientTests.cs
@offbyone offbyone force-pushed the handle-rate-limiting-in-graphql-queries branch 3 times, most recently from 86818ca to 9c632c6 Compare March 27, 2026 16:26
@offbyone offbyone enabled auto-merge March 30, 2026 16:33
@offbyone offbyone force-pushed the handle-rate-limiting-in-graphql-queries branch 3 times, most recently from db8ecf7 to be707f3 Compare March 31, 2026 15:34
@offbyone offbyone force-pushed the handle-rate-limiting-in-graphql-queries branch from be707f3 to 77daf4b Compare March 31, 2026 15:51
@offbyone offbyone merged commit 27cf3c3 into main Apr 1, 2026
32 checks passed
@offbyone offbyone deleted the handle-rate-limiting-in-graphql-queries branch April 1, 2026 17:19
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 1, 2026

Code Coverage

Package Line Rate Branch Rate Complexity Health
ado2gh 71% 69% 741
gei 81% 73% 608
bbs2gh 83% 78% 667
Octoshift 83% 73% 1834
Summary 81% (7955 / 9874) 73% (1959 / 2679) 3850

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants