From 4033a03b369868214d40424322012c9718df23f7 Mon Sep 17 00:00:00 2001 From: Devesh-Skyflow Date: Tue, 28 Jul 2026 15:05:50 +0530 Subject: [PATCH] SK-3023 add configurable connect/read/write timeouts (v3) Extends the v3 HTTP config (Phase 2 of CUST-4311) with per-attempt connectTimeout, readTimeout, and writeTimeout, in seconds, at both the client-wide (Skyflow.builder()) and per-vault (VaultConfig) levels, with the same per-field precedence as the existing timeout/retry knobs. Unset phase timeouts leave OkHttp's built-in 10s default in place, so existing integrations are unchanged. The overall `timeout` (callTimeout) remains the total ceiling that bounds the whole call including retries; the three phase timeouts bound a single phase of one attempt. - VaultConfig: connectTimeout/readTimeout/writeTimeout fields + accessors - Skyflow.SkyflowClientBuilder: client-wide setters + propagation - VaultClient: resolve per-field and apply only when configured - Tests: OkHttp-default-when-unset, vault/client overrides, per-field - README: settings table + "how the four timeouts relate" guidance - TimeoutAndRetryConfigExample: show the three new timeouts in the sample Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 20 ++++- .../vault/TimeoutAndRetryConfigExample.java | 17 +++++ v3/src/main/java/com/skyflow/Skyflow.java | 42 ++++++++++- v3/src/main/java/com/skyflow/VaultClient.java | 40 +++++++++- .../java/com/skyflow/config/VaultConfig.java | 45 +++++++++++ .../SkyflowClientBuilderHttpConfigTests.java | 15 +++- .../skyflow/VaultClientHttpConfigTests.java | 74 ++++++++++++++++++- 7 files changed, 239 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 20ff2ed2..5180cd44 100644 --- a/README.md +++ b/README.md @@ -180,14 +180,19 @@ You can control how long a request is allowed to run and whether failed requests | Setting | Unit | Default | Description | | --- | --- | --- | --- | | `timeout` | seconds | `60` | Overall time budget for a request, including any retries and backoff. | +| `connectTimeout` | seconds | `10` | Time budget for establishing a connection, per attempt. | +| `readTimeout` | seconds | `10` | Time budget for reading the response, per attempt. | +| `writeTimeout` | seconds | `10` | Time budget for writing the request, per attempt. | | `maxRetries` | count | `0` | Number of retry attempts. `0` disables retries. | | `initialRetryDelayMillis` | milliseconds | `500` | Base delay before the first retry. | | `maxRetryDelayMillis` | milliseconds | `2000` | Upper bound on the delay between retries. | +**How the four timeouts relate:** `timeout` is the *total* ceiling for the whole call — every attempt, every backoff sleep, all counted together. `connectTimeout`, `readTimeout`, and `writeTimeout` each bound a single *phase of one attempt* (connecting, reading, writing). Because the phase timeouts are per attempt, their sum across retries can exceed `timeout` — but `timeout` always wins and cuts the call off. In practice, set `timeout` to cap worst-case blocking, and use the phase timeouts to fail faster on a stalled connection or a slow-but-not-dead server. The three phase timeouts default to the underlying HTTP client's `10` seconds; leaving them unset preserves that default. + Each setting is available at two levels: -- **Client-wide** — on `Skyflow.builder()`: `.timeout(int)`, `.maxRetries(int)`, `.initialRetryDelayMillis(long)`, `.maxRetryDelayMillis(long)`. Applies to every vault. -- **Per vault** — on `VaultConfig`: `.setTimeout(int)`, `.setMaxRetries(int)`, `.setInitialRetryDelayMillis(long)`, `.setMaxRetryDelayMillis(long)`. Applies to that vault only. +- **Client-wide** — on `Skyflow.builder()`: `.timeout(int)`, `.connectTimeout(int)`, `.readTimeout(int)`, `.writeTimeout(int)`, `.maxRetries(int)`, `.initialRetryDelayMillis(long)`, `.maxRetryDelayMillis(long)`. Applies to every vault. +- **Per vault** — on `VaultConfig`: `.setTimeout(int)`, `.setConnectTimeout(int)`, `.setReadTimeout(int)`, `.setWriteTimeout(int)`, `.setMaxRetries(int)`, `.setInitialRetryDelayMillis(long)`, `.setMaxRetryDelayMillis(long)`. Applies to that vault only. **Precedence:** a value set on `VaultConfig` (per vault) overrides the client-wide value set on `Skyflow.builder()`, which overrides the SDK default. Resolution is per field, so a vault can override just `timeout` and still inherit the client-wide retry settings. @@ -206,6 +211,9 @@ vaultConfig.setClusterId(""); vaultConfig.setEnv(Env.PROD); vaultConfig.setCredentials(credentials); vaultConfig.setTimeout(30); // seconds — overall request timeout +vaultConfig.setConnectTimeout(5); // seconds — per-attempt connect timeout +vaultConfig.setReadTimeout(20); // seconds — per-attempt read timeout +vaultConfig.setWriteTimeout(5); // seconds — per-attempt write timeout vaultConfig.setMaxRetries(3); // retry attempts (0 = retries off) vaultConfig.setInitialRetryDelayMillis(1000L); // base backoff in milliseconds vaultConfig.setMaxRetryDelayMillis(4000L); // backoff cap in milliseconds @@ -213,14 +221,18 @@ vaultConfig.setMaxRetryDelayMillis(4000L); // backoff cap in milliseconds // Client-wide defaults: apply to every vault unless overridden on the vault (as above). Skyflow skyflowClient = Skyflow.builder() .timeout(60) // seconds — overall request timeout + .connectTimeout(10) // seconds — per-attempt connect timeout + .readTimeout(15) // seconds — per-attempt read timeout + .writeTimeout(10) // seconds — per-attempt write timeout .maxRetries(2) // retry attempts (0 = retries off) .initialRetryDelayMillis(500L) // base backoff in milliseconds .maxRetryDelayMillis(2000L) // backoff cap in milliseconds .addVaultConfig(vaultConfig) .build(); -// Result for this vault: timeout=30, maxRetries=3, initialRetryDelayMillis=1000, maxRetryDelayMillis=4000 -// (all overridden per vault). A vault that sets none of these inherits the client-wide values above. +// Result for this vault: timeout=30, connectTimeout=5, readTimeout=20, writeTimeout=5, +// maxRetries=3, initialRetryDelayMillis=1000, maxRetryDelayMillis=4000 (all overridden per vault). +// A vault that sets none of these inherits the client-wide values above. ``` # Vault diff --git a/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java index eb4bf1e9..60bb145d 100644 --- a/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java +++ b/samples/src/main/java/com/example/vault/TimeoutAndRetryConfigExample.java @@ -14,6 +14,12 @@ *
    *
  • {@code timeout} – overall call timeout in seconds (bounds the whole * request including retries and backoff). Default: 60.
  • + *
  • {@code connectTimeout} – per-attempt connection-establishment timeout in + * seconds. Default: 10 (the underlying HTTP client default).
  • + *
  • {@code readTimeout} – per-attempt response-read timeout in seconds. + * Default: 10.
  • + *
  • {@code writeTimeout} – per-attempt request-write timeout in seconds. + * Default: 10.
  • *
  • {@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 initialRetryDelayMillis} – base backoff before the first retry, in milliseconds. @@ -22,6 +28,11 @@ * milliseconds. Default: 2000.
  • *
