Skip to content

Commit 664b6fd

Browse files
committed
feat: add linear retry strategy
1 parent 9216e93 commit 664b6fd

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

sdk/src/main/java/software/amazon/lambda/durable/retry/RetryStrategies.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ public static class Presets {
2828
JitterStrategy.FULL // jitter
2929
);
3030

31+
/** Linear retry strategy: 6 total attempts (1 initial + 5 retries) with 1s, 2s, 3s, 4s, and 5s delays. */
32+
public static final RetryStrategy LINEAR = linearBackoff(6, Duration.ofSeconds(1), Duration.ofSeconds(1));
33+
3134
/** No retry strategy - fails immediately on first error. Use this for operations that should not be retried. */
3235
public static final RetryStrategy NO_RETRY = (error, attempt) -> RetryDecision.fail();
3336
}
@@ -79,6 +82,33 @@ public static RetryStrategy exponentialBackoff(
7982
};
8083
}
8184

85+
/**
86+
* Creates a linear backoff retry strategy.
87+
*
88+
* <p>The delay calculation follows the formula: delay = initialDelay + increment × (attempt-1)
89+
*
90+
* @param maxAttempts Maximum number of attempts (including initial attempt)
91+
* @param initialDelay Initial delay before first retry
92+
* @param increment Amount to add to the delay after each retry attempt
93+
* @return RetryStrategy implementing linear backoff
94+
*/
95+
public static RetryStrategy linearBackoff(int maxAttempts, Duration initialDelay, Duration increment) {
96+
if (maxAttempts <= 0) {
97+
throw new IllegalArgumentException("maxAttempts must be positive");
98+
}
99+
ParameterValidator.validateDuration(initialDelay, "initialDelay");
100+
ParameterValidator.validateDuration(increment, "increment");
101+
102+
return (error, attempt) -> {
103+
if (attempt >= maxAttempts) {
104+
return RetryDecision.fail();
105+
}
106+
107+
var delay = initialDelay.plus(increment.multipliedBy(attempt - 1));
108+
return RetryDecision.retry(delay);
109+
};
110+
}
111+
82112
/**
83113
* Creates a simple retry strategy that retries a fixed number of times with a fixed delay.
84114
*

sdk/src/test/java/software/amazon/lambda/durable/retry/RetryStrategiesTest.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,104 @@ void testFixedDelayStrategy() {
138138
assertFalse(decision3.shouldRetry());
139139
}
140140

141+
@Test
142+
void linearBackoff_withCustomDelays_shouldIncreaseByIncrement() {
143+
var strategy = RetryStrategies.linearBackoff(5, Duration.ofSeconds(2), Duration.ofSeconds(3));
144+
145+
var decision1 = strategy.makeRetryDecision(new RuntimeException("test"), 1);
146+
var decision2 = strategy.makeRetryDecision(new RuntimeException("test"), 2);
147+
var decision3 = strategy.makeRetryDecision(new RuntimeException("test"), 3);
148+
var decision4 = strategy.makeRetryDecision(new RuntimeException("test"), 4);
149+
150+
assertTrue(decision1.shouldRetry());
151+
assertEquals(Duration.ofSeconds(2), decision1.delay());
152+
153+
assertTrue(decision2.shouldRetry());
154+
assertEquals(Duration.ofSeconds(5), decision2.delay());
155+
156+
assertTrue(decision3.shouldRetry());
157+
assertEquals(Duration.ofSeconds(8), decision3.delay());
158+
159+
assertTrue(decision4.shouldRetry());
160+
assertEquals(Duration.ofSeconds(11), decision4.delay());
161+
}
162+
163+
@Test
164+
void linearPreset_shouldUseOneThroughFiveSecondDelays() {
165+
var strategy = RetryStrategies.Presets.LINEAR;
166+
167+
for (int attempt = 1; attempt <= 5; attempt++) {
168+
var decision = strategy.makeRetryDecision(new RuntimeException("test"), attempt);
169+
170+
assertTrue(decision.shouldRetry(), "Should retry on attempt " + attempt);
171+
assertEquals(Duration.ofSeconds(attempt), decision.delay());
172+
}
173+
174+
var finalDecision = strategy.makeRetryDecision(new RuntimeException("test"), 6);
175+
assertFalse(finalDecision.shouldRetry());
176+
}
177+
178+
@Test
179+
void linearBackoff_shouldStopAtMaxAttempts() {
180+
var strategy = RetryStrategies.linearBackoff(3, Duration.ofSeconds(1), Duration.ofSeconds(1));
181+
182+
var decision1 = strategy.makeRetryDecision(new RuntimeException("test"), 1);
183+
var decision2 = strategy.makeRetryDecision(new RuntimeException("test"), 2);
184+
var decision3 = strategy.makeRetryDecision(new RuntimeException("test"), 3);
185+
186+
assertTrue(decision1.shouldRetry());
187+
assertTrue(decision2.shouldRetry());
188+
assertFalse(decision3.shouldRetry());
189+
}
190+
191+
@Test
192+
void linearBackoff_withInvalidMaxAttempts_shouldThrow() {
193+
var exception = assertThrows(
194+
IllegalArgumentException.class,
195+
() -> RetryStrategies.linearBackoff(0, Duration.ofSeconds(1), Duration.ofSeconds(1)));
196+
197+
assertTrue(exception.getMessage().contains("maxAttempts"));
198+
assertTrue(exception.getMessage().contains("positive"));
199+
}
200+
201+
@Test
202+
void linearBackoff_withSubSecondInitialDelay_shouldThrow() {
203+
var exception = assertThrows(
204+
IllegalArgumentException.class,
205+
() -> RetryStrategies.linearBackoff(3, Duration.ofMillis(500), Duration.ofSeconds(1)));
206+
207+
assertTrue(exception.getMessage().contains("initialDelay"));
208+
assertTrue(exception.getMessage().contains("at least 1 second"));
209+
}
210+
211+
@Test
212+
void linearBackoff_withNullInitialDelay_shouldThrow() {
213+
var exception = assertThrows(
214+
IllegalArgumentException.class, () -> RetryStrategies.linearBackoff(3, null, Duration.ofSeconds(1)));
215+
216+
assertTrue(exception.getMessage().contains("initialDelay"));
217+
assertTrue(exception.getMessage().contains("cannot be null"));
218+
}
219+
220+
@Test
221+
void linearBackoff_withSubSecondIncrement_shouldThrow() {
222+
var exception = assertThrows(
223+
IllegalArgumentException.class,
224+
() -> RetryStrategies.linearBackoff(3, Duration.ofSeconds(1), Duration.ofMillis(500)));
225+
226+
assertTrue(exception.getMessage().contains("increment"));
227+
assertTrue(exception.getMessage().contains("at least 1 second"));
228+
}
229+
230+
@Test
231+
void linearBackoff_withNullIncrement_shouldThrow() {
232+
var exception = assertThrows(
233+
IllegalArgumentException.class, () -> RetryStrategies.linearBackoff(3, Duration.ofSeconds(1), null));
234+
235+
assertTrue(exception.getMessage().contains("increment"));
236+
assertTrue(exception.getMessage().contains("cannot be null"));
237+
}
238+
141239
@Test
142240
void testInvalidParameters() {
143241
assertThrows(

0 commit comments

Comments
 (0)