Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.example.vault;

import com.skyflow.Skyflow;
import com.skyflow.config.Credentials;
import com.skyflow.config.VaultConfig;
import com.skyflow.enums.Env;
import com.skyflow.enums.LogLevel;
import com.skyflow.errors.SkyflowException;

/**
* This sample demonstrates how to configure HTTP timeout and retry behavior in the Skyflow Java SDK.
*
* <p>Configurable settings (all optional):
* <ul>
* <li>{@code timeout} – overall call timeout in <b>seconds</b> (bounds the whole
* request including retries and backoff). Default: 60.</li>
* <li>{@code maxRetries} – retry attempts after the first failure (retries on HTTP
* 408 / 429 / 5xx). Default: 3.</li>
* <li>{@code initialRetryDelayMillis} – base backoff before the first retry, in <b>milliseconds</b>.
* Default: 500.</li>
* <li>{@code maxRetryDelayMillis} – cap on the (exponentially growing) backoff, in
* <b>milliseconds</b>. Default: 2000.</li>
* </ul>
*
* <p><b>Two levels + precedence:</b> set client-wide defaults on {@code Skyflow.builder()}, and/or
* per-vault overrides on {@code VaultConfig}. The most specific value wins, resolved per field:
* <b>per-vault &rarr; client-wide &rarr; SDK default</b>.
*/
public class TimeoutAndRetryConfigExample {

public static void main(String[] args) {
try {
// Step 1: Initialize credentials with the path to your service account key file
String filePath = "<YOUR_CREDENTIALS_FILE_PATH>";
Credentials credentials = new Credentials();
credentials.setPath(filePath);

// Step 2: Configure the vault. Optionally override timeout/retry settings for THIS vault only.
VaultConfig vaultConfig = new VaultConfig();
vaultConfig.setVaultId("<YOUR_VAULT_ID>");
vaultConfig.setClusterId("<YOUR_CLUSTER_ID>");
vaultConfig.setEnv(Env.PROD);
vaultConfig.setCredentials(credentials);
// Per-vault overrides (optional). Any field left unset inherits the client-wide default below,
// and then the SDK default.
vaultConfig.setTimeout(30); // seconds – tighter overall ceiling for this vault
vaultConfig.setMaxRetries(2); // fewer retries for this vault
vaultConfig.setInitialRetryDelayMillis(500L);
vaultConfig.setMaxRetryDelayMillis(1000L);

// Step 3: Create the Skyflow client. Client-wide defaults apply to every vault
// unless that vault overrides them (as above).
Skyflow skyflowClient = Skyflow.builder()
.setLogLevel(LogLevel.ERROR)
.timeout(60) // seconds – client-wide overall call timeout
.maxRetries(3) // client-wide retry attempts
.initialRetryDelayMillis(500L) // client-wide base backoff (ms)
.maxRetryDelayMillis(2000L) // client-wide backoff cap (ms)
.addVaultConfig(vaultConfig)
.build();

// Step 4: Use the client as usual. Requests now fail fast at the configured timeout and
// retry transient 408/429/5xx responses with exponential backoff + jitter.
System.out.println("Skyflow client configured with custom timeout & retry settings: " + skyflowClient);

// Example (uncomment and fill in a real request to try it):
// DetokenizeResponse response = skyflowClient.vault().detokenize(detokenizeRequest);
// System.out.println(response);
} catch (SkyflowException e) {
// Step 5: Handle any errors that occur during the process
System.err.println("Error in Skyflow operations: " + e.getMessage());
}
}
}
43 changes: 42 additions & 1 deletion v3/src/main/java/com/skyflow/Skyflow.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ public VaultController vault() throws SkyflowException {
public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder {
private final LinkedHashMap<String, VaultConfig> vaultConfigMap;
private final LinkedHashMap<String, VaultController> vaultClientsMap;
// Client-wide HTTP config defaults (apply to all vaults unless a vault overrides). null => SDK default.
private Integer timeout;
private Integer maxRetries;
private Long initialRetryDelayMillis;
private Long maxRetryDelayMillis;

public SkyflowClientBuilder() {
this.vaultConfigMap = new LinkedHashMap<>();
Expand All @@ -78,7 +83,9 @@ public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws Skyfl
ErrorMessage.VaultIdAlreadyInConfigList.getMessage());
} else {
this.vaultConfigMap.put(vaultConfigCopy.getVaultId(), vaultConfigCopy); // add new config in map
this.vaultClientsMap.put(vaultConfigCopy.getVaultId(), new VaultController(vaultConfigCopy, this.skyflowCredentials)); // add new controller with new config
VaultController controller = new VaultController(vaultConfigCopy, this.skyflowCredentials); // new controller with new config
controller.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelayMillis, this.maxRetryDelayMillis);
this.vaultClientsMap.put(vaultConfigCopy.getVaultId(), controller);
LogUtil.printInfoLog(Utils.parameterizedString(
InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfigCopy.getVaultId()));
}
Expand All @@ -100,6 +107,40 @@ public SkyflowClientBuilder addSkyflowCredentials(Credentials credentials) throw
return this;
}

