diff --git a/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java index d537df30..eb4bf1e9 100644 --- a/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java +++ b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java @@ -16,9 +16,9 @@ * request including retries and backoff). Default: 60. *
  • {@code maxRetries} – retry attempts after the first failure (retries on HTTP * 408 / 429 / 5xx). Default: 0 — retries are OFF unless you set this (avoids auto-retrying non-idempotent writes).
  • - *
  • {@code initialRetryDelay} – base backoff before the first retry, in milliseconds. + *
  • {@code initialRetryDelayMillis} – base backoff before the first retry, in milliseconds. * Default: 500.
  • - *
  • {@code maxRetryDelay} – cap on the (exponentially growing) backoff, in + *
  • {@code maxRetryDelayMillis} – cap on the (exponentially growing) backoff, in * milliseconds. Default: 2000.
  • * * @@ -45,8 +45,8 @@ public static void main(String[] args) { // and then the SDK default. vaultConfig.setTimeout(30); // seconds – tighter overall ceiling for this vault vaultConfig.setMaxRetries(2); // fewer retries for this vault - vaultConfig.setInitialRetryDelay(500L); - vaultConfig.setMaxRetryDelay(1000L); + 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). @@ -54,8 +54,8 @@ public static void main(String[] args) { .setLogLevel(LogLevel.ERROR) .timeout(60) // seconds – client-wide overall call timeout .maxRetries(3) // client-wide retry attempts - .initialRetryDelay(500L) // client-wide base backoff (ms) - .maxRetryDelay(2000L) // client-wide backoff cap (ms) + .initialRetryDelayMillis(500L) // client-wide base backoff (ms) + .maxRetryDelayMillis(2000L) // client-wide backoff cap (ms) .addVaultConfig(vaultConfig) .build(); diff --git a/v3/src/main/java/com/skyflow/Skyflow.java b/v3/src/main/java/com/skyflow/Skyflow.java index 81d3eb5f..df530215 100644 --- a/v3/src/main/java/com/skyflow/Skyflow.java +++ b/v3/src/main/java/com/skyflow/Skyflow.java @@ -58,8 +58,8 @@ public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder // Client-wide HTTP config defaults (apply to all vaults unless a vault overrides). null => SDK default. private Integer timeout; private Integer maxRetries; - private Long initialRetryDelay; - private Long maxRetryDelay; + private Long initialRetryDelayMillis; + private Long maxRetryDelayMillis; public SkyflowClientBuilder() { this.vaultConfigMap = new LinkedHashMap<>(); @@ -84,7 +84,7 @@ public SkyflowClientBuilder addVaultConfig(VaultConfig vaultConfig) throws Skyfl } else { this.vaultConfigMap.put(vaultConfigCopy.getVaultId(), vaultConfigCopy); // add new config in map VaultController controller = new VaultController(vaultConfigCopy, this.skyflowCredentials); // new controller with new config - controller.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelay, this.maxRetryDelay); + 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())); @@ -122,22 +122,22 @@ public SkyflowClientBuilder maxRetries(int maxRetries) { } /** Client-wide base retry backoff in milliseconds. Applies to all vaults unless a vault overrides it. */ - public SkyflowClientBuilder initialRetryDelay(long initialRetryDelay) { - this.initialRetryDelay = initialRetryDelay; + 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 maxRetryDelay(long maxRetryDelay) { - this.maxRetryDelay = maxRetryDelay; + 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.initialRetryDelay, this.maxRetryDelay); + vault.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelayMillis, this.maxRetryDelayMillis); } } diff --git a/v3/src/main/java/com/skyflow/VaultClient.java b/v3/src/main/java/com/skyflow/VaultClient.java index 2540bb70..b06f2bf6 100644 --- a/v3/src/main/java/com/skyflow/VaultClient.java +++ b/v3/src/main/java/com/skyflow/VaultClient.java @@ -50,8 +50,8 @@ public class VaultClient { // Client-wide (Skyflow builder) HTTP config defaults; null => fall back to the SDK defaults below. private Integer commonTimeout; private Integer commonMaxRetries; - private Long commonInitialRetryDelay; - private Long commonMaxRetryDelay; + 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 = 0; // retries OFF by default (opt-in) so non-idempotent writes aren't auto-retried @@ -85,11 +85,11 @@ protected void setCommonCredentials(Credentials commonCredentials) throws Skyflo * so the next call rebuilds with the new values. */ protected void setCommonHttpConfig(Integer timeout, Integer maxRetries, - Long initialRetryDelay, Long maxRetryDelay) { + Long initialRetryDelayMillis, Long maxRetryDelayMillis) { this.commonTimeout = timeout; this.commonMaxRetries = maxRetries; - this.commonInitialRetryDelay = initialRetryDelay; - this.commonMaxRetryDelay = maxRetryDelay; + this.commonInitialRetryDelayMillis = initialRetryDelayMillis; + this.commonMaxRetryDelayMillis = maxRetryDelayMillis; this.sharedHttpClient = null; this.apiClient = null; } @@ -171,16 +171,16 @@ 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 initialRetryDelay = resolveLong( - vaultConfig.getInitialRetryDelay(), commonInitialRetryDelay, DEFAULT_INITIAL_RETRY_DELAY_MILLIS); - long maxRetryDelay = resolveLong( - vaultConfig.getMaxRetryDelay(), commonMaxRetryDelay, DEFAULT_MAX_RETRY_DELAY_MILLIS); + 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)) .callTimeout(timeoutSeconds, TimeUnit.SECONDS) // overall ceiling; bounds the whole call incl. retries .addInterceptor(new RetryInterceptor( // OUTER: retries (Fern generated; jitter default 0.2) - maxRetries, Optional.of(initialRetryDelay), Optional.of(maxRetryDelay), Optional.empty())) + maxRetries, Optional.of(initialRetryDelayMillis), Optional.of(maxRetryDelayMillis), Optional.empty())) .addInterceptor(chain -> { // INNER: auth (reads this.token per request) Request requestWithAuth = chain.request().newBuilder() .header("Authorization", "Bearer " + this.token) diff --git a/v3/src/main/java/com/skyflow/config/VaultConfig.java b/v3/src/main/java/com/skyflow/config/VaultConfig.java index 69bec4d0..29c7f02c 100644 --- a/v3/src/main/java/com/skyflow/config/VaultConfig.java +++ b/v3/src/main/java/com/skyflow/config/VaultConfig.java @@ -11,8 +11,8 @@ public class VaultConfig implements Cloneable { // 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 initialRetryDelay; // base backoff before the first retry, in ms - private Long maxRetryDelay; // cap on the (exponentially growing) backoff, in ms + 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; @@ -22,8 +22,8 @@ public VaultConfig() { this.credentials = null; this.timeout = null; this.maxRetries = null; - this.initialRetryDelay = null; - this.maxRetryDelay = null; + this.initialRetryDelayMillis = null; + this.maxRetryDelayMillis = null; } public String getVaultId() { @@ -84,22 +84,22 @@ public void setMaxRetries(Integer maxRetries) { this.maxRetries = maxRetries; } - public Long getInitialRetryDelay() { - return initialRetryDelay; + public Long getInitialRetryDelayMillis() { + return initialRetryDelayMillis; } /** Base retry backoff in milliseconds for this vault. Overrides the client-wide default. */ - public void setInitialRetryDelay(Long initialRetryDelay) { - this.initialRetryDelay = initialRetryDelay; + public void setInitialRetryDelayMillis(Long initialRetryDelayMillis) { + this.initialRetryDelayMillis = initialRetryDelayMillis; } - public Long getMaxRetryDelay() { - return maxRetryDelay; + public Long getMaxRetryDelayMillis() { + return maxRetryDelayMillis; } /** Cap on retry backoff in milliseconds for this vault. Overrides the client-wide default. */ - public void setMaxRetryDelay(Long maxRetryDelay) { - this.maxRetryDelay = maxRetryDelay; + public void setMaxRetryDelayMillis(Long maxRetryDelayMillis) { + this.maxRetryDelayMillis = maxRetryDelayMillis; } @Override diff --git a/v3/src/main/java/com/skyflow/utils/Utils.java b/v3/src/main/java/com/skyflow/utils/Utils.java index cdd0b03b..0eb6d656 100644 --- a/v3/src/main/java/com/skyflow/utils/Utils.java +++ b/v3/src/main/java/com/skyflow/utils/Utils.java @@ -24,6 +24,7 @@ import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvException; +import java.io.InterruptedIOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; @@ -106,6 +107,26 @@ private static String extractRequestId(Map> headers) { return (ids == null || ids.isEmpty()) ? null : ids.get(0); } + private static final int HTTP_REQUEST_TIMEOUT = 408; + private static final String REQUEST_TIMED_OUT_MESSAGE = "Request timed out."; + + /** + * Builds the {@link ErrorRecord} for a network-layer failure (a batch future that + * completed exceptionally with a non-API exception). A client-side timeout — OkHttp's + * call timeout surfaces as {@link InterruptedIOException} ({@link java.net.SocketTimeoutException} + * is a subclass) anywhere in the cause chain — is reported as HTTP 408 with a clean + * message, so callers can tell a timeout apart from a server 500. Any other network + * error keeps the existing 500 behaviour. + */ + private static ErrorRecord networkErrorRecord(Throwable ex, int index) { + for (Throwable t = ex; t != null; t = t.getCause()) { + if (t instanceof InterruptedIOException) { + return new ErrorRecord(index, REQUEST_TIMED_OUT_MESSAGE, HTTP_REQUEST_TIMEOUT); + } + } + return new ErrorRecord(index, ex.getMessage(), 500); + } + public static List handleBatchException( Throwable ex, List batch, int batchNumber, int batchSize ) { @@ -153,8 +174,7 @@ public static List handleBatchException( } else { int indexNumber = batchNumber > 0 ? batchNumber * batchSize : 0; for (int j = 0; j < batch.size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } @@ -210,8 +230,7 @@ public static List handleDetokenizeBatchException( } else { int indexNumber = batchNumber * batchSize; for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } @@ -398,8 +417,7 @@ public static List handleDeleteTokensBatchException( } else { int indexNumber = batchNumber * batchSize; for (int j = 0; j < batch.getTokens().get().size(); j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } @@ -508,8 +526,7 @@ public static List handleTokenizeBatchException( int indexNumber = batchNumber * batchSize; int batchDataSize = batch.getData().isPresent() ? batch.getData().get().size() : 0; for (int j = 0; j < batchDataSize; j++) { - ErrorRecord err = new ErrorRecord(indexNumber, ex.getMessage(), 500); - errorRecords.add(err); + errorRecords.add(networkErrorRecord(ex, indexNumber)); indexNumber++; } } diff --git a/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java index 145bc8d1..1d738266 100644 --- a/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java +++ b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java @@ -33,30 +33,30 @@ public void clientWideConfigSetBeforeAddVaultReachesController() throws Exceptio Skyflow client = Skyflow.builder() .timeout(30) .maxRetries(2) - .initialRetryDelay(300L) - .maxRetryDelay(1500L) + .initialRetryDelayMillis(300L) + .maxRetryDelayMillis(1500L) .addVaultConfig(vaultConfig()) .build(); VaultController controller = client.vault(); Assert.assertEquals(Integer.valueOf(30), commonField(controller, "commonTimeout")); Assert.assertEquals(Integer.valueOf(2), commonField(controller, "commonMaxRetries")); - Assert.assertEquals(Long.valueOf(300L), commonField(controller, "commonInitialRetryDelay")); - Assert.assertEquals(Long.valueOf(1500L), commonField(controller, "commonMaxRetryDelay")); + Assert.assertEquals(Long.valueOf(300L), commonField(controller, "commonInitialRetryDelayMillis")); + Assert.assertEquals(Long.valueOf(1500L), commonField(controller, "commonMaxRetryDelayMillis")); } @Test public void clientWideConfigSetAfterAddVaultPropagatesToExistingController() throws Exception { Skyflow.SkyflowClientBuilder builder = Skyflow.builder().addVaultConfig(vaultConfig()); // Set config AFTER the controller already exists -> exercises propagateHttpConfig's loop. - builder.timeout(45).maxRetries(4).initialRetryDelay(700L).maxRetryDelay(3000L); + builder.timeout(45).maxRetries(4).initialRetryDelayMillis(700L).maxRetryDelayMillis(3000L); Skyflow client = builder.build(); VaultController controller = client.vault(); Assert.assertEquals(Integer.valueOf(45), commonField(controller, "commonTimeout")); Assert.assertEquals(Integer.valueOf(4), commonField(controller, "commonMaxRetries")); - Assert.assertEquals(Long.valueOf(700L), commonField(controller, "commonInitialRetryDelay")); - Assert.assertEquals(Long.valueOf(3000L), commonField(controller, "commonMaxRetryDelay")); + Assert.assertEquals(Long.valueOf(700L), commonField(controller, "commonInitialRetryDelayMillis")); + Assert.assertEquals(Long.valueOf(3000L), commonField(controller, "commonMaxRetryDelayMillis")); } @Test @@ -66,7 +66,7 @@ public void noClientWideConfigLeavesCommonFieldsNull() throws Exception { VaultController controller = client.vault(); Assert.assertNull(commonField(controller, "commonTimeout")); Assert.assertNull(commonField(controller, "commonMaxRetries")); - Assert.assertNull(commonField(controller, "commonInitialRetryDelay")); - Assert.assertNull(commonField(controller, "commonMaxRetryDelay")); + Assert.assertNull(commonField(controller, "commonInitialRetryDelayMillis")); + Assert.assertNull(commonField(controller, "commonMaxRetryDelayMillis")); } } diff --git a/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java b/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java index b8c1933e..f206a437 100644 --- a/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java +++ b/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java @@ -93,18 +93,18 @@ public void vaultConfigStoresHttpFieldsAndDefaultsToNull() throws SkyflowExcepti VaultConfig cfg = new VaultConfig(); Assert.assertNull(cfg.getTimeout()); Assert.assertNull(cfg.getMaxRetries()); - Assert.assertNull(cfg.getInitialRetryDelay()); - Assert.assertNull(cfg.getMaxRetryDelay()); + Assert.assertNull(cfg.getInitialRetryDelayMillis()); + Assert.assertNull(cfg.getMaxRetryDelayMillis()); cfg.setTimeout(30); cfg.setMaxRetries(2); - cfg.setInitialRetryDelay(250L); - cfg.setMaxRetryDelay(1500L); + cfg.setInitialRetryDelayMillis(250L); + cfg.setMaxRetryDelayMillis(1500L); Assert.assertEquals(Integer.valueOf(30), cfg.getTimeout()); Assert.assertEquals(Integer.valueOf(2), cfg.getMaxRetries()); - Assert.assertEquals(Long.valueOf(250L), cfg.getInitialRetryDelay()); - Assert.assertEquals(Long.valueOf(1500L), cfg.getMaxRetryDelay()); + Assert.assertEquals(Long.valueOf(250L), cfg.getInitialRetryDelayMillis()); + Assert.assertEquals(Long.valueOf(1500L), cfg.getMaxRetryDelayMillis()); } private RetryInterceptor retryInterceptor(VaultClient client) throws Exception { @@ -145,8 +145,8 @@ public void defaultRetryConfigAppliedToInterceptor() throws Exception { public void vaultLevelRetryConfigOverridesDefault() throws Exception { VaultConfig cfg = apiKeyConfig(); cfg.setMaxRetries(1); - cfg.setInitialRetryDelay(250L); - cfg.setMaxRetryDelay(1200L); + cfg.setInitialRetryDelayMillis(250L); + cfg.setMaxRetryDelayMillis(1200L); VaultClient client = new VaultClient(cfg, cfg.getCredentials()); client.setBearerToken(); diff --git a/v3/src/test/java/com/skyflow/utils/UtilsTests.java b/v3/src/test/java/com/skyflow/utils/UtilsTests.java index be1456b1..da4eed19 100644 --- a/v3/src/test/java/com/skyflow/utils/UtilsTests.java +++ b/v3/src/test/java/com/skyflow/utils/UtilsTests.java @@ -2211,4 +2211,72 @@ public void testFormatTokenizeResponseAbsentBatchData_returnsEmptySuccessAndErro Assert.assertTrue(result.getSuccess().isEmpty()); Assert.assertTrue(result.getErrors().isEmpty()); } + + // ── SK-3002: client-side timeout is classified as 408, not a generic 500 ── + + private static Throwable timeoutFailure() { + // Mirrors the real chain: OkHttp call-timeout (SocketTimeoutException, a + // subclass of InterruptedIOException) -> wrapped by the generated client in + // ApiClientException -> wrapped by CompletableFuture in CompletionException. + return new java.util.concurrent.CompletionException( + new com.skyflow.generated.rest.core.ApiClientException( + "Network error executing HTTP request", + new java.net.SocketTimeoutException("timeout"))); + } + + @Test + public void handleBatchException_timeout_yields408AndCleanMessage() { + List batch = Arrays.asList( + V1InsertRecordData.builder().build(), + V1InsertRecordData.builder().build()); + + List errors = Utils.handleBatchException(timeoutFailure(), batch, 0, 1); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } + + @Test + public void handleDetokenizeBatchException_timeout_yields408AndCleanMessage() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDetokenizeRequest.builder() + .tokens(Arrays.asList("t1", "t2")).vaultId("v1").build(); + + List errors = Utils.handleDetokenizeBatchException(timeoutFailure(), batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } + + @Test + public void handleDeleteTokensBatchException_timeout_yields408AndCleanMessage() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowDeleteTokenRequest.builder() + .tokens(Arrays.asList("tok1", "tok2")).vaultId("v1").build(); + + List errors = Utils.handleDeleteTokensBatchException(timeoutFailure(), batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } + + @Test + public void handleTokenizeBatchException_timeout_yields408AndCleanMessage() { + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest batch = + com.skyflow.generated.rest.resources.flowservice.requests.V1FlowTokenizeRequest.builder() + .vaultId("v1") + .data(Arrays.asList( + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v1").build(), + com.skyflow.generated.rest.types.V1FlowTokenizeRequestObject.builder().value("v2").build())) + .build(); + + List errors = Utils.handleTokenizeBatchException(timeoutFailure(), batch, 0, 2); + + Assert.assertEquals(2, errors.size()); + Assert.assertEquals("timeout maps to 408", 408, errors.get(0).getCode()); + Assert.assertEquals("Request timed out.", errors.get(0).getError()); + } } \ No newline at end of file