Skip to content

Commit ec1a0f0

Browse files
Merge branch 'main' into devin/1778159323-failed-retry-wait-time
2 parents 1ee95c6 + 9fcc6de commit ec1a0f0

4 files changed

Lines changed: 93 additions & 10 deletions

File tree

airbyte_cdk/sources/declarative/declarative_component_schema.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,13 +1874,18 @@ definitions:
18741874
- "$ref": "#/definitions/CustomBackoffStrategy"
18751875
max_retries:
18761876
title: Max Retry Count
1877-
description: The maximum number of time to retry a retryable request before giving up and failing.
1878-
type: integer
1877+
description: The maximum number of times to retry a retryable request before giving up and failing. Can be a hardcoded integer or a string interpolated from the connector config.
1878+
anyOf:
1879+
- type: integer
1880+
- type: string
1881+
interpolation_context:
1882+
- config
18791883
default: 5
18801884
examples:
18811885
- 5
18821886
- 0
18831887
- 10
1888+
- "{{ config['max_retries_on_throttle'] }}"
18841889
response_filters:
18851890
title: Response Filters
18861891
description: List of response filters to iterate on when deciding how to handle an error. When using an array of multiple filters, the filters will be applied sequentially and the response will be selected if it matches any of the filter's predicate.

airbyte_cdk/sources/declarative/models/declarative_component_schema.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,10 +2045,10 @@ class DefaultErrorHandler(BaseModel):
20452045
description="List of backoff strategies to use to determine how long to wait before retrying a retryable request.",
20462046
title="Backoff Strategies",
20472047
)
2048-
max_retries: Optional[int] = Field(
2048+
max_retries: Optional[Union[int, str]] = Field(
20492049
5,
2050-
description="The maximum number of time to retry a retryable request before giving up and failing.",
2051-
examples=[5, 0, 10],
2050+
description="The maximum number of times to retry a retryable request before giving up and failing. Can be a hardcoded integer or a string interpolated from the connector config.",
2051+
examples=[5, 0, 10, "{{ config['max_retries_on_throttle'] }}"],
20522052
title="Max Retry Count",
20532053
)
20542054
response_filters: Optional[List[HttpResponseFilter]] = Field(

airbyte_cdk/sources/declarative/requesters/error_handlers/default_error_handler.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
33
#
44

5-
from dataclasses import InitVar, dataclass, field
5+
from dataclasses import InitVar, dataclass
66
from typing import Any, List, Mapping, MutableMapping, Optional, Union
77

88
import requests
99

10+
from airbyte_cdk.sources.declarative.interpolation import InterpolatedString
1011
from airbyte_cdk.sources.declarative.requesters.error_handlers.default_http_response_filter import (
1112
DefaultHttpResponseFilter,
1213
)
@@ -88,18 +89,23 @@ class DefaultErrorHandler(ErrorHandler):
8889
8990
Attributes:
9091
response_filters (Optional[List[HttpResponseFilter]]): response filters to iterate on
91-
max_retries (Optional[int]): maximum retry attempts
92+
max_retries (Optional[Union[int, str]]): maximum retry attempts. Either a hardcoded int or
93+
a string that interpolates from the connector config (e.g.
94+
`"{{ config['max_retries_on_throttle'] }}"`). The string variant is evaluated once at
95+
construction time and replaced with the resolved int.
9296
backoff_strategies (Optional[List[BackoffStrategy]]): list of backoff strategies to use to determine how long
9397
to wait before retrying
9498
"""
9599

96100
parameters: InitVar[Mapping[str, Any]]
97101
config: Config
98102
response_filters: Optional[List[HttpResponseFilter]] = None
99-
max_retries: Optional[int] = 5
103+
# The base class declares max_retries as Optional[int]. We widen the input type to
104+
# also accept a Jinja-interpolatable string (e.g. "{{ config['max_retries_on_throttle'] }}"),
105+
# which is resolved to an int in __post_init__ so the post-construction invariant matches
106+
# the base class contract.
107+
max_retries: Optional[Union[int, str]] = 5 # type: ignore[assignment]
100108
max_time: int = 60 * 10
101-
_max_retries: int = field(init=False, repr=False, default=5)
102-
_max_time: int = field(init=False, repr=False, default=60 * 10)
103109
backoff_strategies: Optional[List[BackoffStrategy]] = None
104110

105111
def __post_init__(self, parameters: Mapping[str, Any]) -> None:
@@ -108,6 +114,18 @@ def __post_init__(self, parameters: Mapping[str, Any]) -> None:
108114

109115
self._last_request_to_attempt_count: MutableMapping[requests.PreparedRequest, int] = {}
110116

117+
if isinstance(self.max_retries, str):
118+
evaluated = InterpolatedString(
119+
string=self.max_retries, default="5", parameters=parameters
120+
).eval(config=self.config)
121+
try:
122+
self.max_retries = int(evaluated)
123+
except (TypeError, ValueError) as exc:
124+
raise ValueError(
125+
f"DefaultErrorHandler.max_retries did not evaluate to an integer "
126+
f"(got {evaluated!r})"
127+
) from exc
128+
111129
def interpret_response(
112130
self, response_or_exception: Optional[Union[requests.Response, Exception]]
113131
) -> ErrorResolution:

unit_tests/sources/declarative/requesters/error_handlers/test_default_error_handler.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,3 +308,63 @@ def test_predicate_takes_precedent_over_default_mapped_error():
308308
assert actual_error_resolution.response_action == ResponseAction.FAIL
309309
assert actual_error_resolution.failure_type == FailureType.system_error
310310
assert actual_error_resolution.error_message == DEFAULT_ERROR_MAPPING.get(404).error_message
311+
312+
313+
def test_max_retries_default_when_unspecified():
314+
error_handler = DefaultErrorHandler(config={}, parameters={})
315+
assert error_handler.max_retries == 5
316+
317+
318+
def test_max_retries_with_literal_int():
319+
error_handler = DefaultErrorHandler(config={}, parameters={}, max_retries=10)
320+
assert error_handler.max_retries == 10
321+
322+
323+
def test_max_retries_with_zero():
324+
error_handler = DefaultErrorHandler(config={}, parameters={}, max_retries=0)
325+
assert error_handler.max_retries == 0
326+
327+
328+
def test_max_retries_interpolated_from_config():
329+
error_handler = DefaultErrorHandler(
330+
config={"max_retries_on_throttle": 1},
331+
parameters={},
332+
max_retries="{{ config['max_retries_on_throttle'] }}",
333+
)
334+
assert error_handler.max_retries == 1
335+
336+
337+
def test_max_retries_interpolated_from_config_with_jinja_default():
338+
error_handler = DefaultErrorHandler(
339+
config={},
340+
parameters={},
341+
max_retries="{{ config.get('max_retries_on_throttle', 7) }}",
342+
)
343+
assert error_handler.max_retries == 7
344+
345+
346+
def test_max_retries_interpolated_string_resolving_to_zero():
347+
error_handler = DefaultErrorHandler(
348+
config={"max_retries_on_throttle": 0},
349+
parameters={},
350+
max_retries="{{ config['max_retries_on_throttle'] }}",
351+
)
352+
assert error_handler.max_retries == 0
353+
354+
355+
def test_max_retries_interpolated_string_with_numeric_string():
356+
error_handler = DefaultErrorHandler(
357+
config={"max_retries_on_throttle": "3"},
358+
parameters={},
359+
max_retries="{{ config['max_retries_on_throttle'] }}",
360+
)
361+
assert error_handler.max_retries == 3
362+
363+
364+
def test_max_retries_raises_when_interpolation_does_not_resolve_to_int():
365+
with pytest.raises(ValueError, match="did not evaluate to an integer"):
366+
DefaultErrorHandler(
367+
config={"max_retries_on_throttle": "not-a-number"},
368+
parameters={},
369+
max_retries="{{ config['max_retries_on_throttle'] }}",
370+
)

0 commit comments

Comments
 (0)