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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.launchdarkly.eventsource;

import java.security.SecureRandom;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import static com.launchdarkly.eventsource.Helpers.millisFromTimeUnit;
Expand All @@ -9,59 +9,72 @@
* Default implementation of the retry delay strategy, providing exponential backoff
* and jitter.
* <p>
* The algorithm is as follows:
* <ul>
* <li> Start with the configured base delay as set by
* {@link EventSource.Builder#retryDelay(long, java.util.concurrent.TimeUnit)}. </li>
* <li> On each subsequent attempt, multiply the base delay by the backoff multiplier
* (default: {@link #DEFAULT_BACKOFF_MULTIPLIER}) giving the current base delay. </li>
* <li> If the maximum delay (default: {@link #DEFAULT_MAX_DELAY_MILLIS}) is
* non-zero, the base delay is pinned to be no greater than that value. </li>
* <li> If the jitter multipler (default: {@link #DEFAULT_JITTER_MULTIPLIER}) is
* non-zero, the actual delay for each attempt is equal to the current base delay
* minus a pseudo-random number equal to that ratio times itself. For instance, a
* jitter multiplier of 0.25 would mean that a base delay of 1000 is changed to
* a value in the range [750, 1000]. </li>
* </ul>
* Each instance is immutable: {@link #getDelayMillis()} returns the delay for this
* instance, and {@link #getNext()} returns the successor instance with the base
* delay multiplied by the backoff multiplier (pinned at the max delay). Jitter is
* rolled once per instance at construction so {@link #getDelayMillis()} is
* deterministic on a given instance.
* <p>
* This class is immutable. {@link RetryDelayStrategy#defaultStrategy()} returns the
* default instance. To change any parameters, call methods which return a modified
* instance:
* <pre><code>
* RetryDelayStrategy strategy = RetryDelayStrategy.defaultStrategy()
* .jitterMultiplier(0.25)
* .initialDelay(1, TimeUnit.SECONDS)
* .jitterMultiplier(0.25f)
* .maxDelay(20, TimeUnit.SECONDS);
* </code></pre>
*
* @since 4.0.0
*/
public class DefaultRetryDelayStrategy extends RetryDelayStrategy {
/**
* The default value for {@link #initialDelay(long, TimeUnit)}: 1 second.
*/
public static final long DEFAULT_INITIAL_DELAY_MILLIS = 1000;

/**
* The default value for {@link #maxDelay(long, TimeUnit)}: 30 seconds.
*/
public static final long DEFAULT_MAX_DELAY_MILLIS = 30000;

/**
* The default value for {@link #backoffMultiplier(float)}: 2.
*/
public static final float DEFAULT_BACKOFF_MULTIPLIER = 2;

/**
* The default value for {@link #jitterMultiplier(float)}: 0.5.
*/
public static final float DEFAULT_JITTER_MULTIPLIER = 0.5f;

static DefaultRetryDelayStrategy INSTANCE = new DefaultRetryDelayStrategy(0,
static final DefaultRetryDelayStrategy INSTANCE = new DefaultRetryDelayStrategy(
DEFAULT_INITIAL_DELAY_MILLIS,
DEFAULT_MAX_DELAY_MILLIS,
DEFAULT_BACKOFF_MULTIPLIER,
DEFAULT_JITTER_MULTIPLIER);
private final long lastBaseDelayMillis;

final long baseDelayMillis;
private final long maxDelayMillis;
private final float backoffMultiplier;
private final float jitterMultiplier;
private static final SecureRandom random = new SecureRandom();

private final long delayMillis;

/**
* Returns a modified strategy with a specific initial (base) delay. The returned
* instance is fresh — its backoff progression is reset.
*
* @param initialDelay the initial delay in whatever time unit is specified by {@code timeUnit}
* @param timeUnit the time unit, or {@code TimeUnit.MILLISECONDS} if null
* @return a new instance with the specified initial delay
* @since 5.0.0
* @see #DEFAULT_INITIAL_DELAY_MILLIS
*/
public DefaultRetryDelayStrategy initialDelay(long initialDelay, TimeUnit timeUnit) {
return new DefaultRetryDelayStrategy(millisFromTimeUnit(initialDelay, timeUnit),
this.maxDelayMillis, this.backoffMultiplier, this.jitterMultiplier);
}

/**
* Returns a modified strategy with a specific maximum delay.
*
Expand All @@ -71,7 +84,7 @@ public class DefaultRetryDelayStrategy extends RetryDelayStrategy {
* @see #DEFAULT_MAX_DELAY_MILLIS
*/
public DefaultRetryDelayStrategy maxDelay(long maxDelay, TimeUnit timeUnit) {
return new DefaultRetryDelayStrategy(lastBaseDelayMillis,
return new DefaultRetryDelayStrategy(this.baseDelayMillis,
millisFromTimeUnit(maxDelay, timeUnit),
this.backoffMultiplier,
this.jitterMultiplier
Expand All @@ -81,58 +94,68 @@ public DefaultRetryDelayStrategy maxDelay(long maxDelay, TimeUnit timeUnit) {
/**
* Returns a modified strategy with a specific backoff multipler. A multipler of 1
* means the base delay never changes, 2 means it doubles each time, etc.
*
*
* @param newBackoffMultiplier the backoff multipler
* @return a new instance with the specified backoff multiplier
* @see #DEFAULT_BACKOFF_MULTIPLIER
*/
public DefaultRetryDelayStrategy backoffMultiplier(float newBackoffMultiplier) {
return new DefaultRetryDelayStrategy(0, this.maxDelayMillis, newBackoffMultiplier, this.jitterMultiplier);
return new DefaultRetryDelayStrategy(this.baseDelayMillis, this.maxDelayMillis,
newBackoffMultiplier, this.jitterMultiplier);
}

/**
* Returns a modified strategy with a specific jitter multipler. A multipler of 0.5
* means each delay is reduced randomly by up to 50%, 0.25 means it is reduced
* randomly by up to 25%, etc. Zero means there is no jitter.
*
*
* @param newJitterMultiplier the jigger multipler
* @return a new instance with the specified jitter multipler
* @see #DEFAULT_JITTER_MULTIPLIER
*/
public DefaultRetryDelayStrategy jitterMultiplier(float newJitterMultiplier) {
return new DefaultRetryDelayStrategy(0, this.maxDelayMillis, this.backoffMultiplier, newJitterMultiplier);
return new DefaultRetryDelayStrategy(this.baseDelayMillis, this.maxDelayMillis,
this.backoffMultiplier, newJitterMultiplier);
}

private DefaultRetryDelayStrategy(
long lastBaseDelayMillis,
long baseDelayMillis,
long maxDelayMillis,
float backoffMultiplier,
float jitterMultiplier
) {
this.lastBaseDelayMillis = lastBaseDelayMillis;
this.baseDelayMillis = baseDelayMillis;
this.maxDelayMillis = maxDelayMillis;
this.backoffMultiplier = backoffMultiplier;
this.jitterMultiplier = jitterMultiplier;
}

@Override
public Result apply(long baseDelayMillis) {
long nextBaseDelay = lastBaseDelayMillis == 0 ? baseDelayMillis :
(long)(lastBaseDelayMillis * backoffMultiplier);
if (maxDelayMillis > 0 && nextBaseDelay > maxDelayMillis) {
nextBaseDelay = maxDelayMillis;
}
long adjustedDelay = nextBaseDelay;
if (jitterMultiplier > 0) {
long adjustedDelay = baseDelayMillis;
if (jitterMultiplier > 0 && baseDelayMillis > 0) {
// 2^31 milliseconds is much longer than any reconnect time we would reasonably want to use, so we can pin this to int
int maxTimeInt = nextBaseDelay > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)nextBaseDelay;
int maxTimeInt = baseDelayMillis > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)baseDelayMillis;
int jitterRange = Math.round(maxTimeInt * jitterMultiplier);
if (jitterRange > 0) {
adjustedDelay -= random.nextInt(jitterRange);
adjustedDelay -= ThreadLocalRandom.current().nextInt(jitterRange);
}
}
RetryDelayStrategy updatedStrategy =
new DefaultRetryDelayStrategy(nextBaseDelay, maxDelayMillis, backoffMultiplier, jitterMultiplier);
return new Result(adjustedDelay, updatedStrategy);
this.delayMillis = adjustedDelay;
}

@Override
public long getDelayMillis() {
return delayMillis;
}

@Override
public RetryDelayStrategy getNext() {
long nextBase = (long)(baseDelayMillis * backoffMultiplier);
if (maxDelayMillis > 0 && nextBase > maxDelayMillis) {
nextBase = maxDelayMillis;
}
return new DefaultRetryDelayStrategy(nextBase, maxDelayMillis, backoffMultiplier, jitterMultiplier);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Max delay not applied to current base

Medium Severity

maxDelay is enforced only when building the successor in getNext(), not when constructing the current instance. So getDelayMillis() can exceed the configured maximum whenever initialDelay or withBaseDelayMillis (including sticky retry: hints) sets a base above max. The previous apply() path pinned every attempt, including the first.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8fc09bd. Configure here.


@Override
public DefaultRetryDelayStrategy withBaseDelayMillis(long millis) {
return new DefaultRetryDelayStrategy(millis, maxDelayMillis, backoffMultiplier, jitterMultiplier);
}
}
Loading
Loading