Skip to content

Commit 79f707b

Browse files
AdityaAudiAditya Audi
authored andcommitted
feat: expand RetryPresets with fixed, linear, and slow strategies
- Add BackoffStrategy enum (EXPONENTIAL, LINEAR, FIXED) to config.py - Add backoff_strategy field to RetryStrategyConfig (defaults to EXPONENTIAL) - Delegate delay calculation to BackoffStrategy.calculate_base_delay() - Add three new presets: fixed_wait, linear_backoff, slow - Add comprehensive tests for new backoff strategies and presets Resolves #328
1 parent 79fcb95 commit 79f707b

3 files changed

Lines changed: 365 additions & 6 deletions

File tree

src/aws_durable_execution_sdk_python/config.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,55 @@ def result(self, timeout_seconds: int | None = None) -> T:
453453
return self.future.result(timeout=timeout_seconds)
454454

455455

456+
# region Backoff
457+
458+
459+
class BackoffStrategy(StrEnum):
460+
"""
461+
Backoff strategies determine how retry delay grows between attempts.
462+
463+
members:
464+
:EXPONENTIAL: Delay grows exponentially: initial_delay * backoff_rate ^ (attempts - 1)
465+
:LINEAR: Delay grows linearly: initial_delay * attempts_made
466+
:FIXED: Delay stays constant: initial_delay
467+
"""
468+
469+
EXPONENTIAL = "EXPONENTIAL"
470+
LINEAR = "LINEAR"
471+
FIXED = "FIXED"
472+
473+
def calculate_base_delay(
474+
self,
475+
initial_delay_seconds: int,
476+
backoff_rate: Numeric,
477+
attempts_made: int,
478+
max_delay_seconds: int,
479+
) -> float:
480+
"""Calculate base delay before jitter for the given attempt.
481+
482+
Args:
483+
initial_delay_seconds: The initial delay in seconds.
484+
backoff_rate: The rate at which delay grows (used by EXPONENTIAL).
485+
attempts_made: Number of attempts already made (1-based).
486+
max_delay_seconds: Maximum delay cap in seconds.
487+
488+
Returns:
489+
The base delay in seconds, capped at max_delay_seconds.
490+
"""
491+
match self:
492+
case BackoffStrategy.FIXED:
493+
base: float = float(initial_delay_seconds)
494+
case BackoffStrategy.LINEAR:
495+
base = float(initial_delay_seconds) * attempts_made
496+
case _: # default is EXPONENTIAL
497+
base = initial_delay_seconds * (backoff_rate ** (attempts_made - 1))
498+
499+
return min(base, max_delay_seconds)
500+
501+
502+
# endregion Backoff
503+
504+
456505
# region Jitter
457506

458507

src/aws_durable_execution_sdk_python/retries.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
from dataclasses import dataclass, field
88
from typing import TYPE_CHECKING
99

10-
from aws_durable_execution_sdk_python.config import Duration, JitterStrategy
10+
from aws_durable_execution_sdk_python.config import (
11+
BackoffStrategy,
12+
Duration,
13+
JitterStrategy,
14+
)
1115

1216
if TYPE_CHECKING:
1317
from collections.abc import Callable
@@ -49,6 +53,7 @@ class RetryStrategyConfig:
4953
default_factory=lambda: Duration.from_minutes(5)
5054
) # 5 minutes
5155
backoff_rate: Numeric = 2.0
56+
backoff_strategy: BackoffStrategy = field(default=BackoffStrategy.EXPONENTIAL)
5257
jitter_strategy: JitterStrategy = field(default=JitterStrategy.FULL)
5358
retryable_errors: list[str | re.Pattern] | None = None
5459
retryable_error_types: list[type[Exception]] | None = None
@@ -103,10 +108,12 @@ def retry_strategy(error: Exception, attempts_made: int) -> RetryDecision:
103108
if not is_retryable_error_message and not is_retryable_error_type:
104109
return RetryDecision.no_retry()
105110

106-
# Calculate delay with exponential backoff
107-
base_delay: float = min(
108-
config.initial_delay_seconds * (config.backoff_rate ** (attempts_made - 1)),
109-
config.max_delay_seconds,
111+
# Calculate delay using configured backoff strategy
112+
base_delay: float = config.backoff_strategy.calculate_base_delay(
113+
initial_delay_seconds=config.initial_delay_seconds,
114+
backoff_rate=config.backoff_rate,
115+
attempts_made=attempts_made,
116+
max_delay_seconds=config.max_delay_seconds,
110117
)
111118
# Apply jitter to get final delay
112119
delay_with_jitter: float = config.jitter_strategy.apply_jitter(base_delay)
@@ -172,3 +179,42 @@ def critical(cls) -> Callable[[Exception, int], RetryDecision]:
172179
jitter_strategy=JitterStrategy.NONE,
173180
)
174181
)
182+
183+
@classmethod
184+
def fixed_wait(cls) -> Callable[[Exception, int], RetryDecision]:
185+
"""Constant delay between retries with no backoff."""
186+
return create_retry_strategy(
187+
RetryStrategyConfig(
188+
max_attempts=5,
189+
initial_delay=Duration.from_seconds(5),
190+
max_delay=Duration.from_minutes(5),
191+
backoff_strategy=BackoffStrategy.FIXED,
192+
jitter_strategy=JitterStrategy.NONE,
193+
)
194+
)
195+
196+
@classmethod
197+
def linear_backoff(cls) -> Callable[[Exception, int], RetryDecision]:
198+
"""Linearly increasing delay between retries."""
199+
return create_retry_strategy(
200+
RetryStrategyConfig(
201+
max_attempts=5,
202+
initial_delay=Duration.from_seconds(5),
203+
max_delay=Duration.from_minutes(5),
204+
backoff_strategy=BackoffStrategy.LINEAR,
205+
jitter_strategy=JitterStrategy.FULL,
206+
)
207+
)
208+
209+
@classmethod
210+
def slow(cls) -> Callable[[Exception, int], RetryDecision]:
211+
"""Long delays for operations that need extended recovery time."""
212+
return create_retry_strategy(
213+
RetryStrategyConfig(
214+
max_attempts=8,
215+
initial_delay=Duration.from_seconds(30),
216+
max_delay=Duration.from_minutes(10),
217+
backoff_rate=2,
218+
jitter_strategy=JitterStrategy.FULL,
219+
)
220+
)

0 commit comments

Comments
 (0)