Skip to content

[P2][Review] Do not retry exhausted durable quotas or shorten Retry-After into an early request #16

Closed

Description

@karlwaldman

Review-only / approval gate

No implementation, PR merge, package publication or production mutation until the owner approves the full sprint plan.

Evidence

Reviewed main 68795010c4c83b02ae5972959c8bd2321691aebf.

src/Client.php isRetryable uses only status429 or>=500. backoffDelay clamps Retry-After to30 seconds, so a 120-second instruction is followed by a request after30. Current Python client already distinguishes exhausted daily/monthly/trial counter windows from recoverable burst/circuit limits.

Preferred smallest change

Keep the small GET-only transport; add durable-quota classification and respect server minimum delay. If delay exceeds the configured budget, return typed RateLimitException with retry guidance instead of sleeping less. Do not add a generic scheduler or retry dependency.

Acceptance / negative-path tests

Mock response sequence with call/sleeper counters: exhausted monthly/daily/trial one request; burst429 retries; 120-second Retry-After does not send after30; HTTP-date/invalid/negative header behavior explicit; maxRetries=0 one request.

Risk and PR gate

P2, isolated correction; preserve public API compatibility.
One focused issue PR; independent adversarial review must challenge deletion safety, public compatibility, failure recovery and whether a smaller deletion suffices. Include red/green test evidence and packed/installable artifact proof for any published SDK/MCP change. Merge only in the approved priority order after reviewer objections are resolved. Do not publish a breaking change as a patch. If already fixed at execution time, record evidence and skip without a cosmetic PR.

Activity

  1. karlwaldman commented on Sep 5, 2026

    @karlwaldman
    MemberAuthor

    Adversarial sprint admission — rank 33/70

    Scheduled Monday, 2026-09-07 (America/New_York) in the approval-gated seven-night plan. Planning only; no implementation or merge yet.

    Narrow outcome: Remove futile quota retries and premature Retry-After requests.

    Required issue → PR → independent review → ordered merge evidence

    • Reproduce on the execution-day main and confirm exact files/callers; the review baseline was the current-main SHA recorded in this issue. If already fixed or unnecessary, record proof and close without a cosmetic PR.
    • Record why deletion alone does or does not solve this; delete redundant code before adding a helper/guard/automation.
    • A focused failing regression/fixture first where practical; test the relevant malformed, absent, stale, rejected or timeout path as well as success. No production customer mutations or raw-secret evidence.
    • One independently revertible PR, containing the narrow correction and targeted tests; no unrelated cleanup. Explain public/API compatibility and rollback.
    • Independent adversarial reviewer tests the failure case, challenges the assumptions and simpler alternatives, and resolves every blocking finding.
    • Refresh against the actual merge base and rerun checks before merging in this plan's numbered order. Preserve all existing required checks; no bypass to hit ten.
    • Link merge SHA plus approved deployed/published artifact and post-release proof where relevant; otherwise mark release pending rather than claiming shipped.

    Risk window: Only isolated low/medium-risk changes. If admission discovers authentication/entitlement, destructive migration, live ingestion semantics or a breaking interface change, hold and move to the next approved Friday/Saturday window.

  2. karlwaldman commented on Sep 5, 2026

    @karlwaldman
    MemberAuthor

    Coverage-audit revision — current sprint admission

    Plan v2: oilpriceapi-api#8015 supersedes earlier scheduling comments. Rank 37/70; Monday, 2026-09-07, America/New_York. Low/medium-risk only; reclassify to a Friday/Saturday slot if boundary/compatibility risk is higher.

    Intended outcome: Remove futile quota retries and premature Retry-After requests.

    Coverage ledger: #8056. Inventory/pattern screening is complete; exhaustive semantic/runtime sign-off is not. Before implementation, review the complete affected file set and callers, revalidate against current main, supply the missing negative-path proof, and retain prior scope corrections. Prefer deletion, then simplification; correct behavior needs no PR.

    No implementation is approved yet. After Karl approves: focused reversible PR → independent adversarial review → passing affected checks → merge in plan order → separately approved release/publish and artifact/customer-path verification. Maximum ten planned slots per night, not ten forced merges. If the dated window elapses before approval, re-anchor the calendar rather than backdate execution.

  3. karlwaldman commented on Sep 13, 2026

    @karlwaldman
    MemberAuthor

    Confirming this issue against production, and flagging one edge case PR #19 does not close. Executed 2026-09-13.

    The real refusal envelope matches what #19 is built for. Live GET https://api.oilpriceapi.com/v1/demo/prices:

    HTTP/2 429
    retry-after: 26385
    x-ratelimit-limit-day: 25
    x-ratelimit-remaining-day: 0
    
    {"status":"fail","error":{"code":"DEMO_RATE_LIMIT_EXCEEDED", ... "retry_after":26402 ...}}
    

    That is exactly the nested error.code shape isDurableQuotaExhausted() reads and exactly the Retry-After >> 30s case retryDelay() refuses to shorten. On main today this response costs 4 requests and 90 seconds of sleep against a limit that does not reset for 7.3 hours; under #19 it costs one request and no sleep. The policy is correct and it is aimed at the real production body, not a guess.

    Two notes:

    1. Not every zero-delay hot retry is closed by #19. The new guard rejects a negative Retry-After, but two other values still land on 0:

    Retry-After (int) cast main sleeps #19 sleeps
    -5 -5 [0] falls back to backoff (fixed)
    1e400 0 (INF cast to int) [0] [0]
    0.9 0 [0] [0]

    is_numeric('1e400') is true and (int) (float) '1e400' is 0 in PHP 8, so a malformed header produces an immediate retry rather than a backoff. Suggest requiring the value to match /^\d+$/ (RFC 9110 delta-seconds is a non-negative integer) and treating anything else as absent.

    2. The body carries its own delay. The live 429 has error.retry_after: 26402 alongside retry-after: 26385 in the header. Only the header is read, which is fine, but they can disagree and the body's value is the one the customer sees in the JSON.

    Unrelated to the retry logic: #19 branches from pre-#17 main and edits isRetryable() and the retry loop adjacent to #17's new normalizeApiPath()/assertSameOrigin(), so expect a conflict in src/Client.php on rebase.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions