Skip to content

refactor(http-client): replace nested backoff decorators with explicit retry loop#982

Draft
devin-ai-integration[bot] wants to merge 1 commit intomainfrom
devin/1775518713-refactor-http-client-retry
Draft

refactor(http-client): replace nested backoff decorators with explicit retry loop#982
devin-ai-integration[bot] wants to merge 1 commit intomainfrom
devin/1775518713-refactor-http-client-retry

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

Summary

Replaces the three-layer backoff.on_exception decorator chain in HttpClient._send_with_retry with a single explicit while True retry loop. This eliminates three control-flow-only exception types (UserDefinedBackoffException, RateLimitBackoffException, DefaultBackoffException) from the retry path, replacing them with a single RetryRequestException.

What changed:

  • exceptions.py: Added RetryRequestException(BaseBackoffException) with backoff_time and retry_endlessly fields.
  • http_client.py: Rewrote _send_with_retry as an explicit loop; added _compute_backoff helper; simplified _handle_error_resolution from three exception branches to one RetryRequestException raise.
  • rate_limiting.py: Deleted http_client_default_backoff_handler, user_defined_backoff_handler, and rate_limit_default_backoff_handler. Kept default_backoff_handler (exported from airbyte_cdk/__init__.py).
  • test_http_client.py: Updated all exception assertions to use RetryRequestException.

Backward compatibility: BaseBackoffException, DefaultBackoffException, UserDefinedBackoffException, and RateLimitBackoffException remain in exceptions.py and importable from airbyte_cdk/__init__.py. abstract_oauth.py (which independently uses DefaultBackoffException) is untouched.

Review & Testing Checklist for Human

  • Backoff timing shift: _compute_backoff returns 2 ** attempt where attempt starts at 1, yielding [2, 4, 8, …] seconds. The old backoff.expo with base 2 produced [1, 2, 4, …]. Verify this ~1s first-retry increase is acceptable for production connectors.
  • retry_endlessly field is set but not used in the loop: The budget is always enforced (max_tries / max_time). This matches the old behavior (the old rate_limit_default_backoff_handler also received max_tries), but the field on the exception is effectively dead code. Decide if it should be removed or kept for future use.
  • External code catching old exception types: Any connector or CDK consumer that catches DefaultBackoffException / UserDefinedBackoffException from HttpClient._send() will no longer catch the retry signal. Verify no external callers depend on this.
  • time.monotonic() for max_time budget: The old code used backoff library's internal elapsed tracking (based on time.time()). The new code uses time.monotonic(). Confirm this is appropriate and that freezegun-based test fixtures don't mask issues with the max_time path.

Recommended test plan: Run the full HTTP client test suite (pytest unit_tests/sources/streams/http/test_http_client.py -v) and rate limiting tests (pytest unit_tests/utils/test_rate_limiting.py -v). All 62 tests pass locally. Additionally, consider a manual integration test with a connector that hits rate limits to validate real-world retry behavior.

Notes

Link to Devin session: https://app.devin.ai/sessions/993ea9b604cb4ac2b88a360a614c3894

…t retry loop

- Add RetryRequestException to exceptions.py extending BaseBackoffException
- Rewrite _send_with_retry as explicit while-loop with _compute_backoff helper
- Simplify _handle_error_resolution to raise single RetryRequestException
- Delete 3 internal backoff handlers from rate_limiting.py (keep default_backoff_handler)
- Remove unused imports from http_client.py
- Update test_http_client.py to use RetryRequestException
- Preserve backward compatibility: all old exception classes remain importable

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

👋 Greetings, Airbyte Team Member!

Here are some helpful tips and reminders for your convenience.

💡 Show Tips and Tricks

Testing This CDK Version

You can test this version of the CDK using the following:

# Run the CLI from this branch:
uvx 'git+https://github.com/airbytehq/airbyte-python-cdk.git@devin/1775518713-refactor-http-client-retry#egg=airbyte-python-cdk[dev]' --help

# Update a connector to use the CDK from this branch ref:
cd airbyte-integrations/connectors/source-example
poe use-cdk-branch devin/1775518713-refactor-http-client-retry

PR Slash Commands

Airbyte Maintainers can execute the following slash commands on your PR:

  • /autofix - Fixes most formatting and linting issues
  • /poetry-lock - Updates poetry.lock file
  • /test - Runs connector tests with the updated CDK
  • /prerelease - Triggers a prerelease publish with default arguments
  • /poe build - Regenerate git-committed build artifacts, such as the pydantic models which are generated from the manifest JSON schema in YAML.
  • /poe <command> - Runs any poe command in the CDK environment
📚 Show Repo Guidance

Helpful Resources

📝 Edit this welcome message.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

PyTest Results (Fast)

4 012 tests  ±0   4 001 ✅ ±0   7m 38s ⏱️ -5s
    1 suites ±0      11 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit ed42701. ± Comparison against base commit 4aaafcf.

This pull request removes 15 and adds 15 tests. Note that renamed tests count towards both.
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[False-6-RateLimitBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[True-6-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[0.1-UserDefinedBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[None-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[0.1-UserDefinedBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[None-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-RateLimitBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-UserDefinedBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[401-FailureType.config_error-test error message-DefaultBackoffException]
…
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[False-6-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[True-6-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[0.1-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[None-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[0.1-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[None-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-default]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-rate_limited]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-user_defined]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[401-FailureType.config_error-test error message-default]
…

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

PyTest Results (Full)

4 015 tests  ±0   4 003 ✅ ±0   10m 52s ⏱️ -7s
    1 suites ±0      12 💤 ±0 
    1 files   ±0       0 ❌ ±0 

Results for commit ed42701. ± Comparison against base commit 4aaafcf.

This pull request removes 15 and adds 15 tests. Note that renamed tests count towards both.
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[False-6-RateLimitBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[True-6-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[0.1-UserDefinedBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[None-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[0.1-UserDefinedBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[None-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-DefaultBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-RateLimitBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-UserDefinedBackoffException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[401-FailureType.config_error-test error message-DefaultBackoffException]
…
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[False-6-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_backoff_strategy_endless[True-6-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[0.1-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_response_with_unmapped_error[None-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[0.1-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_raises_backoff_exception_with_retry_response_action[None-RetryRequestException]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-default]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-rate_limited]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[400-FailureType.system_error-test error message-user_defined]
unit_tests.sources.streams.http.test_http_client ‑ test_send_with_retry_raises_airbyte_traced_exception_with_failure_type[401-FailureType.config_error-test error message-default]
…

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.

0 participants