Skip to content

Retry a throttled page turn once after a 30-60 s pause; canary walk budget 15 MB - #21

Merged
jehrr merged 1 commit into
mainfrom
fix/throttle-retry-and-budget-2026-10-22
Oct 22, 2026
Merged

jehrr merged 1 commit into
mainfrom
fix/throttle-retry-and-budget-2026-10-22

Conversation

@jehrr

@jehrr jehrr commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Problem

Canary run 39858684619 (2026-10-22, main fd5e1c8, after the sticky-session change in #20) walked the listing through VRBO_PROXY with --block-media:

  • pages 1 and 2: 50 of 50 cards each, each turn arrived in about 2 s;
  • the press for page 3 got one /graphql HTTP 429 (Provisioned request rate has been exceeded, no Retry-After header), and the grid did not come back within 60 s;
  • exit 6, stop_reason: next_page_throttled, proxy traffic 8.7 MB of the 10 MB cap.

Two things were wrong for this walk. A single throttled turn ended the run with no second try, even though the 429 is a rate response that clears. And 8.7 MB for two pages leaves too little of the 10 MB cap for a third page plus any retry.

Change

One paced retry of a throttled page turn, in all three engines

  • page_flow.advance_with_throttle_retry wraps the existing turn. When a press gets no new grid (the state that ends next_page_throttled), it:
    1. checks the proxy budget;
    2. waits a random 2/3 to 4/3 of --throttle-wait-seconds (30-60 s at the default 45) in the same browser, session and exit, with no relaunch and no pool advance;
    3. checks the budget again;
    4. presses next once more.
  • A grid that arrived during the pause counts as the turn and is not pressed again, because a second press would skip a page.
  • A next button that is gone after the failed turn reads as another failed turn, never as the end of the listing.
  • A second failure ends the run as before: partial, exit 6, next_page_throttled.
  • A spent budget raises the existing ProxyBudgetExceeded before the pause, so no retry is made and the run stops with proxy_budget_exceeded.
  • The retry keys on the no-turnover state and not on a counted 429. Playwright counts the /graphql 429s, but Selenium has no response listener and cannot. The count is logged where it is known.
  • New flag --throttle-wait-seconds in playwright, selenium and pyppeteer: default 45, 0 = no retry. A negative value is an argparse error that names the flag.
  • New listing sidecar field throttle_retries: the retries made in the run, 0 when none.

Canary budget

  • The pagination job passes --proxy-budget-mb 15. first-page (no proxy) keeps 10, and the scraper's default stays 10.
  • .github/proxy_traffic.py already took the budget from the sidecar's proxy_budget_bytes. Its docstring still said "of 10 MB" and now describes that.

Docs

  • README: throttling section, the engine-flag list, and the measured 8.7 MB for two pages in the budget paragraph.
  • TROUBLESHOOTING: next_page_throttled.
  • CHANGELOG [Unreleased]: a behaviour-change blockquote plus Added and Changed entries.

Evidence

Offline suite

  • Core-only venv (Python 3.13, requirements.txt): python smoke_test.py gave 772 passed, 0 failed. The engine-import groups skip there.
  • .github/ci_checks.py --help-check --sample-check and --secret-check passed, and env_config.py ran.
  • Selenium venv: 791 passed, 0 failed. pyppeteer venv: 791 passed, 0 failed. In each one only the sibling engines were skipped. No browsers were downloaded.
  • No playwright venv was built locally because the host disk was full (about 120 MB free). CI's engine-smoke job covers playwright.

New check group test_throttle_retry

The pure helper is checked for:

  • a turn that succeeds the first time: no retry, no pause;
  • 429 then success: ADVANCED with 1 retry, two presses, one pause in 30-60 s, the budget checked twice;
  • 429 twice: NO_TURNOVER with 1 retry after exactly two presses;
  • a spent budget: the stop propagates, with one press and no pause;
  • 0 turns the retry off;
  • a late grid is not pressed again;
  • a button gone after the failure is not NO_BUTTON;
  • the pause is random and stays within its bounds.

Through each engine's real _fetch_one_page (engine copies with stub drivers, so this also runs in the core job):

  • 429 then success: page 3 is read, throttle_retries 1, no relaunch or new exit;
  • 429 twice: no_turnover after two presses;
  • a budget spent by the failed turn: ProxyBudgetExceeded, one press;
  • the flag value reaches the pause (3 gives 2-4 s);
  • 0 gives no retry, and the default is 45.

Through each engine's scrape():

  • a retried success: exit 0, 3 pages, throttle_retries 1;
  • throttled twice: exit 6, next_page_throttled, throttle_retries 1, 2 pages;
  • no throttle: throttle_retries 0.

Canary checks:

  • the first page runs at 10 MB and the walk at 15 MB;
  • the walk does not switch the retry off;
  • no summary step passes a fixed budget;
  • the summary script, given a 15 MB sidecar, prints "8.7 MB of 15 MB";
  • over a 15 MB budget it prints ::error::stopped: proxy budget 15 MB exceeded;
  • a null budget reads as "no budget".

Planted-fault controls

I ran 10 controls with PYTHONDONTWRITEBYTECODE=1 and __pycache__ cleared before each run. Each one turned the suite red on its named check, and the restored tree was green (0 failures):

  1. no retry at all
  2. two retries
  3. budget not checked before the retry
  4. pause not random
  5. late grid pressed again
  6. selenium ignores the flag
  7. pyppeteer drops the sidecar count
  8. playwright skips the budget check
  9. walk budget back to 10
  10. summary uses a fixed 10

No paid calls, no proxy and no credentials were used. No live request was made for this change.

Not changed

  • The scraper's default --proxy-budget-mb (10).
  • --delay, the canary schedule and the first-page job.
  • Block retries and rotation.
  • No version bump, tag or release.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CKsd7q25N19WzUZjrAdzx8

…udget 15 MB

A press for the next page that gets no grid back (the site's /graphql
answered 429 with no Retry-After) is now retried once in all three
engines, after a random pause of 2/3 to 4/3 of --throttle-wait-seconds
(default 45, 0 = off), in the same browser session and exit. The proxy
budget is checked before the pause and before the second press. A second
failure ends the run as before: partial, exit 6, next_page_throttled.
The listing sidecar records throttle_retries.

The canary's three-page walk passes --proxy-budget-mb 15 (the scraper's
default stays 10): with --block-media the 2026-10-22 walk used 8.7 MB for
pages 1 and 2 at 50 of 50 cards each.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CKsd7q25N19WzUZjrAdzx8
@jehrr
jehrr merged commit 30a6b72 into main Oct 22, 2026
7 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.

1 participant