fix(client): keep --all crawls inside the API rate limit - #30
Closed
Scorpion197 wants to merge 1 commit into
Closed
Scorpion197 wants to merge 1 commit into
Scorpion197 wants to merge 1 commit into
Conversation
A --all crawl fires up to 1,000 pages at one route back-to-back. The route bucket holds 10 requests and refills at 1/s, so the eleventh page gets a 429 and, without --retry, the crawl dies and every page already fetched is thrown away. Any export past ~1,000 records failed by design. The pagination guide asks clients to add delays between pages and the rate-limiting guide says every response carries the binding bucket's X-RateLimit-* headers, so the crawl now uses them: - After a page that empties the bucket, wait for one token before asking for the next page (Reset / Limit, or the full reset without a Limit). - A 429 mid-crawl pauses and re-requests the same page instead of failing, with or without --retry: a GET is safe to repeat and earlier pages are already in hand. The wait follows Retry-After, then the bucket reset, then the backoff table; past the 60s ceiling or once the table runs out the 429 is reported as before. - Pauses are announced on stderr so a long export does not look hung. Single-page requests are unchanged. The retry loop is split into _send, which returns the last response, so the crawl can read headers without duplicating it. https://docs.dualentry.com/developers/guides/rate-limiting https://docs.dualentry.com/developers/guides/pagination
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
--allfires up to 1,000 pages at a single route with no delay between them. The rate-limiting guide says each route has a bucket of 10 requests refilling at 1/s, so the eleventh page gets a429. Without the global--retryflag that raises, the command prints nothing, and every page already fetched is thrown away. Any--allexport past roughly 1,000 records fails by design, on a CLI whose README pitches CI/CD exports.The pagination guide asks clients to "add delays between requests to respect rate limits", and every response carries the binding bucket's
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Resetheaders. This PR makes the crawl use them.Changes
X-RateLimit-Remaining: 0, the crawl waits for one token before asking for the next page.Resetis when the bucket is full again andLimitis its size, so one token is back afterReset / Limit. Without a usableLimitit waits for the full reset. The wait is capped at the existing 60s ceiling.429mid-crawl pauses and re-requests the same page instead of failing, with or without--retry. AGETis safe to repeat and the earlier pages are already in hand. The wait followsRetry-After, then the bucket reset, then the existing backoff table. Past the ceiling, or once the table runs out, the429is reported exactly as before.Rate limit reached; pausing 1s before the next page...) so a long export does not look hung. stdout still carries only the data._requestis split into_send, which returns the last response, so the crawl can read headers without duplicating the retry logic. Single-page requests are unchanged.Missing or unparsable headers behave exactly as today. Any other error mid-crawl still raises at once.
Proof
Reproduced against a throwaway local server that implements the documented route bucket (burst 10, refill 1/s,
X-RateLimit-*headers on every response,429+Retry-Afterwhen empty) and serves 1,500 records, so a crawl needs 15 pages. Same command both times:Before (main): dies on page 11, nothing printed, 1,000 already-fetched records lost.
After (this branch): all 1,500 records, no
429ever sent, 5s wall clock.stderr:
server log:
The mock server script is below if you want to re-run it: save it,
python3 bucket_server.py 8765, then run the command above.bucket_server.py
Test plan
uv run pytest): 265 passed. 13 new cases inTestPaginateRateLimitscover pacing from headers, missing/unparsable/past reset, the ceiling,429withRetry-After, with reset only, with no headers (backoff then give up), other errors still raising, and the stderr notice.uv run ruff check .,uv run ruff format --check .)dualentry invoices list --allagainst the mock bucket server above, before and after.