diff --git a/src/main/java/com/launchdarkly/client/Components.java b/src/main/java/com/launchdarkly/client/Components.java index e673b8f20..a700a0810 100644 --- a/src/main/java/com/launchdarkly/client/Components.java +++ b/src/main/java/com/launchdarkly/client/Components.java @@ -1,12 +1,15 @@ package com.launchdarkly.client; import com.launchdarkly.client.integrations.PersistentDataStoreBuilder; +import com.launchdarkly.client.integrations.PollingDataSourceBuilder; +import com.launchdarkly.client.integrations.StreamingDataSourceBuilder; import com.launchdarkly.client.interfaces.PersistentDataStoreFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.io.IOException; import java.net.URI; +import java.util.concurrent.Future; + +import static com.google.common.util.concurrent.Futures.immediateFuture; /** * Provides factories for the standard implementations of LaunchDarkly component interfaces. @@ -19,22 +22,27 @@ public abstract class Components { private static final UpdateProcessorFactory defaultUpdateProcessorFactory = new DefaultUpdateProcessorFactory(); private static final UpdateProcessorFactory nullUpdateProcessorFactory = new NullUpdateProcessorFactory(); + private Components() {} + /** - * Returns a factory for the default in-memory implementation of a data store. + * Returns a configuration object for using the default in-memory implementation of a data store. + *

+ * Since it is the default, you do not normally need to call this method, unless you need to create + * a data store instance for testing purposes. *

* Note that the interface is still named {@link FeatureStoreFactory}, but in a future version it * will be renamed to {@code DataStoreFactory}. * * @return a factory object * @see LDConfig.Builder#dataStore(FeatureStoreFactory) - * @since 4.11.0 + * @since 4.12.0 */ public static FeatureStoreFactory inMemoryDataStore() { return inMemoryFeatureStoreFactory; } /** - * Returns a configurable factory for some implementation of a persistent data store. + * Returns a configuration builder for some implementation of a persistent data store. *

* This method is used in conjunction with another factory object provided by specific components * such as the Redis integration. The latter provides builder methods for options that are specific @@ -57,7 +65,7 @@ public static FeatureStoreFactory inMemoryDataStore() { * @return a {@link PersistentDataStoreBuilder} * @see LDConfig.Builder#dataStore(FeatureStoreFactory) * @see com.launchdarkly.client.integrations.Redis - * @since 4.11.0 + * @since 4.12.0 */ public static PersistentDataStoreBuilder persistentDataStore(PersistentDataStoreFactory storeFactory) { return new PersistentDataStoreBuilder(storeFactory); @@ -117,27 +125,80 @@ public static EventProcessorFactory defaultEventProcessor() { public static EventProcessorFactory nullEventProcessor() { return nullEventProcessorFactory; } + + /** + * Returns a configurable factory for using streaming mode to get feature flag data. + *

+ * By default, the SDK uses a streaming connection to receive feature flag data from LaunchDarkly. To use the + * default behavior, you do not need to call this method. However, if you want to customize the behavior of + * the connection, call this method to obtain a builder, change its properties with the + * {@link StreamingDataSourceBuilder} methods, and pass it to {@link LDConfig.Builder#dataSource(UpdateProcessorFactory)}: + *

 
+   *     LDConfig config = new LDConfig.Builder()
+   *         .dataSource(Components.streamingDataSource().initialReconnectDelayMillis(500))
+   *         .build();
+   * 
+ *

+ * These properties will override any equivalent deprecated properties that were set with {@code LDConfig.Builder}, + * such as {@link LDConfig.Builder#reconnectTimeMs(long)}. + *

+ * (Note that the interface is still named {@link UpdateProcessorFactory}, but in a future version it + * will be renamed to {@code DataSourceFactory}.) + * + * @return a builder for setting streaming connection properties + * @since 4.12.0 + */ + public static StreamingDataSourceBuilder streamingDataSource() { + return new StreamingDataSourceBuilderImpl(); + } /** - * Returns a factory for the default implementation of the component for receiving feature flag data - * from LaunchDarkly. Based on your configuration, this implementation uses either streaming or - * polling, or does nothing if the client is offline, or in LDD mode. + * Returns a configurable factory for using polling mode to get feature flag data. + *

+ * This is not the default behavior; by default, the SDK uses a streaming connection to receive feature flag + * data from LaunchDarkly. In polling mode, the SDK instead makes a new HTTP request to LaunchDarkly at regular + * intervals. HTTP caching allows it to avoid redundantly downloading data if there have been no changes, but + * polling is still less efficient than streaming and should only be used on the advice of LaunchDarkly support. + *

+ * To use polling mode, call this method to obtain a builder, change its properties with the + * {@link PollingDataSourceBuilder} methods, and pass it to {@link LDConfig.Builder#dataSource(UpdateProcessorFactory)}: + *


+   *     LDConfig config = new LDConfig.Builder()
+   *         .dataSource(Components.pollingDataSource().pollIntervalMillis(45000))
+   *         .build();
+   * 
+ *

+ * These properties will override any equivalent deprecated properties that were set with {@code LDConfig.Builder}, + * such as {@link LDConfig.Builder#pollingIntervalMillis(long)}. However, setting {@link LDConfig.Builder#offline(boolean)} + * to {@code true} will supersede this setting and completely disable network requests. + *

+ * (Note that the interface is still named {@link UpdateProcessorFactory}, but in a future version it + * will be renamed to {@code DataSourceFactory}.) * - * Note that the interface is still named {@link UpdateProcessorFactory}, but in a future version it - * will be renamed to {@code DataSourceFactory}. - * - * @return a factory object - * @since 4.11.0 - * @see LDConfig.Builder#dataSource(UpdateProcessorFactory) + * @return a builder for setting polling properties + * @since 4.12.0 */ - public static UpdateProcessorFactory defaultDataSource() { - return defaultUpdateProcessorFactory; + public static PollingDataSourceBuilder pollingDataSource() { + return new PollingDataSourceBuilderImpl(); } - + /** - * Deprecated name for {@link #defaultDataSource()}. + * Deprecated method for using the default data source implementation. + *

+ * If you pass the return value of this method to {@link LDConfig.Builder#dataSource(UpdateProcessorFactory)}, + * the behavior is as follows: + *

+ * * @return a factory object - * @deprecated Use {@link #defaultDataSource()}. + * @deprecated Use {@link #streamingDataSource()}, {@link #pollingDataSource()}, or {@link #externalUpdatesOnly()}. */ @Deprecated public static UpdateProcessorFactory defaultUpdateProcessor() { @@ -145,24 +206,38 @@ public static UpdateProcessorFactory defaultUpdateProcessor() { } /** - * Returns a factory for a null implementation of {@link UpdateProcessor}, which does not - * connect to LaunchDarkly, regardless of any other configuration. - * - * Note that the interface is still named {@link UpdateProcessorFactory}, but in a future version it - * will be renamed to {@code DataSourceFactory}. + * Returns a configuration object that disables a direct connection with LaunchDarkly for feature flag updates. + *

+ * Passing this to {@link LDConfig.Builder#dataSource(UpdateProcessorFactory)} causes the SDK + * not to retrieve feature flag data from LaunchDarkly, regardless of any other configuration. + * This is normally done if you are using the Relay Proxy + * in "daemon mode", where an external process-- the Relay Proxy-- connects to LaunchDarkly and populates + * a persistent data store with the feature flag data. The data store could also be populated by + * another process that is running the LaunchDarkly SDK. If there is no external process updating + * the data store, then the SDK will not have any feature flag data and will return application + * default values only. + *


+   *     LDConfig config = new LDConfig.Builder()
+   *         .dataSource(Components.externalUpdatesOnly())
+   *         .dataStore(Components.persistentDataStore(Redis.dataStore())) // assuming the Relay Proxy is using Redis
+   *         .build();
+   * 
+ *

+ * (Note that the interface is still named {@link UpdateProcessorFactory}, but in a future version it + * will be renamed to {@code DataSourceFactory}.) * * @return a factory object - * @since 4.11.0 + * @since 4.12.0 * @see LDConfig.Builder#dataSource(UpdateProcessorFactory) */ - public static UpdateProcessorFactory nullDataSource() { + public static UpdateProcessorFactory externalUpdatesOnly() { return nullUpdateProcessorFactory; } /** - * Deprecated name for {@link #nullDataSource()}. + * Deprecated name for {@link #externalUpdatesOnly()}. * @return a factory object - * @deprecated Use {@link #nullDataSource()}. + * @deprecated Use {@link #externalUpdatesOnly()}. */ @Deprecated public static UpdateProcessorFactory nullUpdateProcessor() { @@ -194,37 +269,117 @@ public EventProcessor createEventProcessor(String sdkKey, LDConfig config) { } private static final class DefaultUpdateProcessorFactory implements UpdateProcessorFactory { - // Note, logger uses LDClient class name for backward compatibility - private static final Logger logger = LoggerFactory.getLogger(LDClient.class); - - @SuppressWarnings("deprecation") @Override public UpdateProcessor createUpdateProcessor(String sdkKey, LDConfig config, FeatureStore featureStore) { - if (config.offline) { - logger.info("Starting LaunchDarkly client in offline mode"); - return new UpdateProcessor.NullUpdateProcessor(); - } else if (config.useLdd) { - logger.info("Starting LaunchDarkly in LDD mode. Skipping direct feature retrieval."); - return new UpdateProcessor.NullUpdateProcessor(); + // We don't need to check config.offline or config.useLdd here; the former is checked automatically + // by StreamingDataSourceBuilder and PollingDataSourceBuilder, and setting the latter is translated + // into using externalUpdatesOnly() by LDConfig.Builder. + if (config.stream) { + return streamingDataSource() + .baseUri(config.deprecatedStreamURI) + .pollingBaseUri(config.deprecatedBaseURI) + .initialReconnectDelayMillis(config.deprecatedReconnectTimeMs) + .createUpdateProcessor(sdkKey, config, featureStore); } else { - DefaultFeatureRequestor requestor = new DefaultFeatureRequestor(sdkKey, config); - if (config.stream) { - logger.info("Enabling streaming API"); - return new StreamProcessor(sdkKey, config, requestor, featureStore, null); - } else { - logger.info("Disabling streaming API"); - logger.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support"); - return new PollingProcessor(config, requestor, featureStore); - } + return pollingDataSource() + .baseUri(config.deprecatedBaseURI) + .pollIntervalMillis(config.deprecatedPollingIntervalMillis) + .createUpdateProcessor(sdkKey, config, featureStore); } } } private static final class NullUpdateProcessorFactory implements UpdateProcessorFactory { - @SuppressWarnings("deprecation") @Override public UpdateProcessor createUpdateProcessor(String sdkKey, LDConfig config, FeatureStore featureStore) { - return new UpdateProcessor.NullUpdateProcessor(); + if (config.offline) { + // If they have explicitly called offline(true) to disable everything, we'll log this slightly + // more specific message. + LDClient.logger.info("Starting LaunchDarkly client in offline mode"); + } else { + LDClient.logger.info("LaunchDarkly client will not connect to Launchdarkly for feature flag data"); + } + return new NullUpdateProcessor(); + } + } + + // Package-private for visibility in tests + static final class NullUpdateProcessor implements UpdateProcessor { + @Override + public Future start() { + return immediateFuture(null); + } + + @Override + public boolean initialized() { + return true; + } + + @Override + public void close() throws IOException {} + } + + private static final class StreamingDataSourceBuilderImpl extends StreamingDataSourceBuilder { + @Override + public UpdateProcessor createUpdateProcessor(String sdkKey, LDConfig config, FeatureStore featureStore) { + // Note, we log startup messages under the LDClient class to keep logs more readable + + if (config.offline) { + LDClient.logger.info("Starting LaunchDarkly client in offline mode"); + return Components.externalUpdatesOnly().createUpdateProcessor(sdkKey, config, featureStore); + } + + LDClient.logger.info("Enabling streaming API"); + + URI streamUri = baseUri == null ? LDConfig.DEFAULT_STREAM_URI : baseUri; + URI pollUri; + if (pollingBaseUri != null) { + pollUri = pollingBaseUri; + } else { + // If they have set a custom base URI, and they did *not* set a custom polling URI, then we can + // assume they're using Relay in which case both of those values are the same. + pollUri = baseUri == null ? LDConfig.DEFAULT_BASE_URI : baseUri; + } + + DefaultFeatureRequestor requestor = new DefaultFeatureRequestor( + sdkKey, + config, + pollUri, + false + ); + + return new StreamProcessor( + sdkKey, + config, + requestor, + featureStore, + null, + streamUri, + initialReconnectDelayMillis + ); + } + } + + private static final class PollingDataSourceBuilderImpl extends PollingDataSourceBuilder { + @Override + public UpdateProcessor createUpdateProcessor(String sdkKey, LDConfig config, FeatureStore featureStore) { + // Note, we log startup messages under the LDClient class to keep logs more readable + + if (config.offline) { + LDClient.logger.info("Starting LaunchDarkly client in offline mode"); + return Components.externalUpdatesOnly().createUpdateProcessor(sdkKey, config, featureStore); + } + + LDClient.logger.info("Disabling streaming API"); + LDClient.logger.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support"); + + DefaultFeatureRequestor requestor = new DefaultFeatureRequestor( + sdkKey, + config, + baseUri == null ? LDConfig.DEFAULT_BASE_URI : baseUri, + true + ); + return new PollingProcessor(requestor, featureStore, pollIntervalMillis); } } } diff --git a/src/main/java/com/launchdarkly/client/DefaultFeatureRequestor.java b/src/main/java/com/launchdarkly/client/DefaultFeatureRequestor.java index 99bbd9535..b0bfdc02a 100644 --- a/src/main/java/com/launchdarkly/client/DefaultFeatureRequestor.java +++ b/src/main/java/com/launchdarkly/client/DefaultFeatureRequestor.java @@ -7,6 +7,7 @@ import java.io.File; import java.io.IOException; +import java.net.URI; import java.util.HashMap; import java.util.Map; @@ -21,7 +22,10 @@ import okhttp3.Request; import okhttp3.Response; -class DefaultFeatureRequestor implements FeatureRequestor { +/** + * Implementation of getting flag data via a polling request. Used by both streaming and polling components. + */ +final class DefaultFeatureRequestor implements FeatureRequestor { private static final Logger logger = LoggerFactory.getLogger(DefaultFeatureRequestor.class); private static final String GET_LATEST_FLAGS_PATH = "/sdk/latest-flags"; private static final String GET_LATEST_SEGMENTS_PATH = "/sdk/latest-segments"; @@ -30,18 +34,22 @@ class DefaultFeatureRequestor implements FeatureRequestor { private final String sdkKey; private final LDConfig config; + private final URI baseUri; private final OkHttpClient httpClient; + private final boolean useCache; - DefaultFeatureRequestor(String sdkKey, LDConfig config) { + DefaultFeatureRequestor(String sdkKey, LDConfig config, URI baseUri, boolean useCache) { this.sdkKey = sdkKey; - this.config = config; + this.config = config; // this is no longer the source of truth for baseURI, but it can still affect HTTP behavior + this.baseUri = baseUri; + this.useCache = useCache; OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); configureHttpClientBuilder(config, httpBuilder); // HTTP caching is used only for FeatureRequestor. However, when streaming is enabled, HTTP GETs // made by FeatureRequester will always guarantee a new flag state, so we disable the cache. - if (!config.stream) { + if (useCache) { File cacheDir = Files.createTempDir(); Cache cache = new Cache(cacheDir, MAX_HTTP_CACHE_SIZE_BYTES); httpBuilder.cache(cache); @@ -78,7 +86,7 @@ public AllData getAllData() throws IOException, HttpErrorException { private String get(String path) throws IOException, HttpErrorException { Request request = getRequestBuilder(sdkKey) - .url(config.baseURI.resolve(path).toURL()) + .url(baseUri.resolve(path).toURL()) .get() .build(); @@ -92,7 +100,7 @@ private String get(String path) throws IOException, HttpErrorException { } logger.debug("Get flag(s) response: " + response.toString() + " with body: " + body); logger.debug("Network response: " + response.networkResponse()); - if(!config.stream) { + if (useCache) { logger.debug("Cache hit count: " + httpClient.cache().hitCount() + " Cache network Count: " + httpClient.cache().networkCount()); logger.debug("Cache response: " + response.cacheResponse()); } diff --git a/src/main/java/com/launchdarkly/client/EventOutputFormatter.java b/src/main/java/com/launchdarkly/client/EventOutputFormatter.java index e1db41097..a8428ca69 100644 --- a/src/main/java/com/launchdarkly/client/EventOutputFormatter.java +++ b/src/main/java/com/launchdarkly/client/EventOutputFormatter.java @@ -13,7 +13,7 @@ * Rather than creating intermediate objects to represent this schema, we use the Gson streaming * output API to construct JSON directly. */ -class EventOutputFormatter { +final class EventOutputFormatter { private final LDConfig config; EventOutputFormatter(LDConfig config) { diff --git a/src/main/java/com/launchdarkly/client/FeatureStoreCacheConfig.java b/src/main/java/com/launchdarkly/client/FeatureStoreCacheConfig.java index 010886791..4c73d2f53 100644 --- a/src/main/java/com/launchdarkly/client/FeatureStoreCacheConfig.java +++ b/src/main/java/com/launchdarkly/client/FeatureStoreCacheConfig.java @@ -96,7 +96,7 @@ public enum StaleValuesPolicy { /** * Used internally for backward compatibility. * @return the equivalent enum value - * @since 4.11.0 + * @since 4.12.0 */ public PersistentDataStoreBuilder.StaleValuesPolicy toNewEnum() { switch (this) { @@ -113,7 +113,7 @@ public PersistentDataStoreBuilder.StaleValuesPolicy toNewEnum() { * Used internally for backward compatibility. * @param policy the enum value in the new API * @return the equivalent enum value - * @since 4.11.0 + * @since 4.12.0 */ public static StaleValuesPolicy fromNewEnum(PersistentDataStoreBuilder.StaleValuesPolicy policy) { switch (policy) { diff --git a/src/main/java/com/launchdarkly/client/HttpErrorException.java b/src/main/java/com/launchdarkly/client/HttpErrorException.java index 8f6672bbb..8450e260f 100644 --- a/src/main/java/com/launchdarkly/client/HttpErrorException.java +++ b/src/main/java/com/launchdarkly/client/HttpErrorException.java @@ -1,7 +1,7 @@ package com.launchdarkly.client; @SuppressWarnings("serial") -class HttpErrorException extends Exception { +final class HttpErrorException extends Exception { private final int status; public HttpErrorException(int status) { diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index 676c518ed..8e713fd0f 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -1,6 +1,7 @@ package com.launchdarkly.client; import com.google.gson.JsonElement; +import com.launchdarkly.client.Components.NullUpdateProcessor; import com.launchdarkly.client.value.LDValue; import org.apache.commons.codec.binary.Hex; @@ -30,7 +31,9 @@ * a single {@code LDClient} for the lifetime of their application. */ public final class LDClient implements LDClientInterface { - private static final Logger logger = LoggerFactory.getLogger(LDClient.class); + // Package-private so other classes can log under the top-level logger's tag + static final Logger logger = LoggerFactory.getLogger(LDClient.class); + private static final String HMAC_ALGORITHM = "HmacSHA256"; static final String CLIENT_VERSION = getClientVersion(); @@ -83,12 +86,13 @@ public LDClient(String sdkKey, LDConfig config) { Components.defaultEventProcessor() : config.eventProcessorFactory; this.eventProcessor = epFactory.createEventProcessor(sdkKey, config); + @SuppressWarnings("deprecation") // defaultUpdateProcessor will be replaced by streamingDataSource once the deprecated config.stream is removed UpdateProcessorFactory upFactory = config.dataSourceFactory == null ? - Components.defaultDataSource() : config.dataSourceFactory; + Components.defaultUpdateProcessor() : config.dataSourceFactory; this.updateProcessor = upFactory.createUpdateProcessor(sdkKey, config, featureStore); Future startFuture = updateProcessor.start(); if (config.startWaitMillis > 0L) { - if (!config.offline && !config.useLdd) { + if (!(updateProcessor instanceof NullUpdateProcessor)) { logger.info("Waiting up to " + config.startWaitMillis + " milliseconds for LaunchDarkly client to start..."); } try { diff --git a/src/main/java/com/launchdarkly/client/LDConfig.java b/src/main/java/com/launchdarkly/client/LDConfig.java index bc73cfb14..fedbbc94a 100644 --- a/src/main/java/com/launchdarkly/client/LDConfig.java +++ b/src/main/java/com/launchdarkly/client/LDConfig.java @@ -2,6 +2,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.launchdarkly.client.integrations.PollingDataSourceBuilder; +import com.launchdarkly.client.integrations.StreamingDataSourceBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,25 +33,25 @@ public final class LDConfig { private static final Logger logger = LoggerFactory.getLogger(LDConfig.class); final Gson gson = new GsonBuilder().registerTypeAdapter(LDUser.class, new LDUser.UserAdapterWithPrivateAttributeBehavior(this)).create(); - private static final URI DEFAULT_BASE_URI = URI.create("https://app.launchdarkly.com"); - private static final URI DEFAULT_EVENTS_URI = URI.create("https://events.launchdarkly.com"); - private static final URI DEFAULT_STREAM_URI = URI.create("https://stream.launchdarkly.com"); + static final URI DEFAULT_BASE_URI = URI.create("https://app.launchdarkly.com"); + static final URI DEFAULT_EVENTS_URI = URI.create("https://events.launchdarkly.com"); + static final URI DEFAULT_STREAM_URI = URI.create("https://stream.launchdarkly.com"); private static final int DEFAULT_CAPACITY = 10000; private static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 2000; private static final int DEFAULT_SOCKET_TIMEOUT_MILLIS = 10000; private static final int DEFAULT_FLUSH_INTERVAL_SECONDS = 5; - private static final long MIN_POLLING_INTERVAL_MILLIS = 30000L; + private static final long MIN_POLLING_INTERVAL_MILLIS = PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS; private static final long DEFAULT_START_WAIT_MILLIS = 5000L; private static final int DEFAULT_SAMPLING_INTERVAL = 0; private static final int DEFAULT_USER_KEYS_CAPACITY = 1000; private static final int DEFAULT_USER_KEYS_FLUSH_INTERVAL_SECONDS = 60 * 5; - private static final long DEFAULT_RECONNECT_TIME_MILLIS = 1000; + private static final long DEFAULT_RECONNECT_TIME_MILLIS = StreamingDataSourceBuilder.DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS; protected static final LDConfig DEFAULT = new Builder().build(); - final URI baseURI; + final URI deprecatedBaseURI; final URI eventsURI; - final URI streamURI; + final URI deprecatedStreamURI; final int capacity; final int flushInterval; final Proxy proxy; @@ -59,15 +61,14 @@ public final class LDConfig { final FeatureStoreFactory dataStoreFactory; final EventProcessorFactory eventProcessorFactory; final UpdateProcessorFactory dataSourceFactory; - final boolean useLdd; final boolean offline; final boolean allAttributesPrivate; final Set privateAttrNames; final boolean sendEvents; - final long pollingIntervalMillis; + final long deprecatedPollingIntervalMillis; final long startWaitMillis; final int samplingInterval; - final long reconnectTimeMs; + final long deprecatedReconnectTimeMs; final int userKeysCapacity; final int userKeysFlushInterval; final boolean inlineUsersInEvents; @@ -79,31 +80,30 @@ public final class LDConfig { final TimeUnit socketTimeoutUnit; protected LDConfig(Builder builder) { - this.baseURI = builder.baseURI; + this.deprecatedBaseURI = builder.baseURI; this.eventsURI = builder.eventsURI; this.capacity = builder.capacity; this.flushInterval = builder.flushIntervalSeconds; this.proxy = builder.proxy(); this.proxyAuthenticator = builder.proxyAuthenticator(); - this.streamURI = builder.streamURI; + this.deprecatedStreamURI = builder.streamURI; this.stream = builder.stream; this.deprecatedFeatureStore = builder.featureStore; this.dataStoreFactory = builder.dataStoreFactory; this.eventProcessorFactory = builder.eventProcessorFactory; this.dataSourceFactory = builder.dataSourceFactory; - this.useLdd = builder.useLdd; this.offline = builder.offline; this.allAttributesPrivate = builder.allAttributesPrivate; this.privateAttrNames = new HashSet<>(builder.privateAttrNames); this.sendEvents = builder.sendEvents; if (builder.pollingIntervalMillis < MIN_POLLING_INTERVAL_MILLIS) { - this.pollingIntervalMillis = MIN_POLLING_INTERVAL_MILLIS; + this.deprecatedPollingIntervalMillis = MIN_POLLING_INTERVAL_MILLIS; } else { - this.pollingIntervalMillis = builder.pollingIntervalMillis; + this.deprecatedPollingIntervalMillis = builder.pollingIntervalMillis; } this.startWaitMillis = builder.startWaitMillis; this.samplingInterval = builder.samplingInterval; - this.reconnectTimeMs = builder.reconnectTimeMillis; + this.deprecatedReconnectTimeMs = builder.reconnectTimeMillis; this.userKeysCapacity = builder.userKeysCapacity; this.userKeysFlushInterval = builder.userKeysFlushInterval; this.inlineUsersInEvents = builder.inlineUsersInEvents; @@ -149,15 +149,14 @@ public static class Builder { private String proxyUsername = null; private String proxyPassword = null; private boolean stream = true; - private boolean useLdd = false; private boolean offline = false; private boolean allAttributesPrivate = false; private boolean sendEvents = true; private long pollingIntervalMillis = MIN_POLLING_INTERVAL_MILLIS; private FeatureStore featureStore = null; - private FeatureStoreFactory dataStoreFactory = Components.inMemoryDataStore(); - private EventProcessorFactory eventProcessorFactory = Components.defaultEventProcessor(); - private UpdateProcessorFactory dataSourceFactory = Components.defaultDataSource(); + private FeatureStoreFactory dataStoreFactory = null; + private EventProcessorFactory eventProcessorFactory = null; + private UpdateProcessorFactory dataSourceFactory = null; private long startWaitMillis = DEFAULT_START_WAIT_MILLIS; private int samplingInterval = DEFAULT_SAMPLING_INTERVAL; private long reconnectTimeMillis = DEFAULT_RECONNECT_TIME_MILLIS; @@ -175,11 +174,17 @@ public Builder() { } /** - * Set the base URL of the LaunchDarkly server for this configuration. + * Deprecated method for setting the base URI for the polling service. + *

+ * This method has no effect if you have used {@link #dataSource(UpdateProcessorFactory)} to + * specify polling or streaming options, which is the preferred method. * * @param baseURI the base URL of the LaunchDarkly server for this configuration. * @return the builder + * @deprecated Use {@link Components#streamingDataSource()} with {@link StreamingDataSourceBuilder#pollingBaseUri(URI)}, + * or {@link Components#pollingDataSource()} with {@link PollingDataSourceBuilder#baseUri(URI)}. */ + @Deprecated public Builder baseURI(URI baseURI) { this.baseURI = baseURI; return this; @@ -197,11 +202,16 @@ public Builder eventsURI(URI eventsURI) { } /** - * Set the base URL of the LaunchDarkly streaming server for this configuration. + * Deprecated method for setting the base URI for the streaming service. + *

+ * This method has no effect if you have used {@link #dataSource(UpdateProcessorFactory)} to + * specify polling or streaming options, which is the preferred method. * * @param streamURI the base URL of the LaunchDarkly streaming server * @return the builder + * @deprecated Use {@link Components#streamingDataSource()} with {@link StreamingDataSourceBuilder#pollingBaseUri(URI)}. */ + @Deprecated public Builder streamURI(URI streamURI) { this.streamURI = streamURI; return this; @@ -218,7 +228,7 @@ public Builder streamURI(URI streamURI) { * * @param factory the factory object * @return the builder - * @since 4.11.0 + * @since 4.12.0 */ public Builder dataStore(FeatureStoreFactory factory) { this.dataStoreFactory = factory; @@ -257,7 +267,7 @@ public Builder featureStoreFactory(FeatureStoreFactory factory) { * you may choose to use a custom implementation (for instance, a test fixture). * @param factory the factory object * @return the builder - * @since 4.11.0 + * @since 4.12.0 */ public Builder eventProcessor(EventProcessorFactory factory) { this.eventProcessorFactory = factory; @@ -278,15 +288,20 @@ public Builder eventProcessorFactory(EventProcessorFactory factory) { /** * Sets the implementation of the component that receives feature flag data from LaunchDarkly, - * using a factory object. The default is {@link Components#defaultDataSource()}, but - * you may choose to use a custom implementation (for instance, a test fixture). - * + * using a factory object. Depending on the implementation, the factory may be a builder that + * allows you to set other configuration options as well. + *

+ * The default is {@link Components#streamingDataSource()}. You may instead use + * {@link Components#pollingDataSource()}, or a test fixture such as + * {@link com.launchdarkly.client.integrations.FileData#dataSource()}. See those methods + * for details on how to configure them. + *

* Note that the interface is still named {@link UpdateProcessorFactory}, but in a future version * it will be renamed to {@code DataSourceFactory}. * * @param factory the factory object * @return the builder - * @since 4.11.0 + * @since 4.12.0 */ public Builder dataSource(UpdateProcessorFactory factory) { this.dataSourceFactory = factory; @@ -307,12 +322,18 @@ public Builder updateProcessorFactory(UpdateProcessorFactory factory) { } /** - * Set whether streaming mode should be enabled. By default, streaming is enabled. It should only be - * disabled on the advice of LaunchDarkly support. - * + * Deprecated method for enabling or disabling streaming mode. + *

+ * By default, streaming is enabled. It should only be disabled on the advice of LaunchDarkly support. + *

+ * This method has no effect if you have specified a data source with {@link #dataSource(UpdateProcessorFactory)}, + * which is the preferred method. + * * @param stream whether streaming mode should be enabled * @return the builder + * @deprecated Use {@link Components#streamingDataSource()} or {@link Components#pollingDataSource()}. */ + @Deprecated public Builder stream(boolean stream) { this.stream = stream; return this; @@ -465,20 +486,34 @@ public Builder sslSocketFactory(SSLSocketFactory sslSocketFactory, X509TrustMana } /** - * Set whether this client should use the LaunchDarkly - * relay in daemon mode, versus subscribing to the streaming or polling API. - * + * Deprecated method for using the LaunchDarkly Relay Proxy in daemon mode. + *

+ * See {@link Components#externalUpdatesOnly()} for the preferred way to do this. + * * @param useLdd true to use the relay in daemon mode; false to use streaming or polling * @return the builder + * @deprecated Use {@link Components#externalUpdatesOnly()}. */ + @Deprecated public Builder useLdd(boolean useLdd) { - this.useLdd = useLdd; - return this; + if (useLdd) { + return dataSource(Components.externalUpdatesOnly()); + } else { + return dataSource(null); + } } /** * Set whether this client is offline. - * + *

+ * In offline mode, the SDK will not make network connections to LaunchDarkly for any purpose. Feature + * flag data will only be available if it already exists in the data store, and analytics events will + * not be sent. + *

+ * This is equivalent to calling {@code dataSource(Components.externalUpdatesOnly())} and + * {@code sendEvents(false)}. It overrides any other values you may have set for + * {@link #dataSource(UpdateProcessorFactory)} or {@link #eventProcessor(EventProcessorFactory)}. + * * @param offline when set to true no calls to LaunchDarkly will be made * @return the builder */ @@ -513,12 +548,18 @@ public Builder sendEvents(boolean sendEvents) { } /** - * Set the polling interval (when streaming is disabled). Values less than the default of - * 30000 will be set to the default. + * Deprecated method for setting the polling interval in polling mode. + *

+ * Values less than the default of 30000 will be set to the default. + *

+ * This method has no effect if you have not disabled streaming mode, or if you have specified + * a non-polling data source with {@link #dataSource(UpdateProcessorFactory)}. * * @param pollingIntervalMillis rule update polling interval in milliseconds * @return the builder + * @deprecated Use {@link Components#pollingDataSource()} and {@link PollingDataSourceBuilder#pollIntervalMillis(long)}. */ + @Deprecated public Builder pollingIntervalMillis(long pollingIntervalMillis) { this.pollingIntervalMillis = pollingIntervalMillis; return this; @@ -554,13 +595,16 @@ public Builder samplingInterval(int samplingInterval) { } /** - * The reconnect base time in milliseconds for the streaming connection. The streaming connection - * uses an exponential backoff algorithm (with jitter) for reconnects, but will start the backoff - * with a value near the value specified here. + * Deprecated method for setting the initial reconnect delay for the streaming connection. + *

+ * This method has no effect if you have disabled streaming mode, or if you have specified a + * non-streaming data source with {@link #dataSource(UpdateProcessorFactory)}. * * @param reconnectTimeMs the reconnect time base value in milliseconds * @return the builder + * @deprecated Use {@link Components#streamingDataSource()} and {@link StreamingDataSourceBuilder#initialReconnectDelayMillis(long)}. */ + @Deprecated public Builder reconnectTimeMs(long reconnectTimeMs) { this.reconnectTimeMillis = reconnectTimeMs; return this; diff --git a/src/main/java/com/launchdarkly/client/PollingProcessor.java b/src/main/java/com/launchdarkly/client/PollingProcessor.java index 436fa4659..6cc20cb87 100644 --- a/src/main/java/com/launchdarkly/client/PollingProcessor.java +++ b/src/main/java/com/launchdarkly/client/PollingProcessor.java @@ -17,19 +17,19 @@ import static com.launchdarkly.client.Util.httpErrorMessage; import static com.launchdarkly.client.Util.isHttpErrorRecoverable; -class PollingProcessor implements UpdateProcessor { +final class PollingProcessor implements UpdateProcessor { private static final Logger logger = LoggerFactory.getLogger(PollingProcessor.class); private final FeatureRequestor requestor; - private final LDConfig config; private final FeatureStore store; + private final long pollIntervalMillis; private AtomicBoolean initialized = new AtomicBoolean(false); private ScheduledExecutorService scheduler = null; - PollingProcessor(LDConfig config, FeatureRequestor requestor, FeatureStore featureStore) { - this.requestor = requestor; - this.config = config; + PollingProcessor(FeatureRequestor requestor, FeatureStore featureStore, long pollIntervalMillis) { + this.requestor = requestor; // note that HTTP configuration is applied to the requestor when it is created this.store = featureStore; + this.pollIntervalMillis = pollIntervalMillis; } @Override @@ -47,7 +47,7 @@ public void close() throws IOException { @Override public Future start() { logger.info("Starting LaunchDarkly polling client with interval: " - + config.pollingIntervalMillis + " milliseconds"); + + pollIntervalMillis + " milliseconds"); final SettableFuture initFuture = SettableFuture.create(); ThreadFactory threadFactory = new ThreadFactoryBuilder() .setNameFormat("LaunchDarkly-PollingProcessor-%d") @@ -75,7 +75,7 @@ public void run() { logger.debug(e.toString(), e); } } - }, 0L, config.pollingIntervalMillis, TimeUnit.MILLISECONDS); + }, 0L, pollIntervalMillis, TimeUnit.MILLISECONDS); return initFuture; } diff --git a/src/main/java/com/launchdarkly/client/SemanticVersion.java b/src/main/java/com/launchdarkly/client/SemanticVersion.java index 29a124fd4..7e0ef034c 100644 --- a/src/main/java/com/launchdarkly/client/SemanticVersion.java +++ b/src/main/java/com/launchdarkly/client/SemanticVersion.java @@ -7,7 +7,7 @@ * Simple implementation of semantic version parsing and comparison according to the Semantic * Versions 2.0.0 standard (http://semver.org). */ -class SemanticVersion implements Comparable { +final class SemanticVersion implements Comparable { private static Pattern VERSION_REGEX = Pattern.compile( "^(?0|[1-9]\\d*)(\\.(?0|[1-9]\\d*))?(\\.(?0|[1-9]\\d*))?" + diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index ce76e09ac..389eb6e21 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -36,8 +36,10 @@ final class StreamProcessor implements UpdateProcessor { private static final int DEAD_CONNECTION_INTERVAL_MS = 300 * 1000; private final FeatureStore store; - private final LDConfig config; private final String sdkKey; + private final LDConfig config; + private final URI streamUri; + private final long initialReconnectDelayMillis; private final FeatureRequestor requestor; private final EventSourceCreator eventSourceCreator; private volatile EventSource es; @@ -46,16 +48,26 @@ final class StreamProcessor implements UpdateProcessor { ConnectionErrorHandler connectionErrorHandler = createDefaultConnectionErrorHandler(); // exposed for testing public static interface EventSourceCreator { - EventSource createEventSource(LDConfig config, EventHandler handler, URI streamUri, ConnectionErrorHandler errorHandler, Headers headers); + EventSource createEventSource(LDConfig config, EventHandler handler, URI streamUri, long initialReconnectDelayMillis, + ConnectionErrorHandler errorHandler, Headers headers); } - StreamProcessor(String sdkKey, LDConfig config, FeatureRequestor requestor, FeatureStore featureStore, - EventSourceCreator eventSourceCreator) { + StreamProcessor( + String sdkKey, + LDConfig config, + FeatureRequestor requestor, + FeatureStore featureStore, + EventSourceCreator eventSourceCreator, + URI streamUri, + long initialReconnectDelayMillis + ) { this.store = featureStore; - this.config = config; this.sdkKey = sdkKey; + this.config = config; // this is no longer the source of truth for streamUri or initialReconnectDelayMillis, but it can affect HTTP behavior this.requestor = requestor; this.eventSourceCreator = eventSourceCreator != null ? eventSourceCreator : new DefaultEventSourceCreator(); + this.streamUri = streamUri; + this.initialReconnectDelayMillis = initialReconnectDelayMillis; } private ConnectionErrorHandler createDefaultConnectionErrorHandler() { @@ -191,7 +203,8 @@ public void onError(Throwable throwable) { }; es = eventSourceCreator.createEventSource(config, handler, - URI.create(config.streamURI.toASCIIString() + "/all"), + URI.create(streamUri.toASCIIString() + "/all"), + initialReconnectDelayMillis, wrappedConnectionErrorHandler, headers); es.start(); @@ -218,31 +231,29 @@ public boolean initialized() { private static final class PutData { FeatureRequestor.AllData data; - public PutData() { - - } + @SuppressWarnings("unused") // used by Gson + public PutData() { } } private static final class PatchData { String path; JsonElement data; - public PatchData() { - - } + @SuppressWarnings("unused") // used by Gson + public PatchData() { } } private static final class DeleteData { String path; int version; - public DeleteData() { - - } + @SuppressWarnings("unused") // used by Gson + public DeleteData() { } } private class DefaultEventSourceCreator implements EventSourceCreator { - public EventSource createEventSource(final LDConfig config, EventHandler handler, URI streamUri, ConnectionErrorHandler errorHandler, Headers headers) { + public EventSource createEventSource(final LDConfig config, EventHandler handler, URI streamUri, long initialReconnectDelayMillis, + ConnectionErrorHandler errorHandler, Headers headers) { EventSource.Builder builder = new EventSource.Builder(handler, streamUri) .clientBuilderActions(new EventSource.Builder.ClientConfigurer() { public void configure(OkHttpClient.Builder builder) { @@ -251,7 +262,7 @@ public void configure(OkHttpClient.Builder builder) { }) .connectionErrorHandler(errorHandler) .headers(headers) - .reconnectTimeMs(config.reconnectTimeMs) + .reconnectTimeMs(initialReconnectDelayMillis) .readTimeoutMs(DEAD_CONNECTION_INTERVAL_MS) .connectTimeoutMs(EventSource.DEFAULT_CONNECT_TIMEOUT_MS) .writeTimeoutMs(EventSource.DEFAULT_WRITE_TIMEOUT_MS); diff --git a/src/main/java/com/launchdarkly/client/UpdateProcessor.java b/src/main/java/com/launchdarkly/client/UpdateProcessor.java index 15cc7231e..52bd712ce 100644 --- a/src/main/java/com/launchdarkly/client/UpdateProcessor.java +++ b/src/main/java/com/launchdarkly/client/UpdateProcessor.java @@ -33,8 +33,10 @@ public interface UpdateProcessor extends Closeable { /** * An implementation of {@link UpdateProcessor} that does nothing. * - * @deprecated Use {@link Components#nullUpdateProcessor()} instead of referring to this implementation class directly. + * @deprecated Use {@link Components#externalUpdatesOnly()} instead of referring to this implementation class directly. */ + // This was exposed because everything in an interface is public. The SDK itself no longer refers to this class; + // instead it uses Components.NullUpdateProcessor. @Deprecated static final class NullUpdateProcessor implements UpdateProcessor { @Override diff --git a/src/main/java/com/launchdarkly/client/integrations/CacheMonitor.java b/src/main/java/com/launchdarkly/client/integrations/CacheMonitor.java index 618edc63d..977982d9f 100644 --- a/src/main/java/com/launchdarkly/client/integrations/CacheMonitor.java +++ b/src/main/java/com/launchdarkly/client/integrations/CacheMonitor.java @@ -7,7 +7,7 @@ * A conduit that an application can use to monitor caching behavior of a persistent data store. * * @see PersistentDataStoreBuilder#cacheMonitor(CacheMonitor) - * @since 4.11.0 + * @since 4.12.0 */ public final class CacheMonitor { private Callable source; @@ -48,7 +48,7 @@ public CacheStats getCacheStats() { * internally, but is not guaranteed to always do so, and to avoid embedding Guava API details in * the SDK API this is provided as a separate class. * - * @since 4.11.0 + * @since 4.12.0 */ public static final class CacheStats { private final long hitCount; diff --git a/src/main/java/com/launchdarkly/client/integrations/FileData.java b/src/main/java/com/launchdarkly/client/integrations/FileData.java index a6f65f3e2..9771db552 100644 --- a/src/main/java/com/launchdarkly/client/integrations/FileData.java +++ b/src/main/java/com/launchdarkly/client/integrations/FileData.java @@ -7,7 +7,7 @@ * typically be used in a test environment, to operate using a predetermined feature flag state * without an actual LaunchDarkly connection. See {@link #dataSource()} for details. * - * @since 4.11.0 + * @since 4.12.0 */ public abstract class FileData { /** @@ -106,7 +106,7 @@ public abstract class FileData { * duplicate key-- it will not load flags from any of the files. * * @return a data source configuration object - * @since 4.11.0 + * @since 4.12.0 */ public static FileDataSourceBuilder dataSource() { return new FileDataSourceBuilder(); diff --git a/src/main/java/com/launchdarkly/client/integrations/FileDataSourceBuilder.java b/src/main/java/com/launchdarkly/client/integrations/FileDataSourceBuilder.java index 4c3cd1993..49df9e3de 100644 --- a/src/main/java/com/launchdarkly/client/integrations/FileDataSourceBuilder.java +++ b/src/main/java/com/launchdarkly/client/integrations/FileDataSourceBuilder.java @@ -18,7 +18,7 @@ *

* For more details, see {@link FileData}. * - * @since 4.11.0 + * @since 4.12.0 */ public final class FileDataSourceBuilder implements UpdateProcessorFactory { private final List sources = new ArrayList<>(); diff --git a/src/main/java/com/launchdarkly/client/integrations/PersistentDataStoreBuilder.java b/src/main/java/com/launchdarkly/client/integrations/PersistentDataStoreBuilder.java index 43a1b42b3..f3c5a9261 100644 --- a/src/main/java/com/launchdarkly/client/integrations/PersistentDataStoreBuilder.java +++ b/src/main/java/com/launchdarkly/client/integrations/PersistentDataStoreBuilder.java @@ -33,7 +33,7 @@ * In this example, {@code .url()} is an option specifically for the Redis integration, whereas * {@code ttlSeconds()} is an option that can be used for any persistent data store. * - * @since 4.11.0 + * @since 4.12.0 */ @SuppressWarnings("deprecation") public final class PersistentDataStoreBuilder implements FeatureStoreFactory { diff --git a/src/main/java/com/launchdarkly/client/integrations/PollingDataSourceBuilder.java b/src/main/java/com/launchdarkly/client/integrations/PollingDataSourceBuilder.java new file mode 100644 index 000000000..7999b63c1 --- /dev/null +++ b/src/main/java/com/launchdarkly/client/integrations/PollingDataSourceBuilder.java @@ -0,0 +1,73 @@ +package com.launchdarkly.client.integrations; + +import com.launchdarkly.client.Components; +import com.launchdarkly.client.UpdateProcessorFactory; + +import java.net.URI; + +/** + * Contains methods for configuring the polling data source. + *

+ * Polling is not the default behavior; by default, the SDK uses a streaming connection to receive feature flag + * data from LaunchDarkly. In polling mode, the SDK instead makes a new HTTP request to LaunchDarkly at regular + * intervals. HTTP caching allows it to avoid redundantly downloading data if there have been no changes, but + * polling is still less efficient than streaming and should only be used on the advice of LaunchDarkly support. + *

+ * To use polling mode, create a builder with {@link Components#pollingDataSource()}, + * change its properties with the methods of this class, and pass it to {@link com.launchdarkly.client.LDConfig.Builder#dataSource(UpdateProcessorFactory)}: + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .dataSource(Components.pollingDataSource().pollIntervalMillis(45000))
+ *         .build();
+ * 
+ *

+ * These properties will override any equivalent deprecated properties that were set with {@code LDConfig.Builder}, + * such as {@link com.launchdarkly.client.LDConfig.Builder#pollingIntervalMillis(long)}. + *

+ * Note that this class is abstract; the actual implementation is created by calling {@link Components#pollingDataSource()}. + * + * @since 4.12.0 + */ +public abstract class PollingDataSourceBuilder implements UpdateProcessorFactory { + /** + * The default and minimum value for {@link #pollIntervalMillis(long)}. + */ + public static final long DEFAULT_POLL_INTERVAL_MILLIS = 30000L; + + protected URI baseUri; + protected long pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS; + + /** + * Sets a custom base URI for the polling service. + *

+ * You will only need to change this value in the following cases: + *

+ * + * @param baseUri the base URI of the polling service; null to use the default + * @return the builder + */ + public PollingDataSourceBuilder baseUri(URI baseUri) { + this.baseUri = baseUri; + return this; + } + + /** + * Sets the interval at which the SDK will poll for feature flag updates. + *

+ * The default and minimum value is {@link #DEFAULT_POLL_INTERVAL_MILLIS}. Values less than this will be + * set to the default. + * + * @param pollIntervalMillis the polling interval in milliseconds + * @return the builder + */ + public PollingDataSourceBuilder pollIntervalMillis(long pollIntervalMillis) { + this.pollIntervalMillis = pollIntervalMillis < DEFAULT_POLL_INTERVAL_MILLIS ? + DEFAULT_POLL_INTERVAL_MILLIS : + pollIntervalMillis; + return this; + } +} diff --git a/src/main/java/com/launchdarkly/client/integrations/Redis.java b/src/main/java/com/launchdarkly/client/integrations/Redis.java index 7a167ae9d..90bed1e00 100644 --- a/src/main/java/com/launchdarkly/client/integrations/Redis.java +++ b/src/main/java/com/launchdarkly/client/integrations/Redis.java @@ -3,7 +3,7 @@ /** * Integration between the LaunchDarkly SDK and Redis. * - * @since 4.11.0 + * @since 4.12.0 */ public abstract class Redis { /** diff --git a/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreBuilder.java b/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreBuilder.java index 70b25b792..5c63ca819 100644 --- a/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreBuilder.java +++ b/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreBuilder.java @@ -23,16 +23,18 @@ * Builder calls can be chained, for example: * *


- * LDConfig config = new LDConfig.Builder()
- *      .dataStore(
- *           Redis.dataStore()
- *               .database(1)
- *               .caching(FeatureStoreCacheConfig.enabled().ttlSeconds(60))
- *      )
- *      .build();
+ *     LDConfig config = new LDConfig.Builder()
+ *         .dataStore(
+ *             Components.persistentDataStore(
+ *                 Redis.dataStore()
+ *                     .url("redis://my-redis-host")
+ *                     .database(1)
+ *             ).cacheSeconds(15)
+ *         )
+ *         .build();
  * 
* - * @since 4.11.0 + * @since 4.12.0 */ public final class RedisDataStoreBuilder implements PersistentDataStoreFactory { /** diff --git a/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreImpl.java b/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreImpl.java index 24e3968b7..c81a02762 100644 --- a/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreImpl.java +++ b/src/main/java/com/launchdarkly/client/integrations/RedisDataStoreImpl.java @@ -22,7 +22,7 @@ import redis.clients.jedis.Transaction; import redis.clients.util.JedisURIHelper; -class RedisDataStoreImpl implements FeatureStoreCore { +final class RedisDataStoreImpl implements FeatureStoreCore { private static final Logger logger = LoggerFactory.getLogger(RedisDataStoreImpl.class); private final JedisPool pool; diff --git a/src/main/java/com/launchdarkly/client/integrations/StreamingDataSourceBuilder.java b/src/main/java/com/launchdarkly/client/integrations/StreamingDataSourceBuilder.java new file mode 100644 index 000000000..b62ca4367 --- /dev/null +++ b/src/main/java/com/launchdarkly/client/integrations/StreamingDataSourceBuilder.java @@ -0,0 +1,88 @@ +package com.launchdarkly.client.integrations; + +import com.launchdarkly.client.Components; +import com.launchdarkly.client.UpdateProcessorFactory; + +import java.net.URI; + +/** + * Contains methods for configuring the streaming data source. + *

+ * By default, the SDK uses a streaming connection to receive feature flag data from LaunchDarkly. If you want + * to customize the behavior of the connection, create a builder with {@link Components#streamingDataSource()}, + * change its properties with the methods of this class, and pass it to {@link com.launchdarkly.client.LDConfig.Builder#dataSource(UpdateProcessorFactory)}: + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .dataSource(Components.streamingDataSource().initialReconnectDelayMillis(500))
+ *         .build();
+ * 
+ *

+ * These properties will override any equivalent deprecated properties that were set with {@code LDConfig.Builder}, + * such as {@link com.launchdarkly.client.LDConfig.Builder#reconnectTimeMs(long)}. + *

+ * Note that this class is abstract; the actual implementation is created by calling {@link Components#streamingDataSource()}. + * + * @since 4.12.0 + */ +public abstract class StreamingDataSourceBuilder implements UpdateProcessorFactory { + /** + * The default value for {@link #initialReconnectDelayMillis(long)}. + */ + public static final long DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS = 1000; + + protected URI baseUri; + protected URI pollingBaseUri; + protected long initialReconnectDelayMillis = DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS; + + /** + * Sets a custom base URI for the streaming service. + *

+ * You will only need to change this value in the following cases: + *

+ * + * @param baseUri the base URI of the streaming service; null to use the default + * @return the builder + */ + public StreamingDataSourceBuilder baseUri(URI baseUri) { + this.baseUri = baseUri; + return this; + } + + /** + * Sets the initial reconnect delay for the streaming connection. + *

+ * The streaming service uses a backoff algorithm (with jitter) every time the connection needs + * to be reestablished. The delay for the first reconnection will start near this value, and then + * increase exponentially for any subsequent connection failures. + *

+ * The default value is {@link #DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS}. + * + * @param initialReconnectDelayMillis the reconnect time base value in milliseconds + * @return the builder + */ + + public StreamingDataSourceBuilder initialReconnectDelayMillis(long initialReconnectDelayMillis) { + this.initialReconnectDelayMillis = initialReconnectDelayMillis; + return this; + } + + /** + * Sets a custom base URI for special polling requests. + *

+ * Even in streaming mode, the SDK sometimes temporarily must do a polling request. You do not need to + * modify this property unless you are connecting to a test server or a nonstandard endpoing for the + * LaunchDarkly service. If you are using the Relay Proxy, + * you only need to set {@link #baseUri(URI)}. + * + * @param pollingBaseUri the polling endpoint URI; null to use the default + * @return the builder + */ + public StreamingDataSourceBuilder pollingBaseUri(URI pollingBaseUri) { + this.pollingBaseUri = pollingBaseUri; + return this; + } +} diff --git a/src/main/java/com/launchdarkly/client/integrations/package-info.java b/src/main/java/com/launchdarkly/client/integrations/package-info.java index 589a2c63a..079858106 100644 --- a/src/main/java/com/launchdarkly/client/integrations/package-info.java +++ b/src/main/java/com/launchdarkly/client/integrations/package-info.java @@ -1,5 +1,6 @@ /** - * This package contains integration tools for connecting the SDK to other software components. + * This package contains integration tools for connecting the SDK to other software components, or + * configuring how it connects to LaunchDarkly. *

* In the current main LaunchDarkly Java SDK library, this package contains {@link com.launchdarkly.client.integrations.Redis} * (for using Redis as a store for flag data) and {@link com.launchdarkly.client.integrations.FileData} diff --git a/src/main/java/com/launchdarkly/client/interfaces/PersistentDataStoreFactory.java b/src/main/java/com/launchdarkly/client/interfaces/PersistentDataStoreFactory.java index 8931247d3..16a5b5544 100644 --- a/src/main/java/com/launchdarkly/client/interfaces/PersistentDataStoreFactory.java +++ b/src/main/java/com/launchdarkly/client/interfaces/PersistentDataStoreFactory.java @@ -10,7 +10,7 @@ * {@link com.launchdarkly.client.Components#persistentDataStore}. * * @see com.launchdarkly.client.Components - * @since 4.11.0 + * @since 4.12.0 */ public interface PersistentDataStoreFactory { /** diff --git a/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java b/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java index b80386f10..276cbab00 100644 --- a/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java +++ b/src/test/java/com/launchdarkly/client/FeatureRequestorTest.java @@ -8,7 +8,6 @@ import javax.net.ssl.SSLHandshakeException; -import static com.launchdarkly.client.TestHttpUtil.baseConfig; import static com.launchdarkly.client.TestHttpUtil.httpsServerWithSelfSignedCert; import static com.launchdarkly.client.TestHttpUtil.jsonResponse; import static com.launchdarkly.client.TestHttpUtil.makeStartedServer; @@ -33,12 +32,23 @@ public class FeatureRequestorTest { private static final String segmentsJson = "{\"" + segment1Key + "\":" + segment1Json + "}"; private static final String allDataJson = "{\"flags\":" + flagsJson + ",\"segments\":" + segmentsJson + "}"; + private DefaultFeatureRequestor makeRequestor(MockWebServer server) { + return makeRequestor(server, LDConfig.DEFAULT); + // We can always use LDConfig.DEFAULT unless we need to modify HTTP properties, since DefaultFeatureRequestor + // no longer uses the deprecated LDConfig.baseUri property. + } + + private DefaultFeatureRequestor makeRequestor(MockWebServer server, LDConfig config) { + URI uri = server.url("").uri(); + return new DefaultFeatureRequestor(sdkKey, config, uri, true); + } + @Test public void requestAllData() throws Exception { MockResponse resp = jsonResponse(allDataJson); try (MockWebServer server = makeStartedServer(resp)) { - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, basePollingConfig(server).build())) { + try (DefaultFeatureRequestor r = makeRequestor(server)) { FeatureRequestor.AllData data = r.getAllData(); RecordedRequest req = server.takeRequest(); @@ -61,7 +71,7 @@ public void requestFlag() throws Exception { MockResponse resp = jsonResponse(flag1Json); try (MockWebServer server = makeStartedServer(resp)) { - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, basePollingConfig(server).build())) { + try (DefaultFeatureRequestor r = makeRequestor(server)) { FeatureFlag flag = r.getFlag(flag1Key); RecordedRequest req = server.takeRequest(); @@ -78,7 +88,7 @@ public void requestSegment() throws Exception { MockResponse resp = jsonResponse(segment1Json); try (MockWebServer server = makeStartedServer(resp)) { - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, basePollingConfig(server).build())) { + try (DefaultFeatureRequestor r = makeRequestor(server)) { Segment segment = r.getSegment(segment1Key); RecordedRequest req = server.takeRequest(); @@ -95,7 +105,7 @@ public void requestFlagNotFound() throws Exception { MockResponse notFoundResp = new MockResponse().setResponseCode(404); try (MockWebServer server = makeStartedServer(notFoundResp)) { - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, basePollingConfig(server).build())) { + try (DefaultFeatureRequestor r = makeRequestor(server)) { try { r.getFlag(flag1Key); Assert.fail("expected exception"); @@ -111,7 +121,7 @@ public void requestSegmentNotFound() throws Exception { MockResponse notFoundResp = new MockResponse().setResponseCode(404); try (MockWebServer server = makeStartedServer(notFoundResp)) { - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, basePollingConfig(server).build())) { + try (DefaultFeatureRequestor r = makeRequestor(server)) { try { r.getSegment(segment1Key); fail("expected exception"); @@ -129,7 +139,7 @@ public void requestsAreCached() throws Exception { .setHeader("Cache-Control", "max-age=1000"); try (MockWebServer server = makeStartedServer(cacheableResp)) { - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, basePollingConfig(server).build())) { + try (DefaultFeatureRequestor r = makeRequestor(server)) { FeatureFlag flag1a = r.getFlag(flag1Key); RecordedRequest req1 = server.takeRequest(); @@ -150,7 +160,7 @@ public void httpClientDoesNotAllowSelfSignedCertByDefault() throws Exception { MockResponse resp = jsonResponse(flag1Json); try (TestHttpUtil.ServerWithCert serverWithCert = httpsServerWithSelfSignedCert(resp)) { - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, basePollingConfig(serverWithCert.server).build())) { + try (DefaultFeatureRequestor r = makeRequestor(serverWithCert.server)) { try { r.getFlag(flag1Key); fail("expected exception"); @@ -167,11 +177,11 @@ public void httpClientCanUseCustomTlsConfig() throws Exception { MockResponse resp = jsonResponse(flag1Json); try (TestHttpUtil.ServerWithCert serverWithCert = httpsServerWithSelfSignedCert(resp)) { - LDConfig config = basePollingConfig(serverWithCert.server) + LDConfig config = new LDConfig.Builder() .sslSocketFactory(serverWithCert.sslClient.socketFactory, serverWithCert.sslClient.trustManager) // allows us to trust the self-signed cert .build(); - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, config)) { + try (DefaultFeatureRequestor r = makeRequestor(serverWithCert.server, config)) { FeatureFlag flag = r.getFlag(flag1Key); verifyFlag(flag, flag1Key); } @@ -184,12 +194,11 @@ public void httpClientCanUseProxyConfig() throws Exception { try (MockWebServer server = makeStartedServer(jsonResponse(flag1Json))) { HttpUrl serverUrl = server.url("/"); LDConfig config = new LDConfig.Builder() - .baseURI(fakeBaseUri) .proxyHost(serverUrl.host()) .proxyPort(serverUrl.port()) .build(); - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, config)) { + try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(sdkKey, config, fakeBaseUri, true)) { FeatureFlag flag = r.getFlag(flag1Key); verifyFlag(flag, flag1Key); @@ -198,11 +207,6 @@ public void httpClientCanUseProxyConfig() throws Exception { } } - private LDConfig.Builder basePollingConfig(MockWebServer server) { - return baseConfig(server) - .stream(false); - } - private void verifyHeaders(RecordedRequest req) { assertEquals(sdkKey, req.getHeader("Authorization")); assertEquals("JavaClient/" + LDClient.CLIENT_VERSION, req.getHeader("User-Agent")); diff --git a/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java index a513e9d0d..f60f3633b 100644 --- a/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java @@ -6,7 +6,8 @@ import org.junit.Test; -import static com.launchdarkly.client.TestHttpUtil.baseConfig; +import static com.launchdarkly.client.TestHttpUtil.basePollingConfig; +import static com.launchdarkly.client.TestHttpUtil.baseStreamingConfig; import static com.launchdarkly.client.TestHttpUtil.httpsServerWithSelfSignedCert; import static com.launchdarkly.client.TestHttpUtil.jsonResponse; import static com.launchdarkly.client.TestHttpUtil.makeStartedServer; @@ -31,8 +32,8 @@ public void clientStartsInPollingMode() throws Exception { MockResponse resp = jsonResponse(makeAllDataJson()); try (MockWebServer server = makeStartedServer(resp)) { - LDConfig config = baseConfig(server) - .stream(false) + LDConfig config = new LDConfig.Builder() + .dataSource(basePollingConfig(server)) .sendEvents(false) .build(); @@ -48,8 +49,8 @@ public void clientFailsInPollingModeWith401Error() throws Exception { MockResponse resp = new MockResponse().setResponseCode(401); try (MockWebServer server = makeStartedServer(resp)) { - LDConfig config = baseConfig(server) - .stream(false) + LDConfig config = new LDConfig.Builder() + .dataSource(basePollingConfig(server)) .sendEvents(false) .build(); @@ -65,8 +66,8 @@ public void clientStartsInPollingModeWithSelfSignedCert() throws Exception { MockResponse resp = jsonResponse(makeAllDataJson()); try (TestHttpUtil.ServerWithCert serverWithCert = httpsServerWithSelfSignedCert(resp)) { - LDConfig config = baseConfig(serverWithCert.server) - .stream(false) + LDConfig config = new LDConfig.Builder() + .dataSource(basePollingConfig(serverWithCert.server)) .sendEvents(false) .sslSocketFactory(serverWithCert.sslClient.socketFactory, serverWithCert.sslClient.trustManager) // allows us to trust the self-signed cert .build(); @@ -85,7 +86,8 @@ public void clientStartsInStreamingMode() throws Exception { MockResponse resp = TestHttpUtil.eventStreamResponse(streamData); try (MockWebServer server = makeStartedServer(resp)) { - LDConfig config = baseConfig(server) + LDConfig config = new LDConfig.Builder() + .dataSource(baseStreamingConfig(server)) .sendEvents(false) .build(); @@ -101,7 +103,8 @@ public void clientFailsInStreamingModeWith401Error() throws Exception { MockResponse resp = new MockResponse().setResponseCode(401); try (MockWebServer server = makeStartedServer(resp)) { - LDConfig config = baseConfig(server) + LDConfig config = new LDConfig.Builder() + .dataSource(baseStreamingConfig(server)) .sendEvents(false) .build(); @@ -119,7 +122,8 @@ public void clientStartsInStreamingModeWithSelfSignedCert() throws Exception { MockResponse resp = TestHttpUtil.eventStreamResponse(streamData); try (TestHttpUtil.ServerWithCert serverWithCert = httpsServerWithSelfSignedCert(resp)) { - LDConfig config = baseConfig(serverWithCert.server) + LDConfig config = new LDConfig.Builder() + .dataSource(baseStreamingConfig(serverWithCert.server)) .sendEvents(false) .sslSocketFactory(serverWithCert.sslClient.socketFactory, serverWithCert.sslClient.trustManager) // allows us to trust the self-signed cert .build(); diff --git a/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java b/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java index 9db225910..83321c7a7 100644 --- a/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientEvaluationTest.java @@ -36,7 +36,7 @@ public class LDClientEvaluationTest { private LDConfig config = new LDConfig.Builder() .dataStore(specificFeatureStore(featureStore)) .eventProcessor(Components.nullEventProcessor()) - .dataSource(Components.nullDataSource()) + .dataSource(Components.externalUpdatesOnly()) .build(); private LDClientInterface client = new LDClient("SDK_KEY", config); @@ -266,7 +266,7 @@ public void appropriateErrorForUnexpectedException() throws Exception { LDConfig badConfig = new LDConfig.Builder() .dataStore(specificFeatureStore(badFeatureStore)) .eventProcessor(Components.nullEventProcessor()) - .dataSource(Components.nullDataSource()) + .dataSource(Components.externalUpdatesOnly()) .build(); try (LDClientInterface badClient = new LDClient("SDK_KEY", badConfig)) { EvaluationDetail expectedResult = EvaluationDetail.fromValue(false, null, diff --git a/src/test/java/com/launchdarkly/client/LDClientEventTest.java b/src/test/java/com/launchdarkly/client/LDClientEventTest.java index caf90b6fe..9e77aa0ec 100644 --- a/src/test/java/com/launchdarkly/client/LDClientEventTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientEventTest.java @@ -31,7 +31,7 @@ public class LDClientEventTest { private LDConfig config = new LDConfig.Builder() .dataStore(specificFeatureStore(featureStore)) .eventProcessor(specificEventProcessor(eventSink)) - .dataSource(Components.nullDataSource()) + .dataSource(Components.externalUpdatesOnly()) .build(); private LDClientInterface client = new LDClient("SDK_KEY", config); diff --git a/src/test/java/com/launchdarkly/client/LDClientLddModeTest.java b/src/test/java/com/launchdarkly/client/LDClientExternalUpdatesOnlyTest.java similarity index 50% rename from src/test/java/com/launchdarkly/client/LDClientLddModeTest.java rename to src/test/java/com/launchdarkly/client/LDClientExternalUpdatesOnlyTest.java index 21a142823..414fd628a 100644 --- a/src/test/java/com/launchdarkly/client/LDClientLddModeTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientExternalUpdatesOnlyTest.java @@ -14,7 +14,51 @@ import static org.junit.Assert.assertTrue; @SuppressWarnings("javadoc") -public class LDClientLddModeTest { +public class LDClientExternalUpdatesOnlyTest { + @Test + public void externalUpdatesOnlyClientHasNullUpdateProcessor() throws Exception { + LDConfig config = new LDConfig.Builder() + .dataSource(Components.externalUpdatesOnly()) + .build(); + try (LDClient client = new LDClient("SDK_KEY", config)) { + assertEquals(Components.NullUpdateProcessor.class, client.updateProcessor.getClass()); + } + } + + @Test + public void externalUpdatesOnlyClientHasDefaultEventProcessor() throws Exception { + LDConfig config = new LDConfig.Builder() + .dataSource(Components.externalUpdatesOnly()) + .build(); + try (LDClient client = new LDClient("SDK_KEY", config)) { + assertEquals(DefaultEventProcessor.class, client.eventProcessor.getClass()); + } + } + + @Test + public void externalUpdatesOnlyClientIsInitialized() throws Exception { + LDConfig config = new LDConfig.Builder() + .dataSource(Components.externalUpdatesOnly()) + .build(); + try (LDClient client = new LDClient("SDK_KEY", config)) { + assertTrue(client.initialized()); + } + } + + @Test + public void externalUpdatesOnlyClientGetsFlagFromFeatureStore() throws IOException { + FeatureStore testFeatureStore = initedFeatureStore(); + LDConfig config = new LDConfig.Builder() + .dataSource(Components.externalUpdatesOnly()) + .dataStore(specificFeatureStore(testFeatureStore)) + .build(); + FeatureFlag flag = flagWithValue("key", LDValue.of(true)); + testFeatureStore.upsert(FEATURES, flag); + try (LDClient client = new LDClient("SDK_KEY", config)) { + assertTrue(client.boolVariation("key", new LDUser("user"), false)); + } + } + @SuppressWarnings("deprecation") @Test public void lddModeClientHasNullUpdateProcessor() throws IOException { @@ -22,12 +66,13 @@ public void lddModeClientHasNullUpdateProcessor() throws IOException { .useLdd(true) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { - assertEquals(UpdateProcessor.NullUpdateProcessor.class, client.updateProcessor.getClass()); + assertEquals(Components.NullUpdateProcessor.class, client.updateProcessor.getClass()); } } @Test public void lddModeClientHasDefaultEventProcessor() throws IOException { + @SuppressWarnings("deprecation") LDConfig config = new LDConfig.Builder() .useLdd(true) .build(); @@ -38,6 +83,7 @@ public void lddModeClientHasDefaultEventProcessor() throws IOException { @Test public void lddModeClientIsInitialized() throws IOException { + @SuppressWarnings("deprecation") LDConfig config = new LDConfig.Builder() .useLdd(true) .build(); @@ -49,6 +95,7 @@ public void lddModeClientIsInitialized() throws IOException { @Test public void lddModeClientGetsFlagFromFeatureStore() throws IOException { FeatureStore testFeatureStore = initedFeatureStore(); + @SuppressWarnings("deprecation") LDConfig config = new LDConfig.Builder() .useLdd(true) .dataStore(specificFeatureStore(testFeatureStore)) diff --git a/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java b/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java index 2785fc5d1..20819fee0 100644 --- a/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientOfflineTest.java @@ -21,14 +21,13 @@ public class LDClientOfflineTest { private static final LDUser user = new LDUser("user"); - @SuppressWarnings("deprecation") @Test public void offlineClientHasNullUpdateProcessor() throws IOException { LDConfig config = new LDConfig.Builder() .offline(true) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { - assertEquals(UpdateProcessor.NullUpdateProcessor.class, client.updateProcessor.getClass()); + assertEquals(Components.NullUpdateProcessor.class, client.updateProcessor.getClass()); } } diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java index ae4a20bd1..3c83cdeb9 100644 --- a/src/test/java/com/launchdarkly/client/LDClientTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientTest.java @@ -86,8 +86,7 @@ public void constructorThrowsExceptionForNullConfig() throws Exception { @Test public void clientHasDefaultEventProcessorIfSendEventsIsTrue() throws Exception { LDConfig config = new LDConfig.Builder() - .stream(false) - .baseURI(URI.create("/fake")) + .dataSource(Components.externalUpdatesOnly()) .startWaitMillis(0) .sendEvents(true) .build(); @@ -99,8 +98,7 @@ public void clientHasDefaultEventProcessorIfSendEventsIsTrue() throws Exception @Test public void clientHasNullEventProcessorIfSendEventsIsFalse() throws IOException { LDConfig config = new LDConfig.Builder() - .stream(false) - .baseURI(URI.create("/fake")) + .dataSource(Components.externalUpdatesOnly()) .startWaitMillis(0) .sendEvents(false) .build(); @@ -112,8 +110,7 @@ public void clientHasNullEventProcessorIfSendEventsIsFalse() throws IOException @Test public void streamingClientHasStreamProcessor() throws Exception { LDConfig config = new LDConfig.Builder() - .stream(true) - .streamURI(URI.create("http://fake")) + .dataSource(Components.streamingDataSource().baseUri(URI.create("http://fake"))) .startWaitMillis(0) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { @@ -124,8 +121,7 @@ public void streamingClientHasStreamProcessor() throws Exception { @Test public void pollingClientHasPollingProcessor() throws IOException { LDConfig config = new LDConfig.Builder() - .stream(false) - .baseURI(URI.create("http://fake")) + .dataSource(Components.pollingDataSource().baseUri(URI.create("http://fake"))) .startWaitMillis(0) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { diff --git a/src/test/java/com/launchdarkly/client/LDConfigTest.java b/src/test/java/com/launchdarkly/client/LDConfigTest.java index f7bfb689a..fbbc881d5 100644 --- a/src/test/java/com/launchdarkly/client/LDConfigTest.java +++ b/src/test/java/com/launchdarkly/client/LDConfigTest.java @@ -71,14 +71,16 @@ public void testProxyAuthPartialConfig() { @Test public void testMinimumPollingIntervalIsEnforcedProperly(){ + @SuppressWarnings("deprecation") LDConfig config = new LDConfig.Builder().pollingIntervalMillis(10L).build(); - assertEquals(30000L, config.pollingIntervalMillis); + assertEquals(30000L, config.deprecatedPollingIntervalMillis); } @Test public void testPollingIntervalIsEnforcedProperly(){ + @SuppressWarnings("deprecation") LDConfig config = new LDConfig.Builder().pollingIntervalMillis(30001L).build(); - assertEquals(30001L, config.pollingIntervalMillis); + assertEquals(30001L, config.deprecatedPollingIntervalMillis); } @Test diff --git a/src/test/java/com/launchdarkly/client/PollingProcessorTest.java b/src/test/java/com/launchdarkly/client/PollingProcessorTest.java index b143cc35f..f70663fb6 100644 --- a/src/test/java/com/launchdarkly/client/PollingProcessorTest.java +++ b/src/test/java/com/launchdarkly/client/PollingProcessorTest.java @@ -14,13 +14,15 @@ @SuppressWarnings("javadoc") public class PollingProcessorTest { + private static final long LENGTHY_INTERVAL = 60000; + @Test public void testConnectionOk() throws Exception { MockFeatureRequestor requestor = new MockFeatureRequestor(); requestor.allData = new FeatureRequestor.AllData(new HashMap(), new HashMap()); FeatureStore store = new InMemoryFeatureStore(); - try (PollingProcessor pollingProcessor = new PollingProcessor(LDConfig.DEFAULT, requestor, store)) { + try (PollingProcessor pollingProcessor = new PollingProcessor(requestor, store, LENGTHY_INTERVAL)) { Future initFuture = pollingProcessor.start(); initFuture.get(1000, TimeUnit.MILLISECONDS); assertTrue(pollingProcessor.initialized()); @@ -34,7 +36,7 @@ public void testConnectionProblem() throws Exception { requestor.ioException = new IOException("This exception is part of a test and yes you should be seeing it."); FeatureStore store = new InMemoryFeatureStore(); - try (PollingProcessor pollingProcessor = new PollingProcessor(LDConfig.DEFAULT, requestor, store)) { + try (PollingProcessor pollingProcessor = new PollingProcessor(requestor, store, LENGTHY_INTERVAL)) { Future initFuture = pollingProcessor.start(); try { initFuture.get(200L, TimeUnit.MILLISECONDS); @@ -80,7 +82,7 @@ public void http500ErrorIsRecoverable() throws Exception { private void testUnrecoverableHttpError(int status) throws Exception { MockFeatureRequestor requestor = new MockFeatureRequestor(); requestor.httpException = new HttpErrorException(status); - try (PollingProcessor pollingProcessor = new PollingProcessor(LDConfig.DEFAULT, requestor, new InMemoryFeatureStore())) { + try (PollingProcessor pollingProcessor = new PollingProcessor(requestor, new InMemoryFeatureStore(), LENGTHY_INTERVAL)) { long startTime = System.currentTimeMillis(); Future initFuture = pollingProcessor.start(); try { @@ -97,7 +99,7 @@ private void testUnrecoverableHttpError(int status) throws Exception { private void testRecoverableHttpError(int status) throws Exception { MockFeatureRequestor requestor = new MockFeatureRequestor(); requestor.httpException = new HttpErrorException(status); - try (PollingProcessor pollingProcessor = new PollingProcessor(LDConfig.DEFAULT, requestor, new InMemoryFeatureStore())) { + try (PollingProcessor pollingProcessor = new PollingProcessor(requestor, new InMemoryFeatureStore(), LENGTHY_INTERVAL)) { Future initFuture = pollingProcessor.start(); try { initFuture.get(200, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/launchdarkly/client/StreamProcessorTest.java b/src/test/java/com/launchdarkly/client/StreamProcessorTest.java index ff49176c3..66608462b 100644 --- a/src/test/java/com/launchdarkly/client/StreamProcessorTest.java +++ b/src/test/java/com/launchdarkly/client/StreamProcessorTest.java @@ -23,7 +23,6 @@ import static com.launchdarkly.client.TestHttpUtil.eventStreamResponse; import static com.launchdarkly.client.TestHttpUtil.makeStartedServer; -import static com.launchdarkly.client.TestUtil.specificFeatureStore; import static com.launchdarkly.client.VersionedDataKind.FEATURES; import static com.launchdarkly.client.VersionedDataKind.SEGMENTS; import static org.easymock.EasyMock.expect; @@ -54,7 +53,6 @@ public class StreamProcessorTest extends EasyMockSupport { "data: {\"data\":{\"flags\":{},\"segments\":{}}}\n\n"; private InMemoryFeatureStore featureStore; - private LDConfig.Builder configBuilder; private FeatureRequestor mockRequestor; private EventSource mockEventSource; private EventHandler eventHandler; @@ -65,39 +63,37 @@ public class StreamProcessorTest extends EasyMockSupport { @Before public void setup() { featureStore = new InMemoryFeatureStore(); - configBuilder = new LDConfig.Builder().dataStore(specificFeatureStore(featureStore)); mockRequestor = createStrictMock(FeatureRequestor.class); mockEventSource = createStrictMock(EventSource.class); } @Test public void streamUriHasCorrectEndpoint() { - LDConfig config = configBuilder.streamURI(STREAM_URI).build(); - createStreamProcessor(SDK_KEY, config).start(); + createStreamProcessor(STREAM_URI).start(); assertEquals(URI.create(STREAM_URI.toString() + "/all"), actualStreamUri); } @Test public void headersHaveAuthorization() { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); assertEquals(SDK_KEY, headers.get("Authorization")); } @Test public void headersHaveUserAgent() { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); assertEquals("JavaClient/" + LDClient.CLIENT_VERSION, headers.get("User-Agent")); } @Test public void headersHaveAccept() { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); assertEquals("text/event-stream", headers.get("Accept")); } @Test public void putCausesFeatureToBeStored() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); MessageEvent event = new MessageEvent("{\"data\":{\"flags\":{\"" + FEATURE1_KEY + "\":" + featureJson(FEATURE1_KEY, FEATURE1_VERSION) + "}," + "\"segments\":{}}}"); @@ -108,7 +104,7 @@ public void putCausesFeatureToBeStored() throws Exception { @Test public void putCausesSegmentToBeStored() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); MessageEvent event = new MessageEvent("{\"data\":{\"flags\":{},\"segments\":{\"" + SEGMENT1_KEY + "\":" + segmentJson(SEGMENT1_KEY, SEGMENT1_VERSION) + "}}}"); eventHandler.onMessage("put", event); @@ -118,27 +114,27 @@ public void putCausesSegmentToBeStored() throws Exception { @Test public void storeNotInitializedByDefault() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); assertFalse(featureStore.initialized()); } @Test public void putCausesStoreToBeInitialized() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); eventHandler.onMessage("put", emptyPutEvent()); assertTrue(featureStore.initialized()); } @Test public void processorNotInitializedByDefault() throws Exception { - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); sp.start(); assertFalse(sp.initialized()); } @Test public void putCausesProcessorToBeInitialized() throws Exception { - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); sp.start(); eventHandler.onMessage("put", emptyPutEvent()); assertTrue(sp.initialized()); @@ -146,14 +142,14 @@ public void putCausesProcessorToBeInitialized() throws Exception { @Test public void futureIsNotSetByDefault() throws Exception { - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); Future future = sp.start(); assertFalse(future.isDone()); } @Test public void putCausesFutureToBeSet() throws Exception { - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); Future future = sp.start(); eventHandler.onMessage("put", emptyPutEvent()); assertTrue(future.isDone()); @@ -161,7 +157,7 @@ public void putCausesFutureToBeSet() throws Exception { @Test public void patchUpdatesFeature() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); eventHandler.onMessage("put", emptyPutEvent()); String path = "/flags/" + FEATURE1_KEY; @@ -174,7 +170,7 @@ public void patchUpdatesFeature() throws Exception { @Test public void patchUpdatesSegment() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); eventHandler.onMessage("put", emptyPutEvent()); String path = "/segments/" + SEGMENT1_KEY; @@ -187,7 +183,7 @@ public void patchUpdatesSegment() throws Exception { @Test public void deleteDeletesFeature() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); eventHandler.onMessage("put", emptyPutEvent()); featureStore.upsert(FEATURES, FEATURE); @@ -201,7 +197,7 @@ public void deleteDeletesFeature() throws Exception { @Test public void deleteDeletesSegment() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); eventHandler.onMessage("put", emptyPutEvent()); featureStore.upsert(SEGMENTS, SEGMENT); @@ -215,7 +211,7 @@ public void deleteDeletesSegment() throws Exception { @Test public void indirectPutRequestsAndStoresFeature() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); setupRequestorToReturnAllDataWithFlag(FEATURE); replayAll(); @@ -226,7 +222,7 @@ public void indirectPutRequestsAndStoresFeature() throws Exception { @Test public void indirectPutInitializesStore() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); setupRequestorToReturnAllDataWithFlag(FEATURE); replayAll(); @@ -237,7 +233,7 @@ public void indirectPutInitializesStore() throws Exception { @Test public void indirectPutInitializesProcessor() throws Exception { - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); sp.start(); setupRequestorToReturnAllDataWithFlag(FEATURE); replayAll(); @@ -249,7 +245,7 @@ public void indirectPutInitializesProcessor() throws Exception { @Test public void indirectPutSetsFuture() throws Exception { - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); Future future = sp.start(); setupRequestorToReturnAllDataWithFlag(FEATURE); replayAll(); @@ -261,7 +257,7 @@ public void indirectPutSetsFuture() throws Exception { @Test public void indirectPatchRequestsAndUpdatesFeature() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); expect(mockRequestor.getFlag(FEATURE1_KEY)).andReturn(FEATURE); replayAll(); @@ -273,7 +269,7 @@ public void indirectPatchRequestsAndUpdatesFeature() throws Exception { @Test public void indirectPatchRequestsAndUpdatesSegment() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); expect(mockRequestor.getSegment(SEGMENT1_KEY)).andReturn(SEGMENT); replayAll(); @@ -285,13 +281,13 @@ public void indirectPatchRequestsAndUpdatesSegment() throws Exception { @Test public void unknownEventTypeDoesNotThrowException() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); eventHandler.onMessage("what", new MessageEvent("")); } @Test public void streamWillReconnectAfterGeneralIOException() throws Exception { - createStreamProcessor(SDK_KEY, configBuilder.build()).start(); + createStreamProcessor(STREAM_URI).start(); ConnectionErrorHandler.Action action = errorHandler.onConnectionError(new IOException()); assertEquals(ConnectionErrorHandler.Action.PROCEED, action); } @@ -336,12 +332,7 @@ public void httpClientDoesNotAllowSelfSignedCertByDefault() throws Exception { try (TestHttpUtil.ServerWithCert server = new TestHttpUtil.ServerWithCert()) { server.server.enqueue(eventStreamResponse(STREAM_RESPONSE_WITH_EMPTY_DATA)); - LDConfig config = new LDConfig.Builder() - .streamURI(server.uri()) - .build(); - - try (StreamProcessor sp = new StreamProcessor("sdk-key", config, - mockRequestor, featureStore, null)) { + try (StreamProcessor sp = createStreamProcessorWithRealHttp(LDConfig.DEFAULT, server.uri())) { sp.connectionErrorHandler = errorSink; Future ready = sp.start(); ready.get(); @@ -360,12 +351,10 @@ public void httpClientCanUseCustomTlsConfig() throws Exception { server.server.enqueue(eventStreamResponse(STREAM_RESPONSE_WITH_EMPTY_DATA)); LDConfig config = new LDConfig.Builder() - .streamURI(server.uri()) .sslSocketFactory(server.sslClient.socketFactory, server.sslClient.trustManager) // allows us to trust the self-signed cert .build(); - try (StreamProcessor sp = new StreamProcessor("sdk-key", config, - mockRequestor, featureStore, null)) { + try (StreamProcessor sp = createStreamProcessorWithRealHttp(config, server.uri())) { sp.connectionErrorHandler = errorSink; Future ready = sp.start(); ready.get(); @@ -382,13 +371,11 @@ public void httpClientCanUseProxyConfig() throws Exception { try (MockWebServer server = makeStartedServer(eventStreamResponse(STREAM_RESPONSE_WITH_EMPTY_DATA))) { HttpUrl serverUrl = server.url("/"); LDConfig config = new LDConfig.Builder() - .streamURI(fakeStreamUri) .proxyHost(serverUrl.host()) .proxyPort(serverUrl.port()) .build(); - try (StreamProcessor sp = new StreamProcessor("sdk-key", config, - mockRequestor, featureStore, null)) { + try (StreamProcessor sp = createStreamProcessorWithRealHttp(config, fakeStreamUri)) { sp.connectionErrorHandler = errorSink; Future ready = sp.start(); ready.get(); @@ -411,7 +398,7 @@ public Action onConnectionError(Throwable t) { private void testUnrecoverableHttpError(int status) throws Exception { UnsuccessfulResponseException e = new UnsuccessfulResponseException(status); long startTime = System.currentTimeMillis(); - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); Future initFuture = sp.start(); ConnectionErrorHandler.Action action = errorHandler.onConnectionError(e); @@ -430,7 +417,7 @@ private void testUnrecoverableHttpError(int status) throws Exception { private void testRecoverableHttpError(int status) throws Exception { UnsuccessfulResponseException e = new UnsuccessfulResponseException(status); long startTime = System.currentTimeMillis(); - StreamProcessor sp = createStreamProcessor(SDK_KEY, configBuilder.build()); + StreamProcessor sp = createStreamProcessor(STREAM_URI); Future initFuture = sp.start(); ConnectionErrorHandler.Action action = errorHandler.onConnectionError(e); @@ -446,10 +433,20 @@ private void testRecoverableHttpError(int status) throws Exception { assertFalse(sp.initialized()); } - private StreamProcessor createStreamProcessor(String sdkKey, LDConfig config) { - return new StreamProcessor(sdkKey, config, mockRequestor, featureStore, new StubEventSourceCreator()); + private StreamProcessor createStreamProcessor(LDConfig config, URI streamUri) { + return new StreamProcessor(SDK_KEY, config, mockRequestor, featureStore, new StubEventSourceCreator(), + streamUri, config.deprecatedReconnectTimeMs); } - + + private StreamProcessor createStreamProcessor(URI streamUri) { + return createStreamProcessor(LDConfig.DEFAULT, streamUri); + } + + private StreamProcessor createStreamProcessorWithRealHttp(LDConfig config, URI streamUri) { + return new StreamProcessor(SDK_KEY, config, mockRequestor, featureStore, null, + streamUri, config.deprecatedReconnectTimeMs); + } + private String featureJson(String key, int version) { return "{\"key\":\"" + key + "\",\"version\":" + version + ",\"on\":true}"; } @@ -477,8 +474,8 @@ private void assertSegmentInStore(Segment segment) { } private class StubEventSourceCreator implements StreamProcessor.EventSourceCreator { - public EventSource createEventSource(LDConfig config, EventHandler handler, URI streamUri, ConnectionErrorHandler errorHandler, - Headers headers) { + public EventSource createEventSource(LDConfig config, EventHandler handler, URI streamUri, + long initialReconnectDelay, ConnectionErrorHandler errorHandler, Headers headers) { StreamProcessorTest.this.eventHandler = handler; StreamProcessorTest.this.actualStreamUri = streamUri; StreamProcessorTest.this.errorHandler = errorHandler; diff --git a/src/test/java/com/launchdarkly/client/TestHttpUtil.java b/src/test/java/com/launchdarkly/client/TestHttpUtil.java index 8f86c7b1b..035ff814e 100644 --- a/src/test/java/com/launchdarkly/client/TestHttpUtil.java +++ b/src/test/java/com/launchdarkly/client/TestHttpUtil.java @@ -1,5 +1,8 @@ package com.launchdarkly.client; +import com.launchdarkly.client.integrations.PollingDataSourceBuilder; +import com.launchdarkly.client.integrations.StreamingDataSourceBuilder; + import java.io.Closeable; import java.io.IOException; import java.net.InetAddress; @@ -30,12 +33,12 @@ static ServerWithCert httpsServerWithSelfSignedCert(MockResponse... responses) t return ret; } - static LDConfig.Builder baseConfig(MockWebServer server) { - URI uri = server.url("").uri(); - return new LDConfig.Builder() - .baseURI(uri) - .streamURI(uri) - .eventsURI(uri); + static StreamingDataSourceBuilder baseStreamingConfig(MockWebServer server) { + return Components.streamingDataSource().baseUri(server.url("").uri()); + } + + static PollingDataSourceBuilder basePollingConfig(MockWebServer server) { + return Components.pollingDataSource().baseUri(server.url("").uri()); } static MockResponse jsonResponse(String body) {