Repository navigation
(MODULES-11965) Retry provision_service on HTTP 5xx - #298
Merged
Merged
Conversation
invoke_cloud_request handled any non-200 response with `exit 1`. SystemExit is not a StandardError, so the RETRY_COUNT rescue loop in #provision never fired and the task died on the first 500 from the provision service. On the 2026-10-01 nightly that accounted for 389 of 451 failed jobs across 34 modules. - Raise ServiceError on non-200, and its ServerError subclass on 5xx, instead of exiting. - Retry ServerError, network errors and empty 200s up to 4 attempts with exponential backoff (30s, 60s, 120s) plus up to 50% jitter. 4xx still fails fast. - Log retries to stderr so stdout stays a valid JSON task result for Bolt/litmus. - Keep the provision_service/service_error envelope in .run, now with an attempts count. Co-Authored-By: Claude <noreply@anthropic.com>
imaqsood
requested review from
gavindidrichsen
and
a balanced review from Copilot
October 8, 2026 10:26
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Human confirmation is needed that retrying a partially completed provisioning POST cannot create duplicate or orphaned VMs.
0 open findings
What changed in this PR
Makes Provision Service retries handle HTTP 5xx failures, addressing #193 while preserving Bolt’s JSON error format.
Changes:
- Adds typed service errors and four attempts with exponential backoff and jitter.
- Sends retry logs to stderr and includes attempt counts in service errors.
- Adds specs for HTTP retry behavior, error output, and delay bounds.
| File | Description |
|---|---|
tasks/provision_service.rb |
Implements retry handling, backoff, and structured errors. |
spec/tasks/provision_service_spec.rb |
Covers retries, fail-fast responses, and error reporting. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
2 of 4 tasks
gavindidrichsen
approved these changes
Oct 12, 2026
gavindidrichsen
left a comment
Contributor
There was a problem hiding this comment.
Looks good to me! Very nice improvement to the retry mechanism.
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
provision_servicenever retried an HTTP 5xx from the provision service.invoke_cloud_requesthandled any non-200 response withexit 1, andSystemExitisn't aStandardError, so theRETRY_COUNTrescue loop in#provisionnever fired. The task died on the first 500.This PR:
ProvisionService::ServiceErroron non-200 responses, and itsServerErrorsubclass on 5xx, instead of callingexit 1;ServerError, network errors and empty 200 responses, up to 4 attempts. The waits double each time (30 s, 60 s, 120 s) plus up to 50% random jitter. A 4xx still fails fast;value['target_names']from it, so any extra text there breaks that JSON (the old retry message went to stdout);provision_service/service_errorerror JSON in.run, now with anattemptsfield so CI logs show whether retries happened;currnet_retry_counttypo and gives the empty-responseraisea message.Additional Context
On the 2026-10-01 supported-modules nightly, provision 500s caused 389 of 451 failed jobs across 34 modules. They're intermittent: about 32% of provisions failed at 00:00 UTC, rising to about 61% by 01:00 UTC, and they hit live image families too (rhel-8/9, windows-2022, debian-12). 21 of 22 sampled failing logs show no retry at all. Example runs: stdlib RedHat-9, acl Windows-2022.
Every module's
.fixtures.ymlclones this repo frommain, so this one change takes effect fleet-wide on the next nightly with no module PRs. The jitter matters because all nightly crons fire at0 0 * * *, and provisioning requests hit the service in a burst. Worst case, the backoff adds about 5 minutes to a job that would have failed anyway.Reviewer question: can the provision service partly create the VMs and still return a 500? If it can, retrying the POST could leave orphaned VMs behind. Our data doesn't answer this either way.
Update 2026-10-08 (from MODULES-11966 log analysis): The nightly 500s mostly come from failed
tofu applyruns in backend-release. The causes are the GCEFIREWALLSquota (500) being exhausted, and earlier in the window a us-west1-c stockout. A failed apply doesn't clean up its firewall rule, and the reaper doesn't remove those rules for 3h. So a 500 can leave orphaned resources behind (firewall rules, not VMs), and each client retry creates another rule. The retries here recover the start of the burst, but they may make the quota plateau worse. The real fix is on the service side (MODULES-11966: raise the quota, or use one shared rule instead of one per request). Reviewers: should this land as-is, or with fewer attempts until 11966 is fixed?Note on CI: the red "Spec tests (Puppet ~> 8.0)" check is the Codecov upload step added in cat-github-actions 70e9e0be. No coverage file is produced because
SIMPLECOV=yesisn't set. All 85 specs pass, and it is not a required check.Tear-down is unchanged: it still makes a single attempt, and a 5xx now surfaces as the same
service_errorJSON as before.Related Issues (if any)
Checklist
spec/tasks/provision_service_spec.rbpasses 22/22, with new cases for a 500 followed by a 200, retries running out (4 attempts, backoff order), a 4xx not being retried, the.runerror JSON andretry_delaybounds. Rubocop is clean.🤖 Generated with Claude Code