/** Client-wide overall call timeout in seconds. Applies to all vaults unless a vault overrides it. */
public SkyflowClientBuilder timeout(int timeout) {
this.timeout = timeout;
propagateHttpConfig();
return this;
}

/** Client-wide retry attempt count. Applies to all vaults unless a vault overrides it. */
public SkyflowClientBuilder maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
propagateHttpConfig();
return this;
}

/** Client-wide base retry backoff in milliseconds. Applies to all vaults unless a vault overrides it. */
public SkyflowClientBuilder initialRetryDelayMillis(long initialRetryDelayMillis) {
this.initialRetryDelayMillis = initialRetryDelayMillis;
propagateHttpConfig();
return this;
}

/** Client-wide retry backoff cap in milliseconds. Applies to all vaults unless a vault overrides it. */
public SkyflowClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) {
this.maxRetryDelayMillis = maxRetryDelayMillis;
propagateHttpConfig();
return this;
}

private void propagateHttpConfig() {
for (VaultController vault : this.vaultClientsMap.values()) {
vault.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelayMillis, this.maxRetryDelayMillis);
}
}

@Override
public SkyflowClientBuilder setLogLevel(LogLevel logLevel) {
super.setLogLevel(logLevel);
Expand Down
52 changes: 51 additions & 1 deletion v3/src/main/java/com/skyflow/VaultClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.skyflow.logs.InfoLogs;
import com.skyflow.serviceaccount.util.Token;
import com.skyflow.utils.Constants;
import com.skyflow.utils.SkyflowRetryInterceptor;
import com.skyflow.utils.Utils;
import com.skyflow.utils.logger.LogUtil;
import com.skyflow.utils.validations.Validations;
Expand All @@ -45,6 +46,16 @@ public class VaultClient {
private String apiKey;
private OkHttpClient sharedHttpClient = null;
private String currentVaultURL = null;
// Client-wide (Skyflow builder) HTTP config defaults; null => fall back to the SDK defaults below.
private Integer commonTimeout;
private Integer commonMaxRetries;
private Long commonInitialRetryDelayMillis;
private Long commonMaxRetryDelayMillis;
// SDK defaults, used when neither the vault-level nor the client-wide value is set.
private static final int DEFAULT_TIMEOUT_SECONDS = 60;
private static final int DEFAULT_MAX_RETRIES = 3;
private static final long DEFAULT_INITIAL_RETRY_DELAY_MILLIS = 500L;
private static final long DEFAULT_MAX_RETRY_DELAY_MILLIS = 2000L;

protected VaultClient(VaultConfig vaultConfig, Credentials credentials) throws SkyflowException {
super();
Expand All @@ -68,6 +79,20 @@ protected void setCommonCredentials(Credentials commonCredentials) throws Skyflo
prioritiseCredentials();
}

/**
* Client-wide HTTP timeout/retry defaults from the Skyflow builder. Nulls out the cached client
* so the next call rebuilds with the new values.
*/
protected void setCommonHttpConfig(Integer timeout, Integer maxRetries,
Long initialRetryDelayMillis, Long maxRetryDelayMillis) {
this.commonTimeout = timeout;
this.commonMaxRetries = maxRetries;
this.commonInitialRetryDelayMillis = initialRetryDelayMillis;
this.commonMaxRetryDelayMillis = maxRetryDelayMillis;
this.sharedHttpClient = null;
this.apiClient = null;
}

protected void setBearerToken() throws SkyflowException {
prioritiseCredentials();
Validations.validateCredentials(this.finalCredentials);
Expand Down Expand Up @@ -143,9 +168,18 @@ private void prioritiseCredentials() throws SkyflowException {

protected void updateExecutorInHTTP() {
if (sharedHttpClient == null) {
int timeoutSeconds = resolveInt(vaultConfig.getTimeout(), commonTimeout, DEFAULT_TIMEOUT_SECONDS);
int maxRetries = resolveInt(vaultConfig.getMaxRetries(), commonMaxRetries, DEFAULT_MAX_RETRIES);
long initialRetryDelayMillis = resolveLong(
vaultConfig.getInitialRetryDelayMillis(), commonInitialRetryDelayMillis, DEFAULT_INITIAL_RETRY_DELAY_MILLIS);
long maxRetryDelayMillis = resolveLong(
vaultConfig.getMaxRetryDelayMillis(), commonMaxRetryDelayMillis, DEFAULT_MAX_RETRY_DELAY_MILLIS);

sharedHttpClient = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(10, 1, TimeUnit.MINUTES))
.addInterceptor(chain -> {
.callTimeout(timeoutSeconds, TimeUnit.SECONDS) // overall ceiling; bounds the whole call incl. retries
.addInterceptor(new SkyflowRetryInterceptor(maxRetries, initialRetryDelayMillis, maxRetryDelayMillis)) // OUTER: retries
.addInterceptor(chain -> { // INNER: auth (reads this.token per request)
Request requestWithAuth = chain.request().newBuilder()
.header("Authorization", "Bearer " + this.token)
.build();
Expand All @@ -156,6 +190,22 @@ protected void updateExecutorInHTTP() {
}
}

/** Resolve an int setting: vault-level override, else client-wide default, else SDK default. */
private static int resolveInt(Integer vaultLevel, Integer clientLevel, int defaultValue) {
if (vaultLevel != null) {
return vaultLevel;
}
return clientLevel != null ? clientLevel : defaultValue;
}

/** Resolve a long setting: vault-level override, else client-wide default, else SDK default. */
private static long resolveLong(Long vaultLevel, Long clientLevel, long defaultValue) {
if (vaultLevel != null) {
return vaultLevel;
}
return clientLevel != null ? clientLevel : defaultValue;
}

protected V1InsertRequest getBulkInsertRequestBody(com.skyflow.vault.data.InsertRequest request, VaultConfig config) {
ArrayList<InsertRecord> records = request.getRecords();
List<V1InsertRecordData> insertRecordDataList = new ArrayList<>();
Expand Down
45 changes: 45 additions & 0 deletions v3/src/main/java/com/skyflow/config/VaultConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ public class VaultConfig implements Cloneable {
private String vaultURL;
private Env env;
private Credentials credentials;
// HTTP timeout & retry config (vault-level overrides). null => inherit client-wide default, then SDK default.
private Integer timeout; // overall call timeout, in seconds
private Integer maxRetries; // retry attempts after the first failure
private Long initialRetryDelayMillis; // base backoff before the first retry, in ms
private Long maxRetryDelayMillis; // cap on the (exponentially growing) backoff, in ms

public VaultConfig() {
this.vaultId = null;
this.clusterId = null;
this.vaultURL = null;
this.env = Env.PROD;
this.credentials = null;
this.timeout = null;
this.maxRetries = null;
this.initialRetryDelayMillis = null;
this.maxRetryDelayMillis = null;
}

public String getVaultId() {
Expand Down Expand Up @@ -57,6 +66,42 @@ public void setVaultURL(String vaultURL) {
this.vaultURL = vaultURL;
}

public Integer getTimeout() {
return timeout;
}

/** Overall call timeout in seconds for this vault. Overrides the client-wide default. */
public void setTimeout(Integer timeout) {
this.timeout = timeout;
}

public Integer getMaxRetries() {
return maxRetries;
}

/** Number of retry attempts after the first failure for this vault. Overrides the client-wide default. */
public void setMaxRetries(Integer maxRetries) {
this.maxRetries = maxRetries;
}

public Long getInitialRetryDelayMillis() {
return initialRetryDelayMillis;
}

/** Base retry backoff in milliseconds for this vault. Overrides the client-wide default. */
public void setInitialRetryDelayMillis(Long initialRetryDelayMillis) {
this.initialRetryDelayMillis = initialRetryDelayMillis;
}

public Long getMaxRetryDelayMillis() {
return maxRetryDelayMillis;
}

/** Cap on retry backoff in milliseconds for this vault. Overrides the client-wide default. */
public void setMaxRetryDelayMillis(Long maxRetryDelayMillis) {
this.maxRetryDelayMillis = maxRetryDelayMillis;
}

@Override
public Object clone() throws CloneNotSupportedException {
VaultConfig cloned = (VaultConfig) super.clone();
Expand Down
85 changes: 85 additions & 0 deletions v3/src/main/java/com/skyflow/utils/SkyflowRetryInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.skyflow.utils;

import java.io.IOException;
import java.util.Random;
import okhttp3.Interceptor;
import okhttp3.Response;

/**
* Retry interceptor for the v3 SDK's shared {@link okhttp3.OkHttpClient}.
*
* <p>Replicates Fern's retry algorithm (exponential backoff + proportional jitter) so behavior
* stays familiar, while exposing {@code maxRetries}, {@code initialRetryDelayMillis} and
* {@code maxRetryDelayMillis} as configurable knobs. The jitter factor is fixed at 0.2 (±10%) and
* intentionally not exposed. Retries fire on HTTP 408 / 429 / 5xx responses only (same triggers as
* Fern); it does not retry connection failures / {@link IOException}.
*
* <p><b>Why this is hand-written instead of reusing Fern's generated {@code RetryInterceptor}:</b>
* the SDK injects its own {@code OkHttpClient} (for the auth header + connection pool), so Fern's
* generated interceptor is never wired by {@code ClientOptions.build()} (it is only added on the
* no-custom-client branch); and the currently vendored Fern interceptor hardcodes its backoff
* (only {@code maxRetries} is configurable), so it cannot honor the delay knobs above. See
* {@code docs/superpowers/specs/2026-07-21-CUST-4311-v3-configurable-timeouts-retries-design.md}.
*
* <p><b>TODO(CUST-4311):</b> replace this class with Fern's generated {@code RetryInterceptor}
* once the SDK is regenerated with a Fern version whose interceptor (a) exposes
* {@code initialRetryDelayMillis} / {@code maxRetryDelayMillis} and (b) is constructable
* standalone so it can be attached to our custom client. Target Fern/generator version: v_____
* (fill in when known).
*/
public final class SkyflowRetryInterceptor implements Interceptor {

/** Fixed jitter factor (±10%). Intentionally not exposed as a public knob (matches Fern's default). */
private static final double JITTER_FACTOR = 0.2;

private final int maxRetries;
private final long initialRetryDelayMillis;
private final long maxRetryDelayMillis;
private final Random random = new Random();

public SkyflowRetryInterceptor(int maxRetries, long initialRetryDelayMillis, long maxRetryDelayMillis) {
this.maxRetries = maxRetries;
this.initialRetryDelayMillis = initialRetryDelayMillis;
this.maxRetryDelayMillis = maxRetryDelayMillis;
}

@Override
public Response intercept(Chain chain) throws IOException {
Response response = chain.proceed(chain.request());

int attempt = 0;
while (shouldRetry(response.code()) && attempt < maxRetries) {
attempt++;
sleep(backoffMillis(attempt));
response.close(); // release the failed response/connection before retrying
response = chain.proceed(chain.request());
}
return response;
}

/**
* Exponential backoff, capped, then proportional jitter (same shape as Fern):
* {@code base(n) = min(initialRetryDelayMillis * 2^(n-1), maxRetryDelayMillis)},
* {@code delay = base * (1 + (rand - 0.5) * JITTER_FACTOR)} — i.e. {@code base * [0.9, 1.1]}.
* Jitter is applied after the cap, so the returned delay can exceed {@code maxRetryDelayMillis}
* by up to the jitter factor.
*/
private long backoffMillis(int attempt) {
double base = Math.min(initialRetryDelayMillis * Math.pow(2, attempt - 1), maxRetryDelayMillis);
double jitter = 1.0 + ((random.nextDouble() - 0.5) * JITTER_FACTOR);
return (long) (base * jitter);
}

private static boolean shouldRetry(int statusCode) {
return statusCode == 408 || statusCode == 429 || statusCode >= 500;
}

private void sleep(long millis) throws IOException {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted during retry backoff", e);
}
}
}
Loading
Loading