@@ -71,84 +71,135 @@ def max_delay_seconds(self) -> int:
7171 return self .max_delay .to_seconds ()
7272
7373
74+ @dataclass
75+ class LinearRetryStrategyConfig :
76+ max_attempts : int = 6
77+ initial_delay : Duration = field (default_factory = lambda : Duration .from_seconds (1 ))
78+ increment : Duration = field (default_factory = lambda : Duration .from_seconds (1 ))
79+ max_delay : Duration = field (default_factory = lambda : Duration .from_minutes (5 ))
80+ jitter_strategy : JitterStrategy = field (default = JitterStrategy .FULL )
81+ retryable_errors : list [str | re .Pattern ] | None = None
82+ retryable_error_types : list [type [Exception ]] | None = None
83+
84+ @property
85+ def initial_delay_seconds (self ) -> int :
86+ """Get initial delay in seconds."""
87+ return self .initial_delay .to_seconds ()
88+
89+ @property
90+ def increment_seconds (self ) -> int :
91+ """Get increment in seconds."""
92+ return self .increment .to_seconds ()
93+
94+ @property
95+ def max_delay_seconds (self ) -> int :
96+ """Get max delay in seconds."""
97+ return self .max_delay .to_seconds ()
98+
99+
100+ def _resolve_retryable_errors (
101+ retryable_errors : list [str | re .Pattern ] | None ,
102+ retryable_error_types : list [type [Exception ]] | None ,
103+ ) -> tuple [list [str | re .Pattern ], list [type [Exception ]]]:
104+ """Resolve the error filters, applying the match-all default only when neither is set."""
105+ should_use_default_errors : bool = (
106+ retryable_errors is None and retryable_error_types is None
107+ )
108+ resolved_errors : list [str | re .Pattern ] = (
109+ retryable_errors
110+ if retryable_errors is not None
111+ else ([_DEFAULT_RETRYABLE_ERROR_PATTERN ] if should_use_default_errors else [])
112+ )
113+ resolved_error_types : list [type [Exception ]] = retryable_error_types or []
114+ return resolved_errors , resolved_error_types
115+
116+
117+ def _is_error_retryable (
118+ error : Exception ,
119+ retryable_errors : list [str | re .Pattern ],
120+ retryable_error_types : list [type [Exception ]],
121+ ) -> bool :
122+ """Return True when the error matches one of the message patterns or types."""
123+ is_retryable_error_message : bool = any (
124+ pattern .search (str (error ))
125+ if isinstance (pattern , re .Pattern )
126+ else pattern in str (error )
127+ for pattern in retryable_errors
128+ )
129+ is_retryable_error_type : bool = any (
130+ isinstance (error , error_type ) for error_type in retryable_error_types
131+ )
132+ return is_retryable_error_message or is_retryable_error_type
133+
134+
135+ def _finalize_delay_seconds (base_delay : float , jitter_strategy : JitterStrategy ) -> int :
136+ """Apply jitter, round up, and clamp to a minimum of 1 second."""
137+ delay_with_jitter : float = jitter_strategy .apply_jitter (base_delay )
138+ return max (1 , math .ceil (delay_with_jitter ))
139+
140+
74141def create_retry_strategy (
75142 config : RetryStrategyConfig | None = None ,
76143) -> Callable [[Exception , int ], RetryDecision ]:
77144 if config is None :
78145 config = RetryStrategyConfig ()
79146
80- # Apply default retryableErrors only if user didn't specify either filter
81- should_use_default_errors : bool = (
82- config .retryable_errors is None and config .retryable_error_types is None
83- )
84-
85- retryable_errors : list [str | re .Pattern ] = (
86- config .retryable_errors
87- if config .retryable_errors is not None
88- else ([_DEFAULT_RETRYABLE_ERROR_PATTERN ] if should_use_default_errors else [])
147+ retryable_errors , retryable_error_types = _resolve_retryable_errors (
148+ config .retryable_errors , config .retryable_error_types
89149 )
90- retryable_error_types : list [type [Exception ]] = config .retryable_error_types or []
91150
92151 def retry_strategy (error : Exception , attempts_made : int ) -> RetryDecision :
93152 # Check if we've exceeded max attempts
94153 if attempts_made >= config .max_attempts :
95154 return RetryDecision .no_retry ()
96155
97- # Check if error is retryable based on error message
98- is_retryable_error_message : bool = any (
99- pattern .search (str (error ))
100- if isinstance (pattern , re .Pattern )
101- else pattern in str (error )
102- for pattern in retryable_errors
103- )
104-
105- # Check if error is retryable based on error type
106- is_retryable_error_type : bool = any (
107- isinstance (error , error_type ) for error_type in retryable_error_types
108- )
109-
110- if not is_retryable_error_message and not is_retryable_error_type :
156+ if not _is_error_retryable (error , retryable_errors , retryable_error_types ):
111157 return RetryDecision .no_retry ()
112158
113159 # Calculate delay with exponential backoff
114160 base_delay : float = min (
115161 config .initial_delay_seconds * (config .backoff_rate ** (attempts_made - 1 )),
116162 config .max_delay_seconds ,
117163 )
118- # Apply jitter to get final delay
119- delay_with_jitter : float = config .jitter_strategy .apply_jitter (base_delay )
120- # Round up and ensure minimum of 1 second
121- final_delay : int = max (1 , math .ceil (delay_with_jitter ))
164+ final_delay : int = _finalize_delay_seconds (base_delay , config .jitter_strategy )
122165
123166 return RetryDecision .retry (Duration (seconds = final_delay ))
124167
125168 return retry_strategy
126169
127170
128171def create_linear_retry_strategy (
129- max_attempts : int = 6 ,
130- initial_delay : Duration | None = None ,
131- increment : Duration | None = None ,
172+ config : LinearRetryStrategyConfig | None = None ,
132173) -> Callable [[Exception , int ], RetryDecision ]:
133- """Linearly increasing delay between retries: initial + increment * (attempts_made - 1) .
174+ """Linearly increasing delay between retries.
134175
135- Mirrors the JS SDK's ``createLinearRetryStrategy``. With the defaults this
136- yields delays of 1s, 2s, 3s, 4s, 5s. No jitter is applied and there is no
137- upper cap on the delay; callers who need either can build their own
138- strategy via ``create_retry_strategy ``.
176+ The base delay is ``initial_delay + increment * (attempts_made - 1)``,
177+ capped at ``max_delay``, with jitter and error filtering applied the same
178+ way as :func:`create_retry_strategy`. Mirrors the JS SDK's
179+ ``createLinearRetryStrategy ``.
139180 """
140- initial : Duration = (
141- initial_delay if initial_delay is not None else Duration .from_seconds (1 )
181+ if config is None :
182+ config = LinearRetryStrategyConfig ()
183+
184+ retryable_errors , retryable_error_types = _resolve_retryable_errors (
185+ config .retryable_errors , config .retryable_error_types
142186 )
143- step : Duration = increment if increment is not None else Duration .from_seconds (1 )
144187
145- def linear_retry_strategy (_error : Exception , attempts_made : int ) -> RetryDecision :
146- if attempts_made >= max_attempts :
188+ def linear_retry_strategy (error : Exception , attempts_made : int ) -> RetryDecision :
189+ if attempts_made >= config .max_attempts :
190+ return RetryDecision .no_retry ()
191+
192+ if not _is_error_retryable (error , retryable_errors , retryable_error_types ):
147193 return RetryDecision .no_retry ()
148- delay_seconds : int = initial .to_seconds () + step .to_seconds () * (
149- attempts_made - 1
194+
195+ base_delay : float = min (
196+ config .initial_delay_seconds
197+ + config .increment_seconds * (attempts_made - 1 ),
198+ config .max_delay_seconds ,
150199 )
151- return RetryDecision .retry (Duration (seconds = delay_seconds ))
200+ final_delay : int = _finalize_delay_seconds (base_delay , config .jitter_strategy )
201+
202+ return RetryDecision .retry (Duration (seconds = final_delay ))
152203
153204 return linear_retry_strategy
154205
@@ -212,9 +263,12 @@ def critical(cls) -> Callable[[Exception, int], RetryDecision]:
212263 def linear (cls ) -> Callable [[Exception , int ], RetryDecision ]:
213264 """Linearly increasing delay between retries: 1s, 2s, 3s, 4s, 5s."""
214265 return create_linear_retry_strategy (
215- max_attempts = 6 ,
216- initial_delay = Duration .from_seconds (1 ),
217- increment = Duration .from_seconds (1 ),
266+ LinearRetryStrategyConfig (
267+ max_attempts = 6 ,
268+ initial_delay = Duration .from_seconds (1 ),
269+ increment = Duration .from_seconds (1 ),
270+ jitter_strategy = JitterStrategy .NONE ,
271+ )
218272 )
219273
220274 @classmethod
0 commit comments