Skip to content

feat(declarative): add WaitTimeFromResponse backoff strategy - #1233

Draft
devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1792070042-constant-backoff-response-interpolation
Draft

devin-ai-integration[bot] wants to merge 7 commits into
mainfrom
devin/1792070042-constant-backoff-response-interpolation

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds a WaitTimeFromResponse backoff strategy so a manifest can compute the wait from the response body without a custom component. This covers APIs that only send the wait time in the body, such as Gmail: {"error": {"message": "... Retry after 2026-10-15T13:15:00Z"}}, with no Retry-After header.

WaitTimeFromResponseBackoffStrategy:

  • wait_time_in_seconds: an interpolated expression with config, response (the parsed JSON body, {} when the body isn't JSON or the input is an exception) and headers.
  • An empty, negative, non-finite or non-numeric result returns None, so DefaultErrorHandler moves on to the next strategy. So does an expression that cannot be evaluated on the response body (a list, or a string where an object was expected): the strategy runs on every retryable response, not only the one the expression was written for. A warning names the strategy and the error type, never the body.
  • Optional max_waiting_time_in_seconds, with the same helper and semantics as WaitTimeFromHeader: if the wait is >= the cap, it raises transient_error. The cap is validated at construction.
  • Registered in the schema, the DefaultErrorHandler.backoff_strategies union and ModelToComponentFactory.

Example (Gmail):

backoff_strategies:
  - type: WaitTimeFromHeader
    header: Retry-After
  - type: WaitTimeFromResponse
    wait_time_in_seconds: "{{ max(1, timestamp(response.error.message | regex_search('Retry after (\\S+Z)')) - now_utc().timestamp()) if (response is mapping and response.get('error') is mapping and ((response.error.message or '') | string | regex_search('Retry after (\\S+Z)'))) else '' }}"
    max_waiting_time_in_seconds: 1800
  - type: ExponentialBackoffStrategy   # used when the body has no timestamp
    factor: 5

The generated model in declarative_component_schema.py was edited by hand, because running poe assemble locally pulled in unrelated generator drift.

Tests: unit_tests/sources/declarative/requesters/error_handlers plus a factory test (518 passed together with test_model_to_component_factory.py). They cover the cap, empty and missing results, a non-JSON body, negative, NaN, infinite and non-numeric results, the floor-less retry_at - now expression with a past and a future time, and the Gmail example on five other body shapes.

Link to Devin session: https://app.devin.ai/sessions/4b1e021516004e0ab53c25b7293450a2
Open in Devin Desktop: https://app.devin.ai/desktop/session/4b1e021516004e0ab53c25b7293450a2?variant=devin

devin-ai-integration Bot and others added 2 commits October 15, 2026 13:21
…d add max_waiting_time_in_seconds

Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
@devin-ai-integration
devin-ai-integration Bot requested a review from a team as a code owner October 15, 2026 13:23
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@github-actions

Copy link
Copy Markdown

👋 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/1792070042-constant-backoff-response-interpolation#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/1792070042-constant-backoff-response-interpolation

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.

Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

PyTest Results (Fast)

5 793 tests  +39   5 762 ✅ +39   11m 29s ⏱️ -10s
    1 suites ± 0      31 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 19c24ff. ± Comparison against base commit 73ccaa9.

♻️ This comment has been updated with latest results.

Co-Authored-By: Daryna Ishchenko <darina.ishchenko17@gmail.com>
@devin-ai-integration devin-ai-integration Bot changed the title feat(declarative): interpolate response in ConstantBackoffStrategy and add max_waiting_time_in_seconds feat(declarative): add WaitTimeFromResponse backoff strategy Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

PyTest Results (Full)

5 796 tests  +39   5 765 ✅ +39   13m 13s ⏱️ + 3m 5s
    1 suites ± 0      31 💤 ± 0 
    1 files   ± 0       0 ❌ ± 0 

Results for commit 19c24ff. ± Comparison against base commit 73ccaa9.

♻️ This comment has been updated with latest results.

… non-numeric WaitTimeFromResponse result

A wait computed as `retry_at - now` goes negative once that time has passed. Returned as-is,
`HttpClient` kept it as a user-defined backoff and `time.sleep(wait + 1)` failed the stream
with "sleep length must be non-negative". A non-numeric result raised a bare `ValueError`
from `float()`, and NaN slipped past the cap into `sleep`.

- Return `None` for a negative or non-finite wait, as `WaitUntilTimeFromHeader` does for a
  past timestamp. A zero wait is still returned as 0, like `WaitTimeFromHeader`.
- Return `None` for a non-numeric result with a warning naming the strategy and the result's
  type (never its value), as the header strategies do.
- Say so in the `wait_time_in_seconds` description (schema and generated model, which still
  matches `datamodel-codegen==0.26.3` output byte for byte).
- Tests for negative, NaN, infinite and non-numeric results, and for the floor-less
  `timestamp(response.retry_at) - now_utc().timestamp()` expression with a past and a future
  time. Copyright year on the two new files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… not fit the response body

The strategy runs on every retryable response, not only the rate limit its expression was
written for. The documented Gmail example called `.get` on `response` and on `response.error`,
so a 5xx with a JSON list body or `{"error": "..."}` raised `UndefinedError` out of
`backoff_time` and failed the stream instead of falling back to the next strategy.

- Catch `UndefinedError`, `TypeError` and `ValueError` from the evaluation, log a warning with
  the error type (never the body), and return `None`.
- Make the example shape-safe (`response is mapping and response.get('error') is mapping and
  ...`) in the schema, the generated model and the test constant. The model still matches
  `datamodel-codegen==0.26.3` output byte for byte.
- Tests: the example on a list body, a string `error`, an `error` without `message`, a null
  `message` and a scalar body; and an expression that raises on the body.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@tolik0 Anatolii Yatsuk (tolik0) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review of the new WaitTimeFromResponse strategy. The change is additive and opt-in, and I confirmed the hand-edited model matches datamodel-codegen 0.26.3 output exactly. One P1 (an odd response body can turn the strategy off for the rest of the sync) and four P2s are inline below.

failure_type=FailureType.transient_error,
)

