Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/aws_durable_execution_sdk_python/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,55 @@ def result(self, timeout_seconds: int | None = None) -> T:
return self.future.result(timeout=timeout_seconds)


# region Backoff


class BackoffStrategy(StrEnum):
"""
Backoff strategies determine how retry delay grows between attempts.

members:
:EXPONENTIAL: Delay grows exponentially: initial_delay * backoff_rate ^ (attempts - 1)
:LINEAR: Delay grows linearly: initial_delay * attempts_made
:FIXED: Delay stays constant: initial_delay
"""

EXPONENTIAL = "EXPONENTIAL"
LINEAR = "LINEAR"
FIXED = "FIXED"

def calculate_base_delay(
self,
initial_delay_seconds: int,
backoff_rate: Numeric,
attempts_made: int,
max_delay_seconds: int,
) -> float:
"""Calculate base delay before jitter for the given attempt.

Args:
initial_delay_seconds: The initial delay in seconds.
backoff_rate: The rate at which delay grows (used by EXPONENTIAL).
attempts_made: Number of attempts already made (1-based).
max_delay_seconds: Maximum delay cap in seconds.

Returns:
The base delay in seconds, capped at max_delay_seconds.
"""
match self:
case BackoffStrategy.FIXED:
base: float = float(initial_delay_seconds)
case BackoffStrategy.LINEAR:
base = float(initial_delay_seconds) * attempts_made
case _: # default is EXPONENTIAL
base = initial_delay_seconds * (backoff_rate ** (attempts_made - 1))

return min(base, max_delay_seconds)


# endregion Backoff


# region Jitter


Expand Down
56 changes: 51 additions & 5 deletions src/aws_durable_execution_sdk_python/retries.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

from aws_durable_execution_sdk_python.config import Duration, JitterStrategy
from aws_durable_execution_sdk_python.config import (
BackoffStrategy,
Duration,
JitterStrategy,
)

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

# Calculate delay with exponential backoff
base_delay: float = min(
config.initial_delay_seconds * (config.backoff_rate ** (attempts_made - 1)),
config.max_delay_seconds,
# Calculate delay using configured backoff strategy
base_delay: float = config.backoff_strategy.calculate_base_delay(
initial_delay_seconds=config.initial_delay_seconds,
backoff_rate=config.backoff_rate,
attempts_made=attempts_made,
max_delay_seconds=config.max_delay_seconds,
)
# Apply jitter to get final delay
delay_with_jitter: float = config.jitter_strategy.apply_jitter(base_delay)
Expand Down Expand Up @@ -172,3 +179,42 @@ def critical(cls) -> Callable[[Exception, int], RetryDecision]:
jitter_strategy=JitterStrategy.NONE,
)
)

@classmethod
def fixed_wait(cls) -> Callable[[Exception, int], RetryDecision]:
"""Constant delay between retries with no backoff."""
return create_retry_strategy(
RetryStrategyConfig(
max_attempts=5,
initial_delay=Duration.from_seconds(5),
max_delay=Duration.from_minutes(5),
backoff_strategy=BackoffStrategy.FIXED,
jitter_strategy=JitterStrategy.NONE,
)
)

@classmethod
def linear_backoff(cls) -> Callable[[Exception, int], RetryDecision]:
"""Linearly increasing delay between retries."""
return create_retry_strategy(
RetryStrategyConfig(
max_attempts=5,
initial_delay=Duration.from_seconds(5),
max_delay=Duration.from_minutes(5),
backoff_strategy=BackoffStrategy.LINEAR,
jitter_strategy=JitterStrategy.FULL,
)
)

@classmethod
def slow(cls) -> Callable[[Exception, int], RetryDecision]:
"""Long delays for operations that need extended recovery time."""
return create_retry_strategy(
RetryStrategyConfig(
max_attempts=8,
initial_delay=Duration.from_seconds(30),
max_delay=Duration.from_minutes(10),
backoff_rate=2,
jitter_strategy=JitterStrategy.FULL,
)
)
Loading