diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java index 18f314ca..593673ff 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/LDClient.java @@ -31,6 +31,7 @@ import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -72,6 +73,10 @@ public final class LDClient implements LDClientInterface { private static final int EXCESSIVE_INIT_WAIT_MILLIS = 60000; + // The largest start wait the SDK can represent in milliseconds. A longer wait is clamped to it rather than + // converted, because converting it would overflow and throw (and would do so after the data system starts). + private static final Duration MAX_MILLIS_START_WAIT = Duration.ofMillis(Long.MAX_VALUE); + /** * Creates a new client instance that connects to LaunchDarkly with the default configuration. *

@@ -259,15 +264,21 @@ public LDClient(String sdkKey, LDConfig config) { // Start the data system Future startFuture = dataSystem.start(); if (!config.startWait.isZero() && !config.startWait.isNegative()) { + // Convert the start wait to milliseconds, clamping a wait too large to represent to the maximum rather + // than converting it, which would overflow and throw. Only the positive boundary matters here, since + // zero and negative waits are excluded above. + long startWaitMillis = config.startWait.compareTo(MAX_MILLIS_START_WAIT) >= 0 + ? Long.MAX_VALUE + : config.startWait.toMillis(); if (!dataSystem.isInitialized()) { baseLogger.info("Waiting up to {} milliseconds for LaunchDarkly client to start...", - config.startWait.toMillis()); - if (config.startWait.toMillis() > EXCESSIVE_INIT_WAIT_MILLIS) { - baseLogger.warn("LaunchDarkly client created with start wait time of {} milliseconds. We recommend a timeout of less than {} milliseconds.", config.startWait.toMillis(), EXCESSIVE_INIT_WAIT_MILLIS); + startWaitMillis); + if (startWaitMillis > EXCESSIVE_INIT_WAIT_MILLIS) { + baseLogger.warn("LaunchDarkly client created with start wait time of {} milliseconds. We recommend a timeout of less than {} milliseconds.", startWaitMillis, EXCESSIVE_INIT_WAIT_MILLIS); } } try { - startFuture.get(config.startWait.toMillis(), TimeUnit.MILLISECONDS); + startFuture.get(startWaitMillis, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { baseLogger.error("Timeout encountered waiting for LaunchDarkly client initialization"); } catch (Exception e) { diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java index e313aa68..a53c80f2 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java @@ -331,6 +331,25 @@ public void willWaitForDataSourceIfWaitMillisIsGreaterThanZero() throws Exceptio mocks.verifyAll(); } + @Test + public void startWaitTooLargeForMillisIsClampedRatherThanOverflowing() throws Exception { + // A start wait time too large to represent in milliseconds must be clamped, not converted directly: + // converting it throws ArithmeticException, and it would do so after the data source has started, leaving + // the data source running with no reference available to close it. + LDConfig.Builder config = new LDConfig.Builder() + .startWait(Duration.ofSeconds(Long.MAX_VALUE)); + + expect(dataSource.start()).andReturn(initFuture); + expect(initFuture.get(Long.MAX_VALUE, TimeUnit.MILLISECONDS)).andReturn(null); + expect(dataSource.isInitialized()).andReturn(false).anyTimes(); + mocks.replayAll(); + + client = createMockClient(config); + assertFalse(client.isInitialized()); + + mocks.verifyAll(); + } + @Test public void noWaitForDataSourceIfWaitMillisIsNegative() throws Exception { LDConfig.Builder config = new LDConfig.Builder()