Skip to content

Commit b8d1a0c

Browse files
authored
Merge branch 'main' into otel_logger
2 parents e3a53ee + fbd48f1 commit b8d1a0c

2 files changed

Lines changed: 139 additions & 0 deletions

File tree

packages/aws-durable-execution-sdk-python/src/aws_durable_execution_sdk_python/retries.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,34 @@ def retry_strategy(error: Exception, attempts_made: int) -> RetryDecision:
125125
return retry_strategy
126126

127127

128+
def create_linear_retry_strategy(
129+
max_attempts: int = 6,
130+
initial_delay: Duration | None = None,
131+
increment: Duration | None = None,
132+
) -> Callable[[Exception, int], RetryDecision]:
133+
"""Linearly increasing delay between retries: initial + increment * (attempts_made - 1).
134+
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``.
139+
"""
140+
initial: Duration = (
141+
initial_delay if initial_delay is not None else Duration.from_seconds(1)
142+
)
143+
step: Duration = increment if increment is not None else Duration.from_seconds(1)
144+
145+
def linear_retry_strategy(_error: Exception, attempts_made: int) -> RetryDecision:
146+
if attempts_made >= max_attempts:
147+
return RetryDecision.no_retry()
148+
delay_seconds: int = initial.to_seconds() + step.to_seconds() * (
149+
attempts_made - 1
150+
)
151+
return RetryDecision.retry(Duration(seconds=delay_seconds))
152+
153+
return linear_retry_strategy
154+
155+
128156
class RetryPresets:
129157
"""Default retry presets."""
130158

@@ -180,6 +208,31 @@ def critical(cls) -> Callable[[Exception, int], RetryDecision]:
180208
)
181209
)
182210

211+
@classmethod
212+
def linear(cls) -> Callable[[Exception, int], RetryDecision]:
213+
"""Linearly increasing delay between retries: 1s, 2s, 3s, 4s, 5s."""
214+
return create_linear_retry_strategy(
215+
max_attempts=6,
216+
initial_delay=Duration.from_seconds(1),
217+
increment=Duration.from_seconds(1),
218+
)
219+
220+
@classmethod
221+
def fixed(
222+
cls, interval: Duration | None = None
223+
) -> Callable[[Exception, int], RetryDecision]:
224+
"""Constant delay between retries (5s by default, no jitter)."""
225+
delay: Duration = interval if interval is not None else Duration.from_seconds(5)
226+
return create_retry_strategy(
227+
RetryStrategyConfig(
228+
max_attempts=5,
229+
initial_delay=delay,
230+
max_delay=delay,
231+
backoff_rate=1,
232+
jitter_strategy=JitterStrategy.NONE,
233+
)
234+
)
235+
183236

184237
@dataclass(frozen=True)
185238
class WithRetryConfig(Generic[T]):

packages/aws-durable-execution-sdk-python/tests/retries_test.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
RetryDecision,
1212
RetryPresets,
1313
RetryStrategyConfig,
14+
create_linear_retry_strategy,
1415
create_retry_strategy,
1516
)
1617

@@ -574,3 +575,88 @@ def test_mixed_error_types_and_patterns():
574575

575576

576577
# endregion
578+
579+
580+
# region create_linear_retry_strategy
581+
582+
583+
def test_linear_retry_strategy_uses_additive_formula():
584+
"""Default config yields delays of 1s, 2s, 3s, 4s, 5s with no jitter."""
585+
strategy = create_linear_retry_strategy()
586+
587+
delays = [
588+
strategy(Exception("e"), attempt).delay_seconds for attempt in range(1, 6)
589+
]
590+
591+
assert delays == [1, 2, 3, 4, 5]
592+
593+
594+
def test_linear_retry_strategy_stops_at_max_attempts():
595+
"""No retry once attempts_made reaches max_attempts."""
596+
strategy = create_linear_retry_strategy(max_attempts=3)
597+
598+
assert strategy(Exception("e"), 1).should_retry is True
599+
assert strategy(Exception("e"), 2).should_retry is True
600+
assert strategy(Exception("e"), 3).should_retry is False
601+
602+
603+
def test_linear_retry_strategy_respects_custom_initial_and_increment():
604+
"""Custom initial_delay and increment shift the additive sequence."""
605+
strategy = create_linear_retry_strategy(
606+
max_attempts=10,
607+
initial_delay=Duration.from_seconds(2),
608+
increment=Duration.from_seconds(3),
609+
)
610+
611+
delays = [
612+
strategy(Exception("e"), attempt).delay_seconds for attempt in range(1, 5)
613+
]
614+
615+
# 2 + 3*0, 2 + 3*1, 2 + 3*2, 2 + 3*3
616+
assert delays == [2, 5, 8, 11]
617+
618+
619+
# endregion
620+
621+
622+
# region RetryPresets.linear / RetryPresets.fixed
623+
624+
625+
def test_retry_presets_linear_matches_js_defaults():
626+
"""RetryPresets.linear() yields 1s, 2s, 3s, 4s, 5s and stops after 6 attempts."""
627+
strategy = RetryPresets.linear()
628+
629+
delays = [
630+
strategy(Exception("e"), attempt).delay_seconds for attempt in range(1, 6)
631+
]
632+
assert delays == [1, 2, 3, 4, 5]
633+
assert strategy(Exception("e"), 6).should_retry is False
634+
635+
636+
def test_retry_presets_fixed_uses_default_interval():
637+
"""RetryPresets.fixed() defaults to a 5s constant delay with no jitter."""
638+
strategy = RetryPresets.fixed()
639+
640+
for attempt in range(1, 5):
641+
decision = strategy(Exception("e"), attempt)
642+
assert decision.should_retry is True
643+
assert decision.delay_seconds == 5
644+
645+
646+
def test_retry_presets_fixed_respects_custom_interval():
647+
"""A caller-supplied interval is used as the constant delay."""
648+
strategy = RetryPresets.fixed(interval=Duration.from_seconds(12))
649+
650+
assert strategy(Exception("e"), 1).delay_seconds == 12
651+
assert strategy(Exception("e"), 3).delay_seconds == 12
652+
653+
654+
def test_retry_presets_fixed_stops_at_max_attempts():
655+
"""RetryPresets.fixed() stops retrying after 5 attempts."""
656+
strategy = RetryPresets.fixed()
657+
658+
assert strategy(Exception("e"), 4).should_retry is True
659+
assert strategy(Exception("e"), 5).should_retry is False
660+
661+
662+
# endregion

0 commit comments

Comments
 (0)