Skip to content

fix(wikis): wait out BWIKI's EdgeOne rate limiting instead of giving up after 3 s - #16

Merged
FridrichMethod merged 2 commits into
mainfrom
claude/charming-fermat-2zwugm
Oct 19, 2026
Merged

FridrichMethod merged 2 commits into
mainfrom
claude/charming-fermat-2zwugm

Conversation

@FridrichMethod

@FridrichMethod FridrichMethod commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Why

The scheduled Check wiki sources run of 2026-10-19 (run 39224451765) failed in discover_bwiki(). BWIKI answered a pages() batch with HTTP 567 (EdgeOne rate limiting) three times in a row:

requests.exceptions.HTTPError: 567 Server Error: Unknown Status for url: https://wiki.biligame.com/deemo/api.php?...

567 was already in RETRY_STATUSES, but it was retried on the same schedule as a server error: 3 attempts, 1 s and 2 s apart. The run stopped about 3 s after the first 567, well inside any rate-limit window.

What changes

scripts/fetch_wikis.py

  • New RATE_LIMITED = {429, 567}. These statuses get RATE_LIMIT_ATTEMPTS (6) attempts. When the server sends no Retry-After, the waits are 5, 10, 20, 30 and 30 s, about 95 s in all.
  • Connection errors, timeouts and 500/502/503/504 keep 3 attempts 1 s and 2 s apart, so a host that is down still fails fast.
  • The two kinds are counted apart: each has its own attempt limit and its own waits, whatever came before it. A request is made at most 8 times, and every failure advances one bounded counter, so the loop always ends.
  • A Retry-After still replaces any wait, capped at 30 s (unchanged).
  • download() no longer tries a Fandom candidate's other URLs after rate limiting. They're all on the host that is limiting, and without this one candidate could cost about 3 × 95 s. A server error still moves on to the next URL.
  • http_status() is the one place retried(), retry_delay() and download() read the status from.

docs/sources-wikis.md, docs/sources-wikis.zh-CN.md: the retry sentence describes both schedules and the separate counting. The Fandom paragraph says rate limiting fails the file at once.

tests/test_fetch_wikis.py

  • Pins both schedules (including connection errors and a 404 that fails at once) and five mixed sequences.
  • Pins the download rule for 429 and 567, with 503 for contrast.
  • Pins both doc mirrors against retry_delay() and the constants. The doc checks now share one assertDocumented() helper.
  • RetryTests caps the fake web at 20 requests, so a broken bound fails instead of hanging.

Review

A review of the first commit found that one shared counter let a 503 after two 567s go unretried, and that rate-limit waits didn't follow the documented schedule after other errors. Both are fixed in the second commit. Findings left as they are:

  • Ctrl-C delay during a rate-limited download: after the URL fix, the longest wait matches what a Retry-After: 30 could already cause.
  • Discover job's 30-minute timeout: a request that keeps failing still gives up after about 95 s with the named error.
  • Jitter and per-host pacing: larger design changes. They're the follow-up if the backoff alone proves insufficient.
  • Doc wording on other RequestExceptions: that was already true before this PR.

Validation

  • The new tests fail on the original code with the same HTTPError: 567 as the CI run. Undoing either review fix also fails them.
  • Every tests/test_*.py passes under coverage; total coverage is 89.6% (fail_under = 89).
  • pre-commit run on the changed files passes with the pinned hooks (ruff 0.17.0 check and format, mypy).

Caveat

BWIKI isn't reachable from the environment this was written in, so the new backoff hasn't been tried against the live site.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AZypKz9iupFY3Dhsd8Q1rK

…up after 3 s

The scheduled source check of 2026-10-19 (run 39224451765) failed in
discover_bwiki(): BWIKI answered a pages() batch with HTTP 567, EdgeOne's
rate limiting, three times in a row. 567 was already retried, but on the same
schedule as a server error, 3 attempts 1 s and 2 s apart, so the run stopped
about 3 s after the first 567, well inside any rate-limit window.

The rate-limit statuses (RATE_LIMITED: 429 and 567) now get
RATE_LIMIT_ATTEMPTS (6) calls, waiting 5, 10, 20, 30 and 30 s when the server
sends no Retry-After, about 95 s in all. Connection errors, timeouts and 500,
502, 503 and 504 keep 3 attempts 1 s and 2 s apart, so a host that is down
still fails fast. A Retry-After still replaces any wait, capped at 30 s.
Attempts count across statuses and the latest error decides the limit.

tests/test_fetch_wikis.py pins both schedules, the mixed case, and the
sentence in docs/sources-wikis.md and its Chinese mirror, computed from
retry_delay() and the constants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZypKz9iupFY3Dhsd8Q1rK
Copilot AI balanced review requested due to automatic review settings October 19, 2026 09:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

… URL after it

Review of the previous commit found two faults in retried(): one attempt
counter served both kinds of error, so a 503 after two 567s was not retried
at all, and the rate-limit waits were indexed by the overall attempt, so
503, 503, 567 waited 20 s after the first 567 instead of the documented 5 s.
Each kind now has its own counter, its own attempt limit and its own waits;
a request is made at most ATTEMPTS + RATE_LIMIT_ATTEMPTS - 1 (8) times, and
every failure advances one bounded counter, so the loop always ends.

download() tried a Fandom candidate's next URL after any requests error, and
each URL now has six rate-limited attempts: a rate-limited candidate would
have cost about 3 x 95 s. Rate limiting now fails the candidate at once, as a
RefusedDownload does; its URLs are all on the host that limits the rate. A
server error still moves on to the next URL.

http_status() reads the status for retried(), retry_delay() and download()
alike. RetryTests caps the fake web at 20 requests so a broken bound fails
instead of hanging; DocumentationTests shares one assertDocumented() helper.
Both docs say the kinds are counted apart and that rate limiting ends an
image download, and the doc test pins it. Undoing either fix fails the new
tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZypKz9iupFY3Dhsd8Q1rK
@FridrichMethod
FridrichMethod merged commit 688f299 into main Oct 19, 2026
6 checks passed
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.

3 participants