Skip to content

Commit 9bd1b4b

Browse files
SK-3026 retry logic update
1 parent 57e14ce commit 9bd1b4b

6 files changed

Lines changed: 427 additions & 36 deletions

File tree

flowvault/src/main/java/com/skyflow/Skyflow.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder<
5555
private Integer readTimeout;
5656
private Integer writeTimeout;
5757
private Integer maxRetries;
58+
private Long initialRetryDelayMillis;
59+
private Long maxRetryDelayMillis;
5860

5961
@Override
6062
protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowException {
@@ -65,7 +67,8 @@ protected void validateVaultConfig(VaultConfig vaultConfig) throws SkyflowExcept
6567
protected void onVaultConfigAdded(VaultConfig vaultConfig) throws SkyflowException {
6668
VaultController controller = new VaultController(vaultConfig, this.skyflowCredentials);
6769
controller.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout,
68-
this.writeTimeout, this.maxRetries);
70+
this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis,
71+
this.maxRetryDelayMillis);
6972
this.vaultClientsMap.put(vaultConfig.getVaultId(), controller);
7073
LogUtil.printInfoLog(Utils.parameterizedString(InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfig.getVaultId()));
7174
}
@@ -82,7 +85,8 @@ protected void onVaultConfigUpdated(VaultConfig updatedConfig) throws SkyflowExc
8285
updated.setVaultConfig(updatedConfig);
8386
}
8487
updated.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout,
85-
this.writeTimeout, this.maxRetries);
88+
this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis,
89+
this.maxRetryDelayMillis);
8690
}
8791

8892
@Override
@@ -142,6 +146,12 @@ private void carryVaultOverrides(VaultConfig incoming) throws SkyflowException {
142146
if (incoming.getMaxRetries() != null) {
143147
merged.setMaxRetries(incoming.getMaxRetries());
144148
}
149+
if (incoming.getInitialRetryDelayMillis() != null) {
150+
merged.setInitialRetryDelayMillis(incoming.getInitialRetryDelayMillis());
151+
}
152+
if (incoming.getMaxRetryDelayMillis() != null) {
153+
merged.setMaxRetryDelayMillis(incoming.getMaxRetryDelayMillis());
154+
}
145155
// The HTTP settings above are resolved lazily on the next request, but the URL is
146156
// resolved once in the VaultClient constructor — which already ran with the old value.
147157
if (incoming.getVaultUrl() != null) {
@@ -232,11 +242,38 @@ public SkyflowClientBuilder maxRetries(int maxRetries) {
232242
return this;
233243
}
234244

245+
/**
246+
* Backoff before the first retry, in milliseconds. Default 500. Only applies when
247+
* {@code maxRetries} is greater than zero.
248+
* <p>
249+
* <b>Precedence:</b> a vault that sets {@link VaultConfig#setInitialRetryDelayMillis(Long)}
250+
* wins; this value applies only to vaults that leave it unset.
251+
*/
252+
public SkyflowClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) {
253+
this.initialRetryDelayMillis = initialRetryDelayMillis;
254+
propagateHttpConfig();
255+
return this;
256+
}
257+
258+
/**
259+
* Ceiling the exponential backoff grows to, in milliseconds. Default 2000. Only applies
260+
* when {@code maxRetries} is greater than zero.
261+
* <p>
262+
* <b>Precedence:</b> a vault that sets {@link VaultConfig#setMaxRetryDelayMillis(Long)}
263+
* wins; this value applies only to vaults that leave it unset.
264+
*/
265+
public SkyflowClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) {
266+
this.maxRetryDelayMillis = maxRetryDelayMillis;
267+
propagateHttpConfig();
268+
return this;
269+
}
270+
235271
/** Push the current client-wide HTTP settings onto every vault controller built so far. */
236272
private void propagateHttpConfig() {
237273
for (VaultController vault : this.vaultClientsMap.values()) {
238274
vault.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout,
239-
this.writeTimeout, this.maxRetries);
275+
this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis,
276+
this.maxRetryDelayMillis);
240277
}
241278
}
242279

