Skip to content

Commit 3110076

Browse files
guidobreitoddbaert
andauthored
feat: tolerate immediately recoverable stream faults, improve logging (#1019)
Signed-off-by: Guido Breitenhuber <[email protected]> Signed-off-by: Todd Baert <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 67682a8 commit 3110076

25 files changed

+1082
-116
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.backoff;
2+
3+
import lombok.Getter;
4+
5+
import java.util.concurrent.ThreadLocalRandom;
6+
7+
/**
8+
* A service that provides backoff functionality.
9+
*/
10+
public class BackoffService {
11+
public static final int DEFAULT_MAX_JITTER = 0x1 << 8; // 256; Random likes boundaries that are a power of 2
12+
13+
@Getter
14+
private final BackoffStrategy strategy;
15+
16+
@Getter
17+
private final int maxJitter;
18+
19+
/**
20+
* Creates a new BackoffService with the given strategy and default maximum jitter.
21+
* The default maximum jitter is 256.
22+
*
23+
* @param strategy The backoff strategy to use
24+
*/
25+
public BackoffService(BackoffStrategy strategy) {
26+
this(strategy, DEFAULT_MAX_JITTER);
27+
}
28+
29+
/**
30+
* Creates a new BackoffService with the given strategy and maximum jitter.
31+
*
32+
* @param strategy The backoff strategy to use
33+
* @param maxJitter The maximum jitter value
34+
*/
35+
public BackoffService(BackoffStrategy strategy, int maxJitter) {
36+
this.strategy = strategy;
37+
this.maxJitter = maxJitter;
38+
}
39+
40+
/**
41+
* Returns the current backoff time in milliseconds.
42+
* This backoff time will be used in waitUntilNextAttempt.
43+
*
44+
* @return the current backoff time in milliseconds
45+
*/
46+
public long getCurrentBackoffMillis() {
47+
return strategy.getCurrentBackoffMillis();
48+
}
49+
50+
/**
51+
* Returns a random jitter value between 0 and maxJitter.
52+
*
53+
* @return a random jitter value
54+
*/
55+
public long getRandomJitter() {
56+
if (maxJitter == 0) {
57+
return 0;
58+
}
59+
60+
return ThreadLocalRandom.current().nextInt(maxJitter);
61+
}
62+
63+
/**
64+
* Resets the backoff strategy to its initial state.
65+
*/
66+
public void reset() {
67+
strategy.reset();
68+
}
69+
70+
/**
71+
* Returns whether the backoff strategy has more attempts left.
72+
* @return true if the backoff strategy has more attempts left, false otherwise
73+
*/
74+
public boolean shouldRetry() {
75+
return !strategy.isExhausted();
76+
}
77+
78+
/**
79+
* Bolocks the current thread until the next attempt should be made.
80+
* The time to wait is determined by the backoff strategy and a random jitter.
81+
*
82+
* @throws InterruptedException if the thread is interrupted while waiting
83+
*/
84+
public void waitUntilNextAttempt() throws InterruptedException {
85+
long retryDelay = getCurrentBackoffMillis() + getRandomJitter();
86+
strategy.nextBackoff();
87+
88+
Thread.sleep(retryDelay);
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.backoff;
2+
3+
/**
4+
* A factory class for creating common backoff strategies.
5+
*/
6+
public class BackoffStrategies {
7+
private BackoffStrategies() {
8+
}
9+
10+
public static BackoffStrategy exponentialTimeBackoff(long initialBackoffMillis) {
11+
return new ExponentialTimeBackoff(initialBackoffMillis);
12+
}
13+
14+
public static BackoffStrategy exponentialTimeBackoff(long initialBackoffMillis, long maxBackoffMillis) {
15+
return new ExponentialTimeBackoff(initialBackoffMillis, maxBackoffMillis);
16+
}
17+
18+
public static BackoffStrategy constantTimeBackoff(long millis) {
19+
return new ConstantTimeBackoff(millis);
20+
}
21+
22+
public static BackoffStrategy noBackoff() {
23+
return new ConstantTimeBackoff(0L);
24+
}
25+
26+
public static BackoffStrategy maxRetriesWithExponentialTimeBackoffStrategy(int maxRetries,
27+
long initialBackoffMillis) {
28+
return new NumberOfRetriesBackoff(maxRetries, exponentialTimeBackoff(initialBackoffMillis));
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.backoff;
2+
3+
/**
4+
* A strategy interface for determining how long to backoff before retrying a failed operation.
5+
*/
6+
public interface BackoffStrategy {
7+
8+
/**
9+
* The current backoff time in milliseconds.
10+
* This value should be used to determine how long to wait before retrying.
11+
*
12+
* @return the current backoff time in milliseconds
13+
*/
14+
long getCurrentBackoffMillis();
15+
16+
/**
17+
* Determines if the backoff strategy has been exhausted.
18+
*
19+
* @return true if the operation should backoff, false otherwise
20+
*/
21+
boolean isExhausted();
22+
23+
/**
24+
* Move to the next backoff time.
25+
*/
26+
void nextBackoff();
27+
28+
/**
29+
* Reset the backoff strategy to its initial state.
30+
*/
31+
void reset();
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.backoff;
2+
3+
import lombok.Getter;
4+
5+
/**
6+
* A backoff strategy that combines multiple backoff strategies.
7+
* The strategy starts with the first provided strategy and will switch to the next backoff strategy in the list when
8+
* the current one is exhausted.
9+
*/
10+
public class CombinedBackoff implements BackoffStrategy {
11+
private final BackoffStrategy[] backoffStrategies;
12+
private int currentStrategyIndex;
13+
14+
@Getter
15+
private BackoffStrategy currentStrategy;
16+
17+
/**
18+
* Creates a new combined backoff strategy.
19+
* The strategy starts with the first provided strategy and will switch to the next backoff strategy in the list
20+
* when the current one is exhausted.
21+
*
22+
* @param backoffStrategies the list of backoff strategies to combine
23+
*/
24+
25+
public CombinedBackoff(BackoffStrategy[] backoffStrategies) {
26+
this.backoffStrategies = backoffStrategies.clone();
27+
this.currentStrategyIndex = 0;
28+
this.currentStrategy = this.backoffStrategies[currentStrategyIndex];
29+
updateCurrentStrategy();
30+
}
31+
32+
@Override
33+
public long getCurrentBackoffMillis() {
34+
return currentStrategy.getCurrentBackoffMillis();
35+
}
36+
37+
@Override
38+
public boolean isExhausted() {
39+
updateCurrentStrategy();
40+
return currentStrategy.isExhausted();
41+
}
42+
43+
@Override
44+
public void nextBackoff() {
45+
updateCurrentStrategy();
46+
currentStrategy.nextBackoff();
47+
}
48+
49+
/**
50+
* Switches to the next backoff strategy if the current one is exhausted.
51+
*/
52+
private void updateCurrentStrategy() {
53+
// Move to the next non-exhausted strategy if the current one is exhausted
54+
while (!isLastStrategy() && currentStrategy.isExhausted()) {
55+
currentStrategyIndex++;
56+
currentStrategy = backoffStrategies[currentStrategyIndex];
57+
}
58+
}
59+
60+
private boolean isLastStrategy() {
61+
return currentStrategyIndex + 1 >= backoffStrategies.length;
62+
}
63+
64+
@Override
65+
public void reset() {
66+
for (int i = 0; i <= currentStrategyIndex; i++) {
67+
backoffStrategies[i].reset();
68+
}
69+
70+
currentStrategyIndex = 0;
71+
currentStrategy = backoffStrategies[currentStrategyIndex];
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.backoff;
2+
3+
/**
4+
* A backoff strategy that always returns the same backoff time.
5+
* This backoff is never exhausted.
6+
*/
7+
public class ConstantTimeBackoff implements BackoffStrategy {
8+
final long millis;
9+
10+
public ConstantTimeBackoff(long millis) {
11+
this.millis = millis;
12+
}
13+
14+
@Override
15+
public long getCurrentBackoffMillis() {
16+
return millis;
17+
}
18+
19+
@Override
20+
public boolean isExhausted() {
21+
return false;
22+
}
23+
24+
@Override
25+
public void nextBackoff() {
26+
}
27+
28+
@Override
29+
public void reset() {
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.backoff;
2+
3+
/**
4+
* A backoff strategy that exponentially increases the backoff time.
5+
* This backoff is never exhausted.
6+
*/
7+
public class ExponentialTimeBackoff implements BackoffStrategy {
8+
public static final long DEFAULT_MAX_BACK_OFF = 120 * 1000;
9+
10+
private final long initialBackoff;
11+
private final long maxBackoff;
12+
private long currentBackoff;
13+
14+
/**
15+
* A backoff strategy that exponentially increases the backoff time.
16+
* This backoff will double the backoff time until the DEFAULT_MAX_BACK_OFF is reached.
17+
*
18+
* @param initialBackoffMillis the initial backoff time in milliseconds
19+
*/
20+
public ExponentialTimeBackoff(long initialBackoffMillis) {
21+
this(initialBackoffMillis, DEFAULT_MAX_BACK_OFF);
22+
}
23+
24+
/**
25+
* A backoff strategy that exponentially increases the backoff time.
26+
* This backoff will double the backoff time until the maximum backoff time is reached.
27+
* It is never exhausted but will stale at the maximum backoff time.
28+
*
29+
* @param initialBackoffMillis the initial backoff time in milliseconds
30+
* @param maxBackoffMillis the maximum backoff time in milliseconds
31+
*/
32+
public ExponentialTimeBackoff(long initialBackoffMillis, long maxBackoffMillis) {
33+
this.initialBackoff = initialBackoffMillis;
34+
this.maxBackoff = maxBackoffMillis;
35+
reset();
36+
}
37+
38+
@Override
39+
public long getCurrentBackoffMillis() {
40+
return currentBackoff;
41+
}
42+
43+
@Override
44+
public boolean isExhausted() {
45+
return false;
46+
}
47+
48+
@Override
49+
public void nextBackoff() {
50+
currentBackoff = Math.min(currentBackoff * 2, maxBackoff);
51+
}
52+
53+
@Override
54+
public void reset() {
55+
currentBackoff = Math.min(initialBackoff, maxBackoff);
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.common.backoff;
2+
3+
/**
4+
* Backoff service that supports "silent" backoff.
5+
*/
6+
public class GrpcStreamConnectorBackoffService extends BackoffService {
7+
private final BackoffStrategy silentRecoverBackoff;
8+
9+
/**
10+
* Create a new backoff service that will not backoff (0ms) on first attempt.
11+
* Subsequent attempts will backoff exponentially.
12+
*
13+
* @param initialBackoffMillis initial backoff time in milliseconds used for exponential error backoff
14+
*/
15+
public GrpcStreamConnectorBackoffService(long initialBackoffMillis) {
16+
this(BackoffStrategies.exponentialTimeBackoff(initialBackoffMillis));
17+
}
18+
19+
/**
20+
* Create a new backoff service that will not backoff (0ms) on first attempt.
21+
* Subsequent attempts will backoff using the provided backoff strategy.
22+
*
23+
* @param errorBackoff backoff strategy to use after the first attempt
24+
*/
25+
public GrpcStreamConnectorBackoffService(BackoffStrategy errorBackoff) {
26+
this(new NumberOfRetriesBackoff(1, BackoffStrategies.noBackoff()), errorBackoff);
27+
}
28+
29+
private GrpcStreamConnectorBackoffService(BackoffStrategy silentRecoverBackoff, BackoffStrategy errorBackoff) {
30+
super(new CombinedBackoff(new BackoffStrategy[]{
31+
silentRecoverBackoff,
32+
errorBackoff
33+
}));
34+
this.silentRecoverBackoff = silentRecoverBackoff;
35+
}
36+
37+
public boolean shouldRetrySilently() {
38+
return ((CombinedBackoff) getStrategy()).getCurrentStrategy() == silentRecoverBackoff;
39+
}
40+
}

0 commit comments

Comments
 (0)