Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public static class Presets {
JitterStrategy.FULL // jitter
);

/** Linear retry strategy: 6 total attempts (1 initial + 5 retries) with 1s, 2s, 3s, 4s, and 5s delays. */
public static final RetryStrategy LINEAR = linearBackoff(6, Duration.ofSeconds(1), Duration.ofSeconds(1));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should match the format (with comments ) of the Default above.


/** No retry strategy - fails immediately on first error. Use this for operations that should not be retried. */
public static final RetryStrategy NO_RETRY = (error, attempt) -> RetryDecision.fail();
}
Expand Down Expand Up @@ -79,6 +82,33 @@ public static RetryStrategy exponentialBackoff(
};
}

/**
* Creates a linear backoff retry strategy.
*
* <p>The delay calculation follows the formula: delay = initialDelay + increment × (attempt-1)
*
* @param maxAttempts Maximum number of attempts (including initial attempt)
* @param initialDelay Initial delay before first retry
* @param increment Amount to add to the delay after each retry attempt
* @return RetryStrategy implementing linear backoff
*/
public static RetryStrategy linearBackoff(int maxAttempts, Duration initialDelay, Duration increment) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be more consistent with exponentialBackoff and have a maxDelay and jitterStrategy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want a jitter for this strategy?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like reference JS implementation doesn't have it #513, but the python SDK just added an implementation with jitter #484.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, they're aligned now #652

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, basically the preset LINEAR may not need a jitter but the RetryStrategy factory linearBackoff should support customizable jitter

if (maxAttempts <= 0) {
throw new IllegalArgumentException("maxAttempts must be positive");
}
ParameterValidator.validateDuration(initialDelay, "initialDelay");
ParameterValidator.validateDuration(increment, "increment");

return (error, attempt) -> {
if (attempt >= maxAttempts) {
return RetryDecision.fail();
}

var delay = initialDelay.plus(increment.multipliedBy(attempt - 1));
return RetryDecision.retry(delay);
};
}

/**
* Creates a simple retry strategy that retries a fixed number of times with a fixed delay.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,104 @@ void testFixedDelayStrategy() {
assertFalse(decision3.shouldRetry());
}

@Test
void linearBackoff_withCustomDelays_shouldIncreaseByIncrement() {
var strategy = RetryStrategies.linearBackoff(5, Duration.ofSeconds(2), Duration.ofSeconds(3));

var decision1 = strategy.makeRetryDecision(new RuntimeException("test"), 1);
var decision2 = strategy.makeRetryDecision(new RuntimeException("test"), 2);
var decision3 = strategy.makeRetryDecision(new RuntimeException("test"), 3);
var decision4 = strategy.makeRetryDecision(new RuntimeException("test"), 4);

assertTrue(decision1.shouldRetry());
assertEquals(Duration.ofSeconds(2), decision1.delay());

assertTrue(decision2.shouldRetry());
assertEquals(Duration.ofSeconds(5), decision2.delay());

assertTrue(decision3.shouldRetry());
assertEquals(Duration.ofSeconds(8), decision3.delay());

assertTrue(decision4.shouldRetry());
assertEquals(Duration.ofSeconds(11), decision4.delay());
}

@Test
void linearPreset_shouldUseOneThroughFiveSecondDelays() {
var strategy = RetryStrategies.Presets.LINEAR;

for (int attempt = 1; attempt <= 5; attempt++) {
var decision = strategy.makeRetryDecision(new RuntimeException("test"), attempt);

assertTrue(decision.shouldRetry(), "Should retry on attempt " + attempt);
assertEquals(Duration.ofSeconds(attempt), decision.delay());
}

var finalDecision = strategy.makeRetryDecision(new RuntimeException("test"), 6);
assertFalse(finalDecision.shouldRetry());
}

@Test
void linearBackoff_shouldStopAtMaxAttempts() {
var strategy = RetryStrategies.linearBackoff(3, Duration.ofSeconds(1), Duration.ofSeconds(1));

var decision1 = strategy.makeRetryDecision(new RuntimeException("test"), 1);
var decision2 = strategy.makeRetryDecision(new RuntimeException("test"), 2);
var decision3 = strategy.makeRetryDecision(new RuntimeException("test"), 3);

assertTrue(decision1.shouldRetry());
assertTrue(decision2.shouldRetry());
assertFalse(decision3.shouldRetry());
}

@Test
void linearBackoff_withInvalidMaxAttempts_shouldThrow() {
var exception = assertThrows(
IllegalArgumentException.class,
() -> RetryStrategies.linearBackoff(0, Duration.ofSeconds(1), Duration.ofSeconds(1)));

assertTrue(exception.getMessage().contains("maxAttempts"));
assertTrue(exception.getMessage().contains("positive"));
}

@Test
void linearBackoff_withSubSecondInitialDelay_shouldThrow() {
var exception = assertThrows(
IllegalArgumentException.class,
() -> RetryStrategies.linearBackoff(3, Duration.ofMillis(500), Duration.ofSeconds(1)));

assertTrue(exception.getMessage().contains("initialDelay"));
assertTrue(exception.getMessage().contains("at least 1 second"));
}

@Test
void linearBackoff_withNullInitialDelay_shouldThrow() {
var exception = assertThrows(
IllegalArgumentException.class, () -> RetryStrategies.linearBackoff(3, null, Duration.ofSeconds(1)));

assertTrue(exception.getMessage().contains("initialDelay"));
assertTrue(exception.getMessage().contains("cannot be null"));
}

@Test
void linearBackoff_withSubSecondIncrement_shouldThrow() {
var exception = assertThrows(
IllegalArgumentException.class,
() -> RetryStrategies.linearBackoff(3, Duration.ofSeconds(1), Duration.ofMillis(500)));

assertTrue(exception.getMessage().contains("increment"));
assertTrue(exception.getMessage().contains("at least 1 second"));
}

@Test
void linearBackoff_withNullIncrement_shouldThrow() {
var exception = assertThrows(
IllegalArgumentException.class, () -> RetryStrategies.linearBackoff(3, Duration.ofSeconds(1), null));

assertTrue(exception.getMessage().contains("increment"));
assertTrue(exception.getMessage().contains("cannot be null"));
}

@Test
void testInvalidParameters() {
assertThrows(
Expand Down
Loading