Skip to content

feat(client): opt-in retry with backoff honoring Retry-After#58

Merged
Datata1 merged 1 commit into
mainfrom
refactor/07-retry-support
Jul 12, 2026
Merged

feat(client): opt-in retry with backoff honoring Retry-After#58
Datata1 merged 1 commit into
mainfrom
refactor/07-retry-support

Conversation

@Datata1

@Datata1 Datata1 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Implements plans/refactor/07-retry-support.md (final ticket):

  • RetryConfig frozen dataclass (max_retries=0, backoff_factor=0.5, retry_statuses={429,502,503,504}, retry_methods={GET,HEAD,PUT,DELETE}), exported from the top-level package and accepted by CodesphereSDK(retry=...)
  • Retry loop in APIHttpClient.request: idempotent methods only by default (POST opt-in via retry_methods), retries on the configured statuses and on httpx timeout/connect errors; delay honors Retry-After (delta-seconds or HTTP-date, past dates clamp to 0) and falls back to exponential backoff with jitter; exhausted retries raise exactly what the non-retry path would (RateLimitError with retry_after populated, TimeoutError, ...); each retry is logged at debug level
  • Plain ~40-line loop, no retry dependency reintroduced
  • Default max_retries=0 means zero behavior change unless opted in (tested: single request on failure with the default config)
  • 17 respx tests: Retry-After honored (429->200 with exactly 2 requests and >=1s sleep), exhaustion, exponential backoff bounds, non-retryable 500, POST gating and opt-in, transport-error retries, SDK wire-through, Retry-After parsing matrix
  • README documents RetryConfig

Implements plans/refactor/07-retry-support.md (final ticket):

- RetryConfig frozen dataclass (max_retries=0, backoff_factor=0.5,
  retry_statuses={429,502,503,504}, retry_methods={GET,HEAD,PUT,DELETE}),
  exported from the top-level package and accepted by
  CodesphereSDK(retry=...)
- Retry loop in APIHttpClient.request: idempotent methods only by
  default (POST opt-in via retry_methods), retries on the configured
  statuses and on httpx timeout/connect errors; delay honors Retry-After
  (delta-seconds or HTTP-date, past dates clamp to 0) and falls back to
  exponential backoff with jitter; exhausted retries raise exactly what
  the non-retry path would (RateLimitError with retry_after populated,
  TimeoutError, ...); each retry is logged at debug level
- Plain ~40-line loop, no retry dependency reintroduced
- Default max_retries=0 means zero behavior change unless opted in
  (tested: single request on failure with the default config)
- 17 respx tests: Retry-After honored (429->200 with exactly 2 requests
  and >=1s sleep), exhaustion, exponential backoff bounds, non-retryable
  500, POST gating and opt-in, transport-error retries, SDK wire-through,
  Retry-After parsing matrix
- README documents RetryConfig
@Datata1 Datata1 self-assigned this Jul 12, 2026
@Datata1 Datata1 merged commit 1a6c672 into main Jul 12, 2026
3 of 4 checks passed
@github-actions

Copy link
Copy Markdown

🛡️ Bandit Security Scan Results

✅ No security issues found by Bandit.

@github-actions

Copy link
Copy Markdown

Coverage

Pytest Coverage Report
FileStmtsMissBranchBrPartCoverMissing
__init__.py10000100% 
client.py40060100% 
config.py16000100% 
exceptions.py940380100% 
http_client.py98322296%124->162, 135->136, 136–137, 162
utils.py1024179%9->10, 10–11
core
   __init__.py4000100% 
   base.py55020100% 
   handler.py59318096%80–82
   operations.py15000100% 
resources
   __init__.py0000100% 
resources/metadata
   __init__.py3000100% 
   operations.py6000100% 
   resources.py13000100% 
   schemas.py28000100% 
resources/team
   __init__.py5000100% 
   operations.py8000100% 
   resources.py11000100% 
   schemas.py27000100% 
resources/team/domain
   __init__.py3000100% 
   operations.py11000100% 
   resources.py16000100% 
   schemas.py5550091%67, 69, 72, 75–76
resources/team/usage
   __init__.py3000100% 
   operations.py4000100% 
   resources.py4328292%99->102, 102, 162->165, 165
   schemas.py7076584%66->67, 67, 72–73, 76, 84–86
resources/workspace
   __init__.py5000100% 
   envVars.py4000100% 
   operations.py12000100% 
   resources.py1624280%13->14, 14, 19->20, 20
   schemas.py99146681%100–101, 103–109, 115–116, 118, 142, 161
resources/workspace/env_vars
   __init__.py3000100% 
   operations.py7000100% 
   resources.py26212484%33->35, 35–36, 38->exit
   schemas.py4000100% 
resources/workspace/git
   __init__.py3000100% 
   operations.py8000100% 
   resources.py15040100% 
   schemas.py4000100% 
resources/workspace/landscape
   __init__.py3000100% 
   operations.py12000100% 
   resources.py96736889%28->29, 29, 129->132, 135->138, 161->162, 162–163, 165->160, 168->169, 169, 173–175
   schemas.py2902910489%113–114, 117, 122–123, 126–127, 130, 135–137, 140–141, 144, 251, 258–259, 334–336, 338–340, 342–343, 458–459, 466–467
resources/workspace/logs
   __init__.py3000100% 
   operations.py5000100% 
   resources.py17550603763%66->68, 68, 97–98, 104, 106–108, 110, 113–114, 117, 120–121, 123–126, 128–129, 132–133, 135–136, 138–139, 141–143, 145–146, 148–152, 154–155, 157–159, 161–166, 179–180, 252–253
   schemas.py22000100% 
TOTAL15191262367189% 

Test Execution Summary

Tests Skipped Failures Errors Time
383 0 💤 0 ❌ 0 🔥 4.344s ⏱️

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