flowvault/src/main/java/com/skyflow/VaultClient.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
import com.skyflow.errors.SkyflowException;
66
import com.skyflow.generated.rest.ApiClient;
77
import com.skyflow.generated.rest.ApiClientBuilder;
8-
import com.skyflow.generated.rest.core.RetryInterceptor;
98
import com.skyflow.generated.rest.resources.flowservice.FlowserviceClient;
109
import com.skyflow.generated.rest.resources.records.RecordsClient;
10+
import com.skyflow.utils.SkyflowRetryInterceptor;
1111
import com.skyflow.utils.Utils;
1212

1313
import java.util.concurrent.TimeUnit;
@@ -25,10 +25,14 @@ public class VaultClient extends BaseVaultClient<VaultConfig> {
2525
private Integer commonReadTimeout;
2626
private Integer commonWriteTimeout;
2727
private Integer commonMaxRetries;
28+
private Long commonInitialRetryDelayMillis;
29+
private Long commonMaxRetryDelayMillis;
2830
// SDK defaults, used when neither the vault-level nor the client-wide value is set.
2931
private static final int DEFAULT_TIMEOUT_SECONDS = 60;
3032
// Retries OFF by default (opt-in) so non-idempotent bulk writes aren't replayed automatically.
3133
private static final int DEFAULT_MAX_RETRIES = 0;
34+
private static final long DEFAULT_INITIAL_RETRY_DELAY_MILLIS = 500L;
35+
private static final long DEFAULT_MAX_RETRY_DELAY_MILLIS = 2000L;
3236

3337
protected VaultClient(VaultConfig vaultConfig, Credentials credentials) throws SkyflowException {
3438
super(vaultConfig, credentials);
@@ -42,12 +46,15 @@ protected VaultClient(VaultConfig vaultConfig, Credentials credentials) throws S
4246
* client and ApiClient so the next call rebuilds them with the new values.
4347
*/
4448
protected void setCommonHttpConfig(Integer timeout, Integer connectTimeout, Integer readTimeout,
45-
Integer writeTimeout, Integer maxRetries) {
49+
Integer writeTimeout, Integer maxRetries,
50+
Long initialRetryDelayMillis, Long maxRetryDelayMillis) {
4651
this.commonTimeout = timeout;
4752
this.commonConnectTimeout = connectTimeout;
4853
this.commonReadTimeout = readTimeout;
4954
this.commonWriteTimeout = writeTimeout;
5055
this.commonMaxRetries = maxRetries;
56+
this.commonInitialRetryDelayMillis = initialRetryDelayMillis;
57+
this.commonMaxRetryDelayMillis = maxRetryDelayMillis;
5158
this.sharedHttpClient = null;
5259
this.apiClient = null;
5360
}
@@ -60,6 +67,14 @@ private static int resolveInt(Integer vaultLevel, Integer clientLevel, int defau
6067
return clientLevel != null ? clientLevel : defaultValue;
6168
}
6269

70+
/** Resolve a long setting: vault-level override, else client-wide default, else the SDK default. */
71+
private static long resolveLong(Long vaultLevel, Long clientLevel, long defaultValue) {
72+
if (vaultLevel != null) {
73+
return vaultLevel;
74+
}
75+
return clientLevel != null ? clientLevel : defaultValue;
76+
}
77+
6378
/**
6479
* Resolve an optional setting: vault-level override, else client-wide default, else null.
6580
* Null means "not configured" — the caller leaves the underlying HTTP client default in place.
@@ -134,6 +149,10 @@ protected void updateExecutorInHTTP() {
134149
if (sharedHttpClient == null) {
135150
int timeoutSeconds = resolveInt(vaultConfig.getTimeout(), commonTimeout, DEFAULT_TIMEOUT_SECONDS);
136151
int maxRetries = resolveInt(vaultConfig.getMaxRetries(), commonMaxRetries, DEFAULT_MAX_RETRIES);
152+
long initialRetryDelayMillis = resolveLong(vaultConfig.getInitialRetryDelayMillis(),
153+
commonInitialRetryDelayMillis, DEFAULT_INITIAL_RETRY_DELAY_MILLIS);
154+
long maxRetryDelayMillis = resolveLong(vaultConfig.getMaxRetryDelayMillis(),
155+
commonMaxRetryDelayMillis, DEFAULT_MAX_RETRY_DELAY_MILLIS);
137156
// Per-attempt timeouts: null => leave OkHttp's built-in default (backward compatible).
138157
Integer connectTimeout = resolveNullableInt(vaultConfig.getConnectTimeout(), commonConnectTimeout);
139158
Integer readTimeout = resolveNullableInt(vaultConfig.getReadTimeout(), commonReadTimeout);
@@ -145,7 +164,7 @@ protected void updateExecutorInHTTP() {
145164
.callTimeout(timeoutSeconds, TimeUnit.SECONDS)
146165
// OUTER: retries. Must wrap the auth interceptor so each attempt re-reads the
147166
// (possibly refreshed) bearer token rather than replaying a stale one.
148-
.addInterceptor(new RetryInterceptor(maxRetries))
167+
.addInterceptor(new SkyflowRetryInterceptor(maxRetries, initialRetryDelayMillis, maxRetryDelayMillis))
149168
.addInterceptor(chain -> { // INNER: auth
150169
Request requestWithAuth = chain.request().newBuilder()
151170
.header("Authorization", "Bearer " + this.token)

flowvault/src/main/java/com/skyflow/config/VaultConfig.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ public class VaultConfig extends BaseVaultConfig {
2121
private Integer readTimeout; // per-attempt response-read timeout, in seconds
2222
private Integer writeTimeout; // per-attempt request-write timeout, in seconds
2323
private Integer maxRetries; // retry attempts after the first failure
24+
private Long initialRetryDelayMillis; // backoff before the first retry, in milliseconds
25+
private Long maxRetryDelayMillis; // ceiling the exponential backoff grows to, in milliseconds
2426

2527
public VaultConfig() {
2628
super();
@@ -30,6 +32,8 @@ public VaultConfig() {
3032
this.readTimeout = null;
3133
this.writeTimeout = null;
3234
this.maxRetries = null;
35+
this.initialRetryDelayMillis = null;
36+
this.maxRetryDelayMillis = null;
3337
}
3438

3539
public String getVaultUrl() {
@@ -114,4 +118,35 @@ public void setMaxRetries(Integer maxRetries) {
114118
this.maxRetries = maxRetries;
115119
}
116120

121+
122+
public Long getInitialRetryDelayMillis() {
123+
return initialRetryDelayMillis;
124+
}
125+
126+
/**
127+
* Backoff before the first retry, in milliseconds, for this vault.
128+
* <p>
129+
* Takes precedence over the client-wide {@code Skyflow.builder().initialRetryDelayMillis(...)}.
130+
* Leave unset (null) to inherit that value, or the SDK default of 500 ms if it is also unset.
131+
* Only applies when {@code maxRetries} is greater than zero.
132+
*/
133+
public void setInitialRetryDelayMillis(Long initialRetryDelayMillis) {
134+
this.initialRetryDelayMillis = initialRetryDelayMillis;
135+
}
136+
137+
public Long getMaxRetryDelayMillis() {
138+
return maxRetryDelayMillis;
139+
}
140+
141+
/**
142+
* Ceiling the exponential backoff grows to, in milliseconds, for this vault.
143+
* <p>
144+
* Takes precedence over the client-wide {@code Skyflow.builder().maxRetryDelayMillis(...)}.
145+
* Leave unset (null) to inherit that value, or the SDK default of 2000 ms if it is also unset.
146+
* Only applies when {@code maxRetries} is greater than zero.
147+
*/
148+
public void setMaxRetryDelayMillis(Long maxRetryDelayMillis) {
149+
this.maxRetryDelayMillis = maxRetryDelayMillis;
150+
}
151+
117152
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package com.skyflow.utils;
2+
3+
import okhttp3.Interceptor;
4+
import okhttp3.Response;
5+
6+
import java.io.IOException;
7+
import java.util.Random;
8+
9+
/**
10+
* Retries failed requests with exponential backoff and jitter.
11+
* <p>
12+
* This exists as hand-written code rather than using the generated
13+
* {@code com.skyflow.generated.rest.core.RetryInterceptor} because that one only accepts a retry
14+
* count — it has no way to configure the backoff delays that {@code VaultConfig} exposes. It also
15+
* keeps its backoff counter on the interceptor instance, so a single shared instance exhausts its
16+
* retry budget once for the whole client rather than once per request; this implementation keeps
17+
* that state per call.
18+
* <p>
19+
* Retries the same statuses the generated interceptor does: 408, 429, and any 5xx.
20+
*/
21+
public final class SkyflowRetryInterceptor implements Interceptor {
22+
23+
/** Fraction of the computed delay applied as random jitter, to avoid synchronised retries. */
24+
private static final double JITTER_FACTOR = 0.2;
25+
26+
private final int maxRetries;
27+
private final long initialRetryDelayMillis;
28+
private final long maxRetryDelayMillis;
29+
private final Random random = new Random();
30+
31+
public SkyflowRetryInterceptor(int maxRetries, long initialRetryDelayMillis, long maxRetryDelayMillis) {
32+
if (maxRetries < 0) {
33+
throw new IllegalArgumentException("maxRetries must be non-negative");
34+
}
35+
if (initialRetryDelayMillis < 0) {
36+
throw new IllegalArgumentException("initialRetryDelayMillis must be non-negative");
37+
}
38+
if (maxRetryDelayMillis < 0) {
39+
throw new IllegalArgumentException("maxRetryDelayMillis must be non-negative");
40+
}
41+
this.maxRetries = maxRetries;
42+
this.initialRetryDelayMillis = initialRetryDelayMillis;
43+
this.maxRetryDelayMillis = maxRetryDelayMillis;
44+
}
45+
46+
@Override
47+
public Response intercept(Chain chain) throws IOException {
48+
Response response = chain.proceed(chain.request());
49+
// Retry budget is scoped to this call, not to the interceptor instance.
50+
for (int attempt = 1; attempt <= maxRetries && shouldRetry(response.code()); attempt++) {
51+
sleep(backoffMillis(attempt));
52+
response.close();
53+
response = chain.proceed(chain.request());
54+
}
55+
return response;
56+
}
57+
58+
/** Exponential growth from the initial delay, capped at the maximum, then jittered. */
59+
long backoffMillis(int attempt) {
60+
long delay = initialRetryDelayMillis;
61+
for (int i = 1; i < attempt && delay < maxRetryDelayMillis; i++) {
62+
delay = delay > maxRetryDelayMillis / 2 ? maxRetryDelayMillis : delay * 2;
63+
}
64+
delay = Math.min(delay, maxRetryDelayMillis);
65+
long jitter = (long) (delay * JITTER_FACTOR);
66+
if (jitter <= 0) {
67+
return delay;
68+
}
69+
// delay +/- up to JITTER_FACTOR, never negative.
70+
return Math.max(0, delay - jitter + random.nextInt((int) Math.min(2 * jitter + 1, Integer.MAX_VALUE)));
71+
}
72+
73+
static boolean shouldRetry(int statusCode) {
74+
return statusCode == 408 || statusCode == 429 || statusCode >= 500;
75+
}
76+
77+
private static void sleep(long millis) throws IOException {
78+
try {
79+
Thread.sleep(millis);
80+
} catch (InterruptedException e) {
81+
Thread.currentThread().interrupt();
82+
throw new IOException("Interrupted while waiting to retry request", e);
83+
}
84+
}
85+
86+
public int getMaxRetries() {
87+
return maxRetries;
88+
}
89+
90+
public long getInitialRetryDelayMillis() {
91+
return initialRetryDelayMillis;
92+
}
93+
94+
public long getMaxRetryDelayMillis() {
95+
return maxRetryDelayMillis;
96+
}
97+
}

0 commit comments

Comments
 (0)