Skip to content

gh-prs-merge: back off secondary rate limits instead of retrying every 65 s - #262

Merged
ralyodio merged 1 commit into
masterfrom
fix/prs-merge-secondary-backoff
Nov 11, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/prs-merge-secondary-backoff

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Problem

A sweep logged this every ~66 s and never resumed, even though gh api rate_limit showed core 4747/5000 and graphql 3758/5000:

WAIT: GitHub API rate limit exhausted; waiting until it resets at 8:40:55 AM (in 2m)
WAIT: GitHub API rate limit exhausted; waiting until it resets at 8:42:01 AM (in 2m)
WAIT: GitHub API rate limit exhausted; waiting until it resets at 8:43:07 AM (in 2m)

The refusal was a secondary (abuse) limit. With no bucket at remaining: 0, parseRateLimitReset fell back to "now + 60 s", so every retry hit GitHub again 65 s later. Each refused call keeps a secondary penalty alive.

Fix

  • parseRateLimitReset returns undefined when no bucket is empty.
  • A secondary limit backs off 1, 2, 4 then 8 m. The streak is shared across calls and resets on the first success. Once the next step would pass --rate-wait, it throws RateLimitError, the same as an out-of-budget primary reset, so the scope is marked FAILED instead of hanging.
  • Notices say "secondary rate limit … backing off 60s (refusal 1 in a row)". Short waits print in seconds ("65s", not "2m").

Tests

  • test/prs-merge.test.ts: 61/61 pass, including the new back-off, give-up and format cases.
  • tsc --noEmit is clean.
  • test/forwardemail.test.ts fails on its own and is untouched here.

🤖 Generated with Claude Code

…y 65 s

When `gh` was refused but no `gh api rate_limit` bucket was empty (a
secondary/abuse limit), parseRateLimitReset invented "now + 60 s". Every
retry then printed "rate limit exhausted; waiting until it resets ... (in 2m)"
and hit GitHub again 65 s later, which keeps a secondary penalty alive: the
reset crept forward a minute per line and the sweep never resumed.

- parseRateLimitReset returns undefined when no bucket is dry.
- Secondary limits back off 1, 2, 4, 8 m (streak shared across calls, reset
  on the first success) and throw RateLimitError once the next step is past
  --rate-wait, the same as an out-of-budget primary reset.
- Notices say "secondary rate limit" rather than "exhausted", and short
  waits print as seconds ("65s", not "2m").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ralyodio
ralyodio merged commit de2141e into master Nov 11, 2026
4 checks passed
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

41 finding(s)

HIGH/CRITICAL: 15 | MEDIUM: 12 | LOW: 14

Severity Rule Location
HIGH py-ssrf-outbound-request dev2/dev2-site:151
HIGH py-ssrf-outbound-request dev2/dev2-site:1099
HIGH py-ssrf-outbound-request dev2/dev2-site:1903
HIGH sh-remote-script-execution root-ubuntu.sh:3262
HIGH sh-remote-script-execution root-ubuntu.sh:3263
HIGH sh-remote-script-execution root-ubuntu.sh:5168
HIGH sh-remote-script-execution root-ubuntu.sh:5172
HIGH sh-remote-script-execution root-w3bs-os.sh:3639
HIGH sh-remote-script-execution root-w3bs-os.sh:3640
HIGH sh-remote-script-execution root-w3bs-os.sh:5569
HIGH sh-remote-script-execution root-w3bs-os.sh:5579
HIGH sh-remote-script-execution root-w3bs-os.sh:5586
HIGH sh-remote-script-execution root-w3bs-os.sh:5602
HIGH sh-remote-script-execution root-w3bs-os.sh:5607
HIGH sh-plaintext-download root-w3bs-os.sh:6289
MEDIUM sql-template-interpolation dev2/dev2-site:1334
MEDIUM sql-template-interpolation dev2/dev2-site:1411
MEDIUM sh-remote-script-execution root-ubuntu.sh:5376
MEDIUM redos-nested-quantifier src/domain-free.ts:56
MEDIUM redos-nested-quantifier src/emoji.ts:167
MEDIUM redos-nested-quantifier src/icon.ts:166
MEDIUM redos-nested-quantifier src/mail.ts:1060
MEDIUM sql-template-interpolation src/users-dump.ts:487
MEDIUM sql-string-concatenation src/users-dump.ts:507
MEDIUM sql-template-interpolation src/users-dump.ts:540
MEDIUM sql-string-concatenation src/users-dump.ts:574
MEDIUM redos-nested-quantifier src/wcag.ts:556
LOW secret-generic-credential src/credentials.ts:41
LOW secret-generic-credential src/credentials.ts:58
LOW secret-generic-credential src/credentials.ts:61
LOW secret-generic-credential src/credentials.ts:66
LOW secret-generic-credential src/user-export.ts:689
LOW secret-generic-credential src/user-export.ts:695
LOW secret-generic-api-key test/credentials.test.ts:208
LOW secret-generic-credential test/mail.test.ts:141
LOW secret-generic-credential test/proxy.test.ts:69
LOW secret-generic-credential test/proxy.test.ts:85
LOW secret-generic-credential test/shorten.test.ts:36
LOW secret-database-url test/users-dump.test.ts:108
LOW secret-database-url test/users-dump.test.ts:119
LOW secret-database-url test/users-dump.test.ts:120

Snippets are redacted; ThreatCrush never prints matched credential material.

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.

1 participant