Skip to content

fix(#6473): retry provider update on concurrent modification errors - #6474

Open
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/6473-provider-update-retry
Open

fix(#6473): retry provider update on concurrent modification errors#6474
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/6473-provider-update-retry

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown
Contributor

Summary

Add retry-with-jittered-exponential-backoff to the provider update path to handle optimistic concurrency conflicts. When multiple fullsend run invocations ensure the same provider concurrently, openshell returns a resource_version conflict error. The updateProvider function now retries up to 3 times (100ms/200ms/400ms with up to 50% jitter) on these specific errors, re-invoking openshell which re-reads the current resource version.

Related Issue

Fixes #6473

Changes

  • Add isConcurrentModificationErr() to detect the "provider was modified concurrently" error pattern from openshell stderr
  • Add updateRetries (3) and updateRetryBaseBackoff (100ms) constants for update-specific retry configuration
  • Modify updateProvider() to retry with jittered exponential backoff on concurrent modification errors only — other failures return immediately
  • Add 5 test cases covering: successful retry, max retries exhausted, no retry on other errors, context cancellation during backoff, and secret redaction in error messages

Testing

  • All new tests pass (go test -race ./internal/sandbox/...)
  • All existing tests continue to pass
  • Patch coverage at 100% for changed functions (updateProvider, isConcurrentModificationErr)
  • go vet clean
  • Secret scan clean

Closes #6473

Post-script verification

  • Branch is not main/master (agent/6473-provider-update-retry)
  • Secret scan passed (gitleaks — 25066fa47de491c5a8326a08c5ab506d55caec22..HEAD)
  • PR body secret scan passed (gitleaks — no-git)

When multiple fullsend run invocations ensure the same provider
concurrently, openshell returns a resource_version conflict:
"provider was modified concurrently". The updateProvider function
now retries with jittered exponential backoff (100ms/200ms/400ms,
3 attempts) on these specific errors. Each retry re-invokes
openshell, which re-reads the current resource_version,
resolving the conflict.

Only concurrent modification errors are retried — other update
failures return immediately. The jitter (up to 50% of the backoff
interval) reduces collision probability when multiple writers
retry at similar times.

Closes #6473
@fullsend-ai-coder
fullsend-ai-coder Bot requested a review from a team as a code owner August 22, 2026 01:20
@fullsend-ai-coder fullsend-ai-coder Bot added the ready-for-review Agent PR ready for human review label Aug 22, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 22, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:21 AM UTC · Completed 1:36 AM UTC

Commit: c5105ee · View workflow run →

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

Low

  • [edge-case] internal/sandbox/sandbox.go:415isConcurrentModificationErr is checked after secret redaction, so if a secret value is a substring of the openshell error phrase "provider was modified concurrently", the redaction corrupts the match string and retries silently stop triggering. In practice this is unlikely (credential values are tokens/keys, not common English words) but creates a latent correctness gap.
    Remediation: Check the raw output for the concurrent-modification pattern before redacting secrets.

  • [test-flakiness] internal/sandbox/sandbox_test.go:1516TestEnsureProvider_ConcurrentModification_ContextCancelled uses a hard-coded 80ms context timeout. Under heavy CI load, two shell-process spawns could exceed this window, causing the marker-count assertion to fail.
    Remediation: Increase the timeout to ~500ms, which still expires before the backoff completes but gives ample room for process spawning.

return fmt.Errorf("provider update %q failed: %w (output: %s)", name, err, outStr)
lastErr = fmt.Errorf("provider update %q failed: %w (output: %s)", name, err, outStr)

if !isConcurrentModificationErr(lastErr) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

isConcurrentModificationErr is checked after secret redaction, so if a secret value is a substring of the openshell error phrase "provider was modified concurrently" (e.g., the secret is literally "provider" or "modified"), the redaction corrupts the match string and retries silently stop triggering. In practice this is unlikely because credential values are tokens/keys, not common English words, but the ordering creates a latent correctness gap.

Suggested fix: Check the raw output for the concurrent-modification pattern before redacting secrets, e.g.: isConcurrent := strings.Contains(strings.ToLower(string(out)), "provider was modified concurrently"), then redact and build lastErr, then branch on isConcurrent.

// select picks up ctx.Done() during the backoff sleep. The update
// retry base backoff is 100ms, so 80ms is enough for one update
// attempt to run but not enough for the backoff to complete.
ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] test-flakiness

TestEnsureProvider_ConcurrentModification_ContextCancelled uses a hard-coded 80ms context timeout, relying on two shell-process spawns (create + update) completing within that window so the context expires during the backoff sleep rather than during process execution. Under heavy CI load, process spawning can exceed 80ms. If the context expires during process execution and before the marker file is written, the assertion assert.Len(t, entries, 1) could fail (0 entries instead of 1).

Suggested fix: Increase the timeout to a more conservative value (e.g., 500ms) that still expires well before the first backoff (100-150ms) completes but gives ample time for process spawning. Alternatively, use a cancellable context and cancel it explicitly after observing the first marker file.

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-merge All reviewers approved — ready to merge ready-for-review Agent PR ready for human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add retry with backoff in provider ensure path for concurrent modification conflicts

0 participants