[AI-7115] Add a request retry strategy to the async GitHub client - #24963
[AI-7115] Add a request retry strategy to the async GitHub client#24963AAraKKe wants to merge 3 commits into
Conversation
- New retry.py: RetryPolicy plus composable predicates, executed by stamina. - Split the two layers: _request retries, _rate_limited_request handles rate limits. - Per-endpoint defaults by replay safety, overridable per call with retry=. - Never follow or retry an unexpected redirect; report it with the endpoint. - Retry the artifact redirect and signed download as a pair. - Expose the limits through [dispatcher.github_retries].
evalya-impact-summaryevalya impact analysis |
🎉 All green!🧪 All tests passed 🚧 4 tests that failed were ignored due to quarantine 🎯 Code Coverage (details) 🔗 Commit SHA: f3b5071 | Docs | View more details | Give us feedback! |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c86bea94d0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| assert NO_RETRY.should_retry is never | ||
| assert NO_RETRY.attempts == 1 |
There was a problem hiding this comment.
Remove the implementation-only NO_RETRY test
These assertions verify the constant's internal predicate identity and configured field rather than any observable request behavior, and they duplicate test_a_caller_can_turn_retrying_off_for_one_call, which already proves that NO_RETRY prevents a failed request from being replayed. An equivalent implementation could therefore break this test without changing the contract; keep the boundary-level test instead.
AGENTS.md reference: AGENTS.md:L181-L185
Useful? React with 👍 / 👎.
- Move the retry config into dispatcher_config, next to the other config models. - Group module constants at the top of retry.py and trim the comments. - RetryPolicy is a plain class with a typed replace instead of a dataclass. - Move the client-specific guard and the retry cause into the client module. - Redact the query string from the artifact URL before logging it.
Validation ReportAll 21 validations passed. Show details
|
What does this PR do?
Adds a retry strategy to the async GitHub client for the failures that are not rate limiting, and separates it from the rate-limit handling that was already there.
Structure worth knowing before reading the diff:
_request(retries) wraps_rate_limited_request(today's loop, renamed). That order matters: each retry re-acquires the limiter, so it waits out any pause the governor is holding. Rate-limit responses stay owned by the inner layer and are never retried by the outer one.retry.pydescribes, stamina executes.RetryPolicyis data: what to retry on, how many attempts, what backoff. No sleeping or backoff arithmetic of ours.retry=to override, and policies compose.download_artifactretries as a pair. The signed URL expires, so the retry has to re-resolve the redirect rather than refetch a dead URL.[dispatcher.github_retries]). Widening what may be retried would make a duplicate side effect a setting.ddev/src/ddev/utils/github_async/AGENTS.mddocuments the layer boundary so the next change lands in the right one.Motivation
Closes AI-7115.
Dispatcher runs for hours and makes thousands of GitHub calls, and until now any failure that was not rate limiting failed on the first attempt. That gives a single blip more power than it should have.
TaskTestRunnerpollsget_workflow_runfor the whole life of a batch inside atry/finallywith noexcept, so one transient 500 aborts the batch, closes its check run as cancelled and throws away the results of every test in it. Other calls swallow the failure and quietly degrade instead: a failedlist_workflow_jobsreturns an empty job list, so job correlation silently loses data.Both get worse as we scale up: more batches and more polling mean more chances to hit the one blip that costs a whole batch of test results. Retrying is also a precondition for trusting the run report, since a report that is missing jobs because of a dropped connection is worse than one that is late.
No task behaviour changes here. Retries only make those paths less likely to fire, and a failure that outlives the ladder surfaces exactly as it does today.
Notes for review
ExecutionState.RETRYING,BatchProgress.retrying_jobs). This is unrelated and only concerns HTTP requests.test_no_retry_on_transport_errorbecametest_the_rate_limit_layer_does_not_retry_a_transport_errorand now calls_rate_limited_request. The property still holds for that layer, but at client level a GET transport error is now retried on purpose.GitHubAuthenticationError, which the guard refuses, so a real denial still fails immediately. Tested both ways.staminalogger, so retries are visible even with no logger injected. Turning it off is global and would also silence the unrelatedstamina.retryinddev/e2e/agent/docker.py, so I left it and documented it. Say the word if you want the client to be the only voice.Review checklist (to be filled by reviewers)
qa/requiredif this PR needs QA validation, orqa/skip-qaif it does not. Exactly one of the two is required.backport/<branch-name>label to the PR and it will automatically open a backport PR once this one is merged