* + *

How they relate: {@code timeout} is the total ceiling for the whole call (all + * attempts + backoff). {@code connectTimeout}/{@code readTimeout}/{@code writeTimeout} each bound a + * single phase of one attempt; because they are per attempt, their sum across retries can + * exceed {@code timeout}, but {@code timeout} always wins and cuts the call off. + * *

Two levels + precedence: set client-wide defaults on {@code Skyflow.builder()}, and/or * per-vault overrides on {@code VaultConfig}. The most specific value wins, resolved per field: * per-vault → client-wide → SDK default. @@ -44,6 +55,9 @@ public static void main(String[] args) { // 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.setConnectTimeout(5); // seconds – fail fast if the connection stalls + vaultConfig.setReadTimeout(20); // seconds – allow a slower response read + vaultConfig.setWriteTimeout(5); // seconds – bound the request write vaultConfig.setMaxRetries(2); // fewer retries for this vault vaultConfig.setInitialRetryDelayMillis(500L); vaultConfig.setMaxRetryDelayMillis(1000L); @@ -53,6 +67,9 @@ public static void main(String[] args) { Skyflow skyflowClient = Skyflow.builder() .setLogLevel(LogLevel.ERROR) .timeout(60) // seconds – client-wide overall call timeout + .connectTimeout(10) // seconds – client-wide per-attempt connect timeout + .readTimeout(15) // seconds – client-wide per-attempt read timeout + .writeTimeout(10) // seconds – client-wide per-attempt write timeout .maxRetries(3) // client-wide retry attempts .initialRetryDelayMillis(500L) // client-wide base backoff (ms) .maxRetryDelayMillis(2000L) // client-wide backoff cap (ms) diff --git a/v3/src/main/java/com/skyflow/Skyflow.java b/v3/src/main/java/com/skyflow/Skyflow.java index df530215..f0d57c77 100644 --- a/v3/src/main/java/com/skyflow/Skyflow.java +++ b/v3/src/main/java/com/skyflow/Skyflow.java @@ -57,6 +57,9 @@ public static final class SkyflowClientBuilder extends BaseSkyflowClientBuilder private final LinkedHashMap vaultClientsMap; // Client-wide HTTP config defaults (apply to all vaults unless a vault overrides). null => SDK default. private Integer timeout; + private Integer connectTimeout; + private Integer readTimeout; + private Integer writeTimeout; private Integer maxRetries; private Long initialRetryDelayMillis; private Long maxRetryDelayMillis; @@ -84,7 +87,8 @@ 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.initialRetryDelayMillis, this.maxRetryDelayMillis); + controller.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, + this.writeTimeout, this.maxRetries, this.initialRetryDelayMillis, this.maxRetryDelayMillis); this.vaultClientsMap.put(vaultConfigCopy.getVaultId(), controller); LogUtil.printInfoLog(Utils.parameterizedString( InfoLogs.VAULT_CONTROLLER_INITIALIZED.getLog(), vaultConfigCopy.getVaultId())); @@ -114,6 +118,39 @@ public SkyflowClientBuilder timeout(int timeout) { return this; } + /** + * Client-wide per-attempt connection-establishment timeout in seconds. Applies to all vaults + * unless a vault overrides it; when unset the underlying HTTP client default (10s) applies. + * The overall {@code timeout} still bounds the whole call, including retries. + */ + public SkyflowClientBuilder connectTimeout(int connectTimeout) { + this.connectTimeout = connectTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Client-wide per-attempt response-read timeout in seconds. Applies to all vaults unless a + * vault overrides it; when unset the underlying HTTP client default (10s) applies. The overall + * {@code timeout} still bounds the whole call, including retries. + */ + public SkyflowClientBuilder readTimeout(int readTimeout) { + this.readTimeout = readTimeout; + propagateHttpConfig(); + return this; + } + + /** + * Client-wide per-attempt request-write timeout in seconds. Applies to all vaults unless a + * vault overrides it; when unset the underlying HTTP client default (10s) applies. The overall + * {@code timeout} still bounds the whole call, including retries. + */ + public SkyflowClientBuilder writeTimeout(int writeTimeout) { + this.writeTimeout = writeTimeout; + 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; @@ -137,7 +174,8 @@ public SkyflowClientBuilder maxRetryDelayMillis(long maxRetryDelayMillis) { private void propagateHttpConfig() { for (VaultController vault : this.vaultClientsMap.values()) { - vault.setCommonHttpConfig(this.timeout, this.maxRetries, this.initialRetryDelayMillis, this.maxRetryDelayMillis); + vault.setCommonHttpConfig(this.timeout, this.connectTimeout, this.readTimeout, + this.writeTimeout, 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 b06f2bf6..174a6ca1 100644 --- a/v3/src/main/java/com/skyflow/VaultClient.java +++ b/v3/src/main/java/com/skyflow/VaultClient.java @@ -49,6 +49,9 @@ public class VaultClient { private String currentVaultURL = null; // Client-wide (Skyflow builder) HTTP config defaults; null => fall back to the SDK defaults below. private Integer commonTimeout; + private Integer commonConnectTimeout; + private Integer commonReadTimeout; + private Integer commonWriteTimeout; private Integer commonMaxRetries; private Long commonInitialRetryDelayMillis; private Long commonMaxRetryDelayMillis; @@ -84,9 +87,13 @@ protected void setCommonCredentials(Credentials commonCredentials) throws Skyflo * 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, + protected void setCommonHttpConfig(Integer timeout, Integer connectTimeout, Integer readTimeout, + Integer writeTimeout, Integer maxRetries, Long initialRetryDelayMillis, Long maxRetryDelayMillis) { this.commonTimeout = timeout; + this.commonConnectTimeout = connectTimeout; + this.commonReadTimeout = readTimeout; + this.commonWriteTimeout = writeTimeout; this.commonMaxRetries = maxRetries; this.commonInitialRetryDelayMillis = initialRetryDelayMillis; this.commonMaxRetryDelayMillis = maxRetryDelayMillis; @@ -175,8 +182,12 @@ protected void updateExecutorInHTTP() { vaultConfig.getInitialRetryDelayMillis(), commonInitialRetryDelayMillis, DEFAULT_INITIAL_RETRY_DELAY_MILLIS); long maxRetryDelayMillis = resolveLong( vaultConfig.getMaxRetryDelayMillis(), commonMaxRetryDelayMillis, DEFAULT_MAX_RETRY_DELAY_MILLIS); + // Per-attempt timeouts: null => leave OkHttp's built-in 10s default (backward compatible). + Integer connectTimeout = resolveNullableInt(vaultConfig.getConnectTimeout(), commonConnectTimeout); + Integer readTimeout = resolveNullableInt(vaultConfig.getReadTimeout(), commonReadTimeout); + Integer writeTimeout = resolveNullableInt(vaultConfig.getWriteTimeout(), commonWriteTimeout); - sharedHttpClient = new OkHttpClient.Builder() + OkHttpClient.Builder httpBuilder = 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) @@ -186,8 +197,18 @@ protected void updateExecutorInHTTP() { .header("Authorization", "Bearer " + this.token) .build(); return chain.proceed(requestWithAuth); - }) - .build(); + }); + // Per-attempt phase timeouts; only override when explicitly configured. + if (connectTimeout != null) { + httpBuilder.connectTimeout(connectTimeout, TimeUnit.SECONDS); + } + if (readTimeout != null) { + httpBuilder.readTimeout(readTimeout, TimeUnit.SECONDS); + } + if (writeTimeout != null) { + httpBuilder.writeTimeout(writeTimeout, TimeUnit.SECONDS); + } + sharedHttpClient = httpBuilder.build(); apiClientBuilder.httpClient(sharedHttpClient); } } @@ -200,6 +221,17 @@ private static int resolveInt(Integer vaultLevel, Integer clientLevel, int defau return clientLevel != null ? clientLevel : defaultValue; } + /** + * Resolve an optional int setting: vault-level override, else client-wide default, else null. + * Null means "not configured" — the caller leaves the underlying HTTP client default in place. + */ + private static Integer resolveNullableInt(Integer vaultLevel, Integer clientLevel) { + if (vaultLevel != null) { + return vaultLevel; + } + return clientLevel; + } + /** 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) { diff --git a/v3/src/main/java/com/skyflow/config/VaultConfig.java b/v3/src/main/java/com/skyflow/config/VaultConfig.java index 29c7f02c..27f72e31 100644 --- a/v3/src/main/java/com/skyflow/config/VaultConfig.java +++ b/v3/src/main/java/com/skyflow/config/VaultConfig.java @@ -10,6 +10,9 @@ public class VaultConfig implements Cloneable { 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 connectTimeout; // per-attempt connection-establishment timeout, in seconds + private Integer readTimeout; // per-attempt response-read timeout, in seconds + private Integer writeTimeout; // per-attempt request-write 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 @@ -21,6 +24,9 @@ public VaultConfig() { this.env = Env.PROD; this.credentials = null; this.timeout = null; + this.connectTimeout = null; + this.readTimeout = null; + this.writeTimeout = null; this.maxRetries = null; this.initialRetryDelayMillis = null; this.maxRetryDelayMillis = null; @@ -75,6 +81,45 @@ public void setTimeout(Integer timeout) { this.timeout = timeout; } + public Integer getConnectTimeout() { + return connectTimeout; + } + + /** + * Per-attempt connection-establishment timeout in seconds for this vault. Overrides the + * client-wide default; when unset the underlying HTTP client default (10s) applies. Note the + * overall {@code timeout} still bounds the whole call, including retries. + */ + public void setConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Integer getReadTimeout() { + return readTimeout; + } + + /** + * Per-attempt response-read timeout in seconds for this vault. Overrides the client-wide + * default; when unset the underlying HTTP client default (10s) applies. Note the overall + * {@code timeout} still bounds the whole call, including retries. + */ + public void setReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + } + + public Integer getWriteTimeout() { + return writeTimeout; + } + + /** + * Per-attempt request-write timeout in seconds for this vault. Overrides the client-wide + * default; when unset the underlying HTTP client default (10s) applies. Note the overall + * {@code timeout} still bounds the whole call, including retries. + */ + public void setWriteTimeout(Integer writeTimeout) { + this.writeTimeout = writeTimeout; + } + public Integer getMaxRetries() { return maxRetries; } diff --git a/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java index 1d738266..4a07c8ac 100644 --- a/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java +++ b/v3/src/test/java/com/skyflow/SkyflowClientBuilderHttpConfigTests.java @@ -32,6 +32,9 @@ private Object commonField(VaultController controller, String name) throws Excep public void clientWideConfigSetBeforeAddVaultReachesController() throws Exception { Skyflow client = Skyflow.builder() .timeout(30) + .connectTimeout(5) + .readTimeout(20) + .writeTimeout(8) .maxRetries(2) .initialRetryDelayMillis(300L) .maxRetryDelayMillis(1500L) @@ -40,6 +43,9 @@ public void clientWideConfigSetBeforeAddVaultReachesController() throws Exceptio VaultController controller = client.vault(); Assert.assertEquals(Integer.valueOf(30), commonField(controller, "commonTimeout")); + Assert.assertEquals(Integer.valueOf(5), commonField(controller, "commonConnectTimeout")); + Assert.assertEquals(Integer.valueOf(20), commonField(controller, "commonReadTimeout")); + Assert.assertEquals(Integer.valueOf(8), commonField(controller, "commonWriteTimeout")); Assert.assertEquals(Integer.valueOf(2), commonField(controller, "commonMaxRetries")); Assert.assertEquals(Long.valueOf(300L), commonField(controller, "commonInitialRetryDelayMillis")); Assert.assertEquals(Long.valueOf(1500L), commonField(controller, "commonMaxRetryDelayMillis")); @@ -49,11 +55,15 @@ public void clientWideConfigSetBeforeAddVaultReachesController() throws Exceptio 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).initialRetryDelayMillis(700L).maxRetryDelayMillis(3000L); + builder.timeout(45).connectTimeout(6).readTimeout(25).writeTimeout(9) + .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(6), commonField(controller, "commonConnectTimeout")); + Assert.assertEquals(Integer.valueOf(25), commonField(controller, "commonReadTimeout")); + Assert.assertEquals(Integer.valueOf(9), commonField(controller, "commonWriteTimeout")); Assert.assertEquals(Integer.valueOf(4), commonField(controller, "commonMaxRetries")); Assert.assertEquals(Long.valueOf(700L), commonField(controller, "commonInitialRetryDelayMillis")); Assert.assertEquals(Long.valueOf(3000L), commonField(controller, "commonMaxRetryDelayMillis")); @@ -65,6 +75,9 @@ public void noClientWideConfigLeavesCommonFieldsNull() throws Exception { VaultController controller = client.vault(); Assert.assertNull(commonField(controller, "commonTimeout")); + Assert.assertNull(commonField(controller, "commonConnectTimeout")); + Assert.assertNull(commonField(controller, "commonReadTimeout")); + Assert.assertNull(commonField(controller, "commonWriteTimeout")); Assert.assertNull(commonField(controller, "commonMaxRetries")); 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 f206a437..e56a3159 100644 --- a/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java +++ b/v3/src/test/java/com/skyflow/VaultClientHttpConfigTests.java @@ -61,7 +61,7 @@ public void vaultLevelTimeoutOverridesDefault() throws Exception { public void clientLevelTimeoutUsedWhenNoVaultOverride() throws Exception { VaultConfig cfg = apiKeyConfig(); VaultClient client = new VaultClient(cfg, cfg.getCredentials()); - client.setCommonHttpConfig(45, null, null, null); // client-wide 45s, no vault override + client.setCommonHttpConfig(45, null, null, null, null, null, null); // client-wide 45s, no vault override client.setBearerToken(); Assert.assertEquals(45000, sharedClient(client).callTimeoutMillis()); @@ -72,7 +72,7 @@ public void vaultLevelTimeoutOverridesClientLevel() throws Exception { VaultConfig cfg = apiKeyConfig(); cfg.setTimeout(15); // vault-level VaultClient client = new VaultClient(cfg, cfg.getCredentials()); - client.setCommonHttpConfig(45, null, null, null); // client-wide 45s — should lose to vault's 15s + client.setCommonHttpConfig(45, null, null, null, null, null, null); // client-wide 45s — should lose to vault's 15s client.setBearerToken(); Assert.assertEquals(15000, sharedClient(client).callTimeoutMillis()); @@ -92,21 +92,89 @@ public void callTimeoutIsBoundedNotZero() throws Exception { public void vaultConfigStoresHttpFieldsAndDefaultsToNull() throws SkyflowException { VaultConfig cfg = new VaultConfig(); Assert.assertNull(cfg.getTimeout()); + Assert.assertNull(cfg.getConnectTimeout()); + Assert.assertNull(cfg.getReadTimeout()); + Assert.assertNull(cfg.getWriteTimeout()); Assert.assertNull(cfg.getMaxRetries()); Assert.assertNull(cfg.getInitialRetryDelayMillis()); Assert.assertNull(cfg.getMaxRetryDelayMillis()); cfg.setTimeout(30); + cfg.setConnectTimeout(5); + cfg.setReadTimeout(20); + cfg.setWriteTimeout(8); cfg.setMaxRetries(2); cfg.setInitialRetryDelayMillis(250L); cfg.setMaxRetryDelayMillis(1500L); Assert.assertEquals(Integer.valueOf(30), cfg.getTimeout()); + Assert.assertEquals(Integer.valueOf(5), cfg.getConnectTimeout()); + Assert.assertEquals(Integer.valueOf(20), cfg.getReadTimeout()); + Assert.assertEquals(Integer.valueOf(8), cfg.getWriteTimeout()); Assert.assertEquals(Integer.valueOf(2), cfg.getMaxRetries()); Assert.assertEquals(Long.valueOf(250L), cfg.getInitialRetryDelayMillis()); Assert.assertEquals(Long.valueOf(1500L), cfg.getMaxRetryDelayMillis()); } + // OkHttp's built-in default for connect/read/write is 10s. Unset SDK values must leave these untouched. + private static final int OKHTTP_DEFAULT_TIMEOUT_MS = 10000; + + @Test + public void connectReadWriteDefaultToOkHttpWhenNothingConfigured() throws Exception { + VaultConfig cfg = apiKeyConfig(); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setBearerToken(); + + OkHttpClient shared = sharedClient(client); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MS, shared.connectTimeoutMillis()); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MS, shared.readTimeoutMillis()); + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MS, shared.writeTimeoutMillis()); + } + + @Test + public void vaultLevelConnectReadWriteOverrideDefaults() throws Exception { + VaultConfig cfg = apiKeyConfig(); + cfg.setConnectTimeout(5); // seconds + cfg.setReadTimeout(20); + cfg.setWriteTimeout(8); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + client.setBearerToken(); + + OkHttpClient shared = sharedClient(client); + Assert.assertEquals(5000, shared.connectTimeoutMillis()); + Assert.assertEquals(20000, shared.readTimeoutMillis()); + Assert.assertEquals(8000, shared.writeTimeoutMillis()); + } + + @Test + public void clientLevelConnectReadWriteUsedWhenNoVaultOverride() throws Exception { + VaultConfig cfg = apiKeyConfig(); + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + // client-wide connect=5, read=20, write=8; no vault override + client.setCommonHttpConfig(null, 5, 20, 8, null, null, null); + client.setBearerToken(); + + OkHttpClient shared = sharedClient(client); + Assert.assertEquals(5000, shared.connectTimeoutMillis()); + Assert.assertEquals(20000, shared.readTimeoutMillis()); + Assert.assertEquals(8000, shared.writeTimeoutMillis()); + } + + @Test + public void connectReadWriteResolvePerFieldVaultOverClient() throws Exception { + VaultConfig cfg = apiKeyConfig(); + cfg.setConnectTimeout(3); // vault overrides only connect + VaultClient client = new VaultClient(cfg, cfg.getCredentials()); + // client-wide connect=5 (loses to vault's 3), read=20 (used), write left unset (OkHttp default) + client.setCommonHttpConfig(null, 5, 20, null, null, null, null); + client.setBearerToken(); + + OkHttpClient shared = sharedClient(client); + Assert.assertEquals(3000, shared.connectTimeoutMillis()); // vault + Assert.assertEquals(20000, shared.readTimeoutMillis()); // client-wide + Assert.assertEquals(OKHTTP_DEFAULT_TIMEOUT_MS, shared.writeTimeoutMillis()); // neither -> OkHttp default + } + private RetryInterceptor retryInterceptor(VaultClient client) throws Exception { for (Interceptor interceptor : sharedClient(client).interceptors()) { if (interceptor instanceof RetryInterceptor) { @@ -160,7 +228,7 @@ public void vaultLevelRetryConfigOverridesDefault() throws Exception { public void clientLevelRetryConfigUsedWhenNoVaultOverride() throws Exception { VaultConfig cfg = apiKeyConfig(); VaultClient client = new VaultClient(cfg, cfg.getCredentials()); - client.setCommonHttpConfig(null, 2, 300L, 1500L); // client-wide retry config, no vault override + client.setCommonHttpConfig(null, null, null, null, 2, 300L, 1500L); // client-wide retry config, no vault override client.setBearerToken(); RetryInterceptor ri = retryInterceptor(client);