return wait_time

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: nothing bounds the wait when no max_waiting_time_in_seconds is set

Any finite positive value reaches time.sleep as it is. max_time does not shorten a sleep that has already started, because the sleep runs in the on_backoff handler (rate_limiting.py:110-119). Response bodies often carry absolute epoch or millisecond timestamps rather than relative seconds, so a one-character manifest mistake goes a long way:

  • {{ response.reset }} with {"reset": 1760500000} sleeps for about 55 years, until the heartbeat kills the job.
  • A value of about 9.2e9 or more makes time.sleep raise OverflowError: timestamp out of range for platform time_t, an untraced failure.

WaitTimeFromHeader has the same pattern, but headers rarely carry epochs. At minimum, please say in the field description that the result must be relative seconds and that a cap is strongly recommended. A hard ceiling here (return None with a warning above, say, a day) would be safer still.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Addressed the minimum in 19c24ff. The wait_time_in_seconds description now says the result must be in seconds relative to now, shows how to convert an absolute timestamp (timestamp(...) - now_utc().timestamp()), and says that nothing bounds the wait unless max_waiting_time_in_seconds is set, so setting it is strongly recommended. The generated model has the same text.

I did not add a hard ceiling. It would change behaviour, and WaitTimeFromHeader has none either. Leaving this thread open in case you think the CDK should enforce one here.

Comment thread airbyte_cdk/sources/declarative/declarative_component_schema.yaml Outdated
…ed response body

Addresses review feedback on the strategy's error handling and docs.

- Evaluate with `JinjaInterpolation` directly. `InterpolatedString` cached the template as
  plain text when a render `TypeError` returned it unchanged, so one odd body disabled the
  strategy for the rest of the sync. A result equal to the raw template now defers.
- Fail at construction when the expression uses a variable other than `config`, `response`,
  `headers` or `parameters`, instead of silently deferring on every response.
- Catch `ArithmeticError` (division by zero, float overflow) during evaluation and conversion.
- Fall back to an empty `response` on any unreadable body (`ValueError`,
  `RequestException`, `RecursionError`), not only `JSONDecodeError`.
- Descriptions: relative seconds, JSON body only, zero/unevaluable results defer, cap
  strongly recommended. Fix the code comment on zero waits.
- Tests: instance reuse after a failed render, arithmetic errors, unreadable body, undeclared
  variable, `$parameters`, factory-built `backoff_time`, and `HttpClient` end to end (sleep
  and cap-exceeded trace).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@tolik0 Anatolii Yatsuk (tolik0) left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed at 19c24ff. The plain-string cache fix and all four P2s check out: a failed render no longer disables the strategy, typos fail at startup, arithmetic and unreadable-body errors defer, and the new HttpClient tests cover the sleep and the cap. I am fine leaving the wait without a hard ceiling, for parity with WaitTimeFromHeader, now that the description recommends a cap.

This branch was successfully deployed

2 active deployments
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.

2 participants