From aeb88c27ea84022dc3ae8ce2d3e5946ebc4621b3 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Mon, 4 Apr 2016 14:07:32 -0700 Subject: [PATCH 01/13] bump eventsource dep --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 676e19717..899bb477b 100644 --- a/build.gradle +++ b/build.gradle @@ -24,7 +24,7 @@ dependencies { compile "com.google.code.gson:gson:2.2.4" compile "com.google.guava:guava:19.0" compile "org.slf4j:slf4j-api:1.7.7" - compile "com.launchdarkly:okhttp-eventsource:0.1.0" + compile "com.launchdarkly:okhttp-eventsource:0.1.2" compile "redis.clients:jedis:2.8.0" testCompile "org.easymock:easymock:3.3" testCompile 'junit:junit:[4.10,)' From e1ac5f8c646c86e303a313f9aed11e8391dc0e2f Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Tue, 5 Apr 2016 17:00:05 -0700 Subject: [PATCH 02/13] Implement bounded resource consumption. --- .../client/InMemoryFeatureStore.java | 6 +- .../com/launchdarkly/client/LDClient.java | 86 +++++++---- .../com/launchdarkly/client/LDConfig.java | 63 ++++++-- .../launchdarkly/client/PollingProcessor.java | 62 ++++++++ .../launchdarkly/client/StreamProcessor.java | 34 +++-- .../launchdarkly/client/UpdateProcessor.java | 23 +++ .../launchdarkly/client/VeryBasicFuture.java | 30 ++++ .../com/launchdarkly/client/LDClientTest.java | 143 +++++++++++++++--- .../com/launchdarkly/client/LDConfigTest.java | 18 ++- .../client/PollingProcessorTest.java | 61 ++++++++ 10 files changed, 445 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/launchdarkly/client/PollingProcessor.java create mode 100644 src/main/java/com/launchdarkly/client/UpdateProcessor.java create mode 100644 src/main/java/com/launchdarkly/client/VeryBasicFuture.java create mode 100644 src/test/java/com/launchdarkly/client/PollingProcessorTest.java diff --git a/src/main/java/com/launchdarkly/client/InMemoryFeatureStore.java b/src/main/java/com/launchdarkly/client/InMemoryFeatureStore.java index 9a5d2f6a3..a4837955f 100644 --- a/src/main/java/com/launchdarkly/client/InMemoryFeatureStore.java +++ b/src/main/java/com/launchdarkly/client/InMemoryFeatureStore.java @@ -12,9 +12,9 @@ */ public class InMemoryFeatureStore implements FeatureStore { - final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - final Map> features = new HashMap<>(); - volatile boolean initialized = false; + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + private final Map> features = new HashMap<>(); + private volatile boolean initialized = false; /** diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index a5f5bdff4..3cf43ff77 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.common.annotations.VisibleForTesting; import com.google.gson.JsonElement; import org.apache.http.annotation.ThreadSafe; import org.slf4j.Logger; @@ -9,14 +10,15 @@ import java.io.Closeable; import java.io.IOException; import java.net.URL; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.jar.Attributes; import java.util.jar.Manifest; /** - * * A client for the LaunchDarkly API. Client instances are thread-safe. Applications should instantiate * a single {@code LDClient} for the lifetime of their application. - * */ @ThreadSafe public class LDClient implements Closeable { @@ -24,7 +26,7 @@ public class LDClient implements Closeable { private final LDConfig config; private final FeatureRequestor requestor; private final EventProcessor eventProcessor; - private final StreamProcessor streamProcessor; + private UpdateProcessor updateProcessor; protected static final String CLIENT_VERSION = getClientVersion(); private volatile boolean offline = false; @@ -33,46 +35,80 @@ public class LDClient implements Closeable { * Creates a new client instance that connects to LaunchDarkly with the default configuration. In most * cases, you should use this constructor. * - * @param apiKey the API key for your account + * @param apiKey the API key for your account + * @param waitForMillis when set to greater than zero allows callers to block until the client + * has connected to LaunchDarkly and is properly initialized */ - public LDClient(String apiKey) { - this(apiKey, LDConfig.DEFAULT); + public LDClient(String apiKey, Long waitForMillis) { + this(apiKey, LDConfig.DEFAULT, waitForMillis); } /** * Creates a new client to connect to LaunchDarkly with a custom configuration. This constructor * can be used to configure advanced client features, such as customizing the LaunchDarkly base URL. * - * @param apiKey the API key for your account - * @param config a client configuration object + * @param apiKey the API key for your account + * @param config a client configuration object + * @param waitForMillis when set to greater than zero allows callers to block until the client + * has connected to LaunchDarkly and is properly initialized */ - public LDClient(String apiKey, LDConfig config) { + public LDClient(String apiKey, LDConfig config, Long waitForMillis) { this.config = config; this.requestor = createFeatureRequestor(apiKey, config); this.eventProcessor = createEventProcessor(apiKey, config); + if (config.offline || config.useLdd) { + logger.info("Starting LaunchDarkly client in offline mode"); + setOffline(); + return; + } + if (config.stream) { - logger.debug("Enabling streaming API"); - this.streamProcessor = createStreamProcessor(apiKey, config, requestor); - this.streamProcessor.subscribe(); + logger.info("Enabling streaming API"); + this.updateProcessor = createStreamProcessor(apiKey, config, requestor); } else { - logger.debug("Streaming API disabled"); - this.streamProcessor = null; + logger.info("Disabling streaming API"); + this.updateProcessor = createPollingProcessor(config); + } + + Future startFuture = updateProcessor.start(); + + if (waitForMillis > 0L) { + logger.info("Waiting up to " + waitForMillis + " milliseconds for LaunchDarkly client to start..."); + try { + startFuture.get(waitForMillis, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + logger.error("Timeout encountered waiting for LaunchDarkly client initialization"); + } catch (Exception e) { + logger.error("Exception encountered waiting for LaunchDarkly client initialization", e); + } } } + public boolean initialized() { + return isOffline() || config.useLdd || updateProcessor.initialized(); + } + + @VisibleForTesting protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { return new FeatureRequestor(apiKey, config); } + @VisibleForTesting protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { return new EventProcessor(apiKey, config); } + @VisibleForTesting protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { return new StreamProcessor(apiKey, config, requestor); } + @VisibleForTesting + protected PollingProcessor createPollingProcessor(LDConfig config) { + return new PollingProcessor(config, requestor); + } + /** * Tracks that a user performed an event. @@ -145,24 +181,11 @@ public boolean getFlag(String featureKey, LDUser user, boolean defaultValue) { * @return whether or not the flag should be enabled, or {@code defaultValue} if the flag is disabled in the LaunchDarkly control panel */ public boolean toggle(String featureKey, LDUser user, boolean defaultValue) { - if (this.offline) { + if (!initialized()) { return defaultValue; } try { - FeatureRep result; - if (this.config.stream && this.streamProcessor != null && this.streamProcessor.initialized()) { - logger.debug("Using feature flag stored from streaming API"); - result = (FeatureRep) this.streamProcessor.getFeature(featureKey); - if (config.debugStreaming) { - FeatureRep pollingResult = requestor.makeRequest(featureKey, true); - if (!result.equals(pollingResult)) { - logger.warn("Mismatch between streaming and polling feature! Streaming: {} Polling: {}", result, pollingResult); - } - } - } else { - // If streaming is enabled, always get the latest version of the feature while polling - result = requestor.makeRequest(featureKey, this.config.stream); - } + FeatureRep result = (FeatureRep) config.featureStore.get(featureKey); if (result == null) { logger.warn("Unknown feature flag " + featureKey + "; returning default value"); sendFlagRequestEvent(featureKey, user, defaultValue, defaultValue); @@ -195,8 +218,8 @@ public boolean toggle(String featureKey, LDUser user, boolean defaultValue) { @Override public void close() throws IOException { this.eventProcessor.close(); - if (this.streamProcessor != null) { - this.streamProcessor.close(); + if (this.updateProcessor != null) { + this.updateProcessor.close(); } } @@ -226,7 +249,6 @@ public void setOnline() { } /** - * * @return whether the client is in offline mode */ public boolean isOffline() { diff --git a/src/main/java/com/launchdarkly/client/LDConfig.java b/src/main/java/com/launchdarkly/client/LDConfig.java index d1e25928b..0a7757991 100644 --- a/src/main/java/com/launchdarkly/client/LDConfig.java +++ b/src/main/java/com/launchdarkly/client/LDConfig.java @@ -21,6 +21,7 @@ public final class LDConfig { private static final int DEFAULT_CONNECT_TIMEOUT = 2000; private static final int DEFAULT_SOCKET_TIMEOUT = 10000; private static final int DEFAULT_FLUSH_INTERVAL = 5; + private static final long DEFAULT_POLLING_INTERVAL_MILLIS = 1000L; private static final Logger logger = LoggerFactory.getLogger(LDConfig.class); protected static final LDConfig DEFAULT = new Builder().build(); @@ -37,6 +38,8 @@ public final class LDConfig { final boolean debugStreaming; final FeatureStore featureStore; final boolean useLdd; + final boolean offline; + final long pollingIntervalMillis; protected LDConfig(Builder builder) { this.baseURI = builder.baseURI; @@ -51,21 +54,26 @@ protected LDConfig(Builder builder) { this.debugStreaming = builder.debugStreaming; this.featureStore = builder.featureStore; this.useLdd = builder.useLdd; + this.offline = builder.offline; + if (builder.pollingIntervalMillis < DEFAULT_POLLING_INTERVAL_MILLIS) { + this.pollingIntervalMillis = DEFAULT_POLLING_INTERVAL_MILLIS; + } else { + this.pollingIntervalMillis = builder.pollingIntervalMillis; + } } /** * A builder that helps construct {@link com.launchdarkly.client.LDConfig} objects. Builder * calls can be chained, enabling the following pattern: - * + * *
    * LDConfig config = new LDConfig.Builder()
    *      .connectTimeout(3)
    *      .socketTimeout(3)
    *      .build()
    * 
- * */ - public static class Builder{ + public static class Builder { private URI baseURI = DEFAULT_BASE_URI; private URI eventsURI = DEFAULT_EVENTS_URI; private URI streamURI = DEFAULT_STREAM_URI; @@ -79,6 +87,8 @@ public static class Builder{ private boolean stream = true; private boolean debugStreaming = false; private boolean useLdd = false; + private boolean offline = false; + private long pollingIntervalMillis = DEFAULT_POLLING_INTERVAL_MILLIS; private FeatureStore featureStore = new InMemoryFeatureStore(); /** @@ -231,6 +241,7 @@ public Builder capacity(int capacity) { * If none of {@link #proxyHost(String)}, {@link #proxyPort(int)} or {@link #proxyScheme(String)} are specified, * a proxy will not be used, and {@link LDClient} will connect to LaunchDarkly directly. *

+ * * @param host * @return the builder */ @@ -242,11 +253,12 @@ public Builder proxyHost(String host) { /** * Set the port to use for an HTTP proxy for making connections to LaunchDarkly. If not set (but {@link #proxyHost(String)} * or {@link #proxyScheme(String)} are specified, the default port for the scheme will be used. - * + *

*

* If none of {@link #proxyHost(String)}, {@link #proxyPort(int)} or {@link #proxyScheme(String)} are specified, * a proxy will not be used, and {@link LDClient} will connect to LaunchDarkly directly. *

+ * * @param port * @return the builder */ @@ -258,11 +270,12 @@ public Builder proxyPort(int port) { /** * Set the scheme to use for an HTTP proxy for making connections to LaunchDarkly. If not set (but {@link #proxyHost(String)} * or {@link #proxyPort(int)} are specified, the default https scheme will be used. - * + *

*

* If none of {@link #proxyHost(String)}, {@link #proxyPort(int)} or {@link #proxyScheme(String)} are specified, * a proxy will not be used, and {@link LDClient} will connect to LaunchDarkly directly. *

+ * * @param scheme * @return the builder */ @@ -274,6 +287,7 @@ public Builder proxyScheme(String scheme) { /** * Set whether this client should subscribe to the streaming API, or whether the LaunchDarkly daemon is in use * instead + * * @param useLdd * @return the builder */ @@ -282,6 +296,29 @@ public Builder useLdd(boolean useLdd) { return this; } + /** + * Set whether this client is offline. + * + * @param offline when set to true no calls to LaunchDarkly will be made. + * @return the builder + */ + public Builder offline(boolean offline) { + this.offline = offline; + return this; + } + + /** + * Set the polling interval (when streaming is disabled). Values less than {@value #DEFAULT_POLLING_INTERVAL_MILLIS} + * will be set to the default of {@value #DEFAULT_POLLING_INTERVAL_MILLIS} + * + * @param pollingIntervalMillis rule update polling interval in milliseconds. + * @return the builder + */ + public Builder pollingIntervalMillis(long pollingIntervalMillis) { + this.pollingIntervalMillis = pollingIntervalMillis; + return this; + } + HttpHost proxyHost() { if (this.proxyHost == null && this.proxyPort == -1 && this.proxyScheme == null) { return null; @@ -294,6 +331,7 @@ HttpHost proxyHost() { /** * Build the configured {@link com.launchdarkly.client.LDConfig} object + * * @return the {@link com.launchdarkly.client.LDConfig} configured by this builder */ public LDConfig build() { @@ -311,9 +349,9 @@ private URIBuilder getBuilder() { private URIBuilder getEventsBuilder() { return new URIBuilder() - .setScheme(eventsURI.getScheme()) - .setHost(eventsURI.getHost()) - .setPort(eventsURI.getPort()); + .setScheme(eventsURI.getScheme()) + .setHost(eventsURI.getHost()) + .setPort(eventsURI.getPort()); } HttpGet getRequest(String apiKey, String path) { @@ -325,8 +363,7 @@ HttpGet getRequest(String apiKey, String path) { request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION); return request; - } - catch (Exception e) { + } catch (Exception e) { logger.error("Unhandled exception in LaunchDarkly client", e); return null; } @@ -341,8 +378,7 @@ HttpPost postRequest(String apiKey, String path) { request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION); return request; - } - catch (Exception e) { + } catch (Exception e) { logger.error("Unhandled exception in LaunchDarkly client", e); return null; } @@ -357,8 +393,7 @@ HttpPost postEventsRequest(String apiKey, String path) { request.addHeader("User-Agent", "JavaClient/" + LDClient.CLIENT_VERSION); return request; - } - catch (Exception e) { + } catch (Exception e) { logger.error("Unhandled exception in LaunchDarkly client", e); return null; } diff --git a/src/main/java/com/launchdarkly/client/PollingProcessor.java b/src/main/java/com/launchdarkly/client/PollingProcessor.java new file mode 100644 index 000000000..abf31ae57 --- /dev/null +++ b/src/main/java/com/launchdarkly/client/PollingProcessor.java @@ -0,0 +1,62 @@ +package com.launchdarkly.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public 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 volatile boolean initialized = false; + private ScheduledExecutorService scheduler = null; + + PollingProcessor(LDConfig config, FeatureRequestor requestor) { + this.requestor = requestor; + this.config = config; + this.store = config.featureStore; + } + + @Override + public boolean initialized() { + return initialized && config.featureStore.initialized(); + } + + @Override + public void close() throws IOException { + scheduler.shutdown(); + } + + @Override + public Future start() { + logger.info("Starting LaunchDarkly polling client with interval: " + + config.pollingIntervalMillis + " milliseconds"); + final VeryBasicFuture initFuture = new VeryBasicFuture(); + scheduler = Executors.newScheduledThreadPool(1); + + scheduler.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + try { + store.init(requestor.makeAllRequest(true)); + if (!initialized) { + logger.info("Initialized LaunchDarkly client."); + initialized = true; + initFuture.completed(null); + } + } catch (IOException e) { + logger.error("Encountered exception in LaunchDarkly client when retrieving update", e); + } + } + }, 0L, config.pollingIntervalMillis, TimeUnit.MILLISECONDS); + + return initFuture; + } +} diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index 951920afd..27da2f6d4 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -2,20 +2,20 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import com.launchdarkly.eventsource.EventHandler; +import com.launchdarkly.eventsource.EventSource; import com.launchdarkly.eventsource.MessageEvent; import okhttp3.Headers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.Closeable; import java.io.IOException; import java.lang.reflect.Type; import java.net.URI; import java.util.Map; -import com.launchdarkly.eventsource.EventSource; -import com.launchdarkly.eventsource.EventHandler; +import java.util.concurrent.Future; -class StreamProcessor implements Closeable { +class StreamProcessor implements UpdateProcessor { private static final String PUT = "put"; private static final String PATCH = "patch"; private static final String DELETE = "delete"; @@ -28,6 +28,7 @@ class StreamProcessor implements Closeable { private final String apiKey; private final FeatureRequestor requestor; private EventSource es; + private volatile boolean initialized = false; StreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { @@ -37,11 +38,9 @@ class StreamProcessor implements Closeable { this.requestor = requestor; } - void subscribe() { - // If the LaunchDarkly daemon is to be used, then do not subscribe to the stream - if (config.useLdd) { - return; - } + @Override + public Future start() { + final VeryBasicFuture initFuture = new VeryBasicFuture(); Headers headers = new Headers.Builder() .add("Authorization", "api_key " + this.apiKey) @@ -63,6 +62,11 @@ public void onMessage(String name, MessageEvent event) throws Exception { Type type = new TypeToken>>(){}.getType(); Map> features = gson.fromJson(event.getData(), type); store.init(features); + if (!initialized) { + initialized = true; + initFuture.completed(null); + logger.info("Initialized LaunchDarkly client."); + } } else if (name.equals(PATCH)) { FeaturePatchData data = gson.fromJson(event.getData(), FeaturePatchData.class); @@ -76,6 +80,11 @@ else if (name.equals(INDIRECT_PUT)) { try { Map> features = requestor.makeAllRequest(true); store.init(features); + if (!initialized) { + initialized = true; + initFuture.completed(null); + logger.info("Initialized LaunchDarkly client."); + } } catch (IOException e) { logger.error("Encountered exception in LaunchDarkly client", e); } @@ -106,7 +115,7 @@ public void onError(Throwable throwable) { .build(); es.start(); - + return initFuture; } @Override @@ -119,8 +128,9 @@ public void close() throws IOException { } } - boolean initialized() { - return store.initialized(); + @Override + public boolean initialized() { + return initialized && store.initialized(); } FeatureRep getFeature(String key) { diff --git a/src/main/java/com/launchdarkly/client/UpdateProcessor.java b/src/main/java/com/launchdarkly/client/UpdateProcessor.java new file mode 100644 index 000000000..d5d2876f7 --- /dev/null +++ b/src/main/java/com/launchdarkly/client/UpdateProcessor.java @@ -0,0 +1,23 @@ +package com.launchdarkly.client; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.Future; + +public interface UpdateProcessor extends Closeable { + + /** + * Starts the client. + * @return {@link Future}'s completion status indicates the client has been initialized. + */ + Future start(); + + /** + * Returns true once the client has been initialized and will never return false again. + * @return + */ + boolean initialized(); + + + void close() throws IOException; +} diff --git a/src/main/java/com/launchdarkly/client/VeryBasicFuture.java b/src/main/java/com/launchdarkly/client/VeryBasicFuture.java new file mode 100644 index 000000000..6bcab2811 --- /dev/null +++ b/src/main/java/com/launchdarkly/client/VeryBasicFuture.java @@ -0,0 +1,30 @@ +package com.launchdarkly.client; + +import org.apache.http.concurrent.BasicFuture; +import org.apache.http.concurrent.FutureCallback; + +import java.util.concurrent.Future; + +/** + * Very Basic {@link Future} implementation extending {@link BasicFuture} with no callback or return value. + */ +public class VeryBasicFuture extends BasicFuture { + + public VeryBasicFuture() { + super(new NoOpFutureCallback()); + } + + static class NoOpFutureCallback implements FutureCallback { + @Override + public void completed(Void result) { + } + + @Override + public void failed(Exception ex) { + } + + @Override + public void cancelled() { + } + } +} diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java index bf8d0a587..26968d05e 100644 --- a/src/test/java/com/launchdarkly/client/LDClientTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientTest.java @@ -1,36 +1,145 @@ package com.launchdarkly.client; -import static org.easymock.EasyMock.*; -import static org.junit.Assert.assertEquals; - -import org.apache.http.impl.client.CloseableHttpClient; -import org.easymock.*; +import org.easymock.EasyMockSupport; +import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.easymock.EasyMock.expect; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; public class LDClientTest extends EasyMockSupport { + private FeatureRequestor requestor; + private StreamProcessor streamProcessor; + private PollingProcessor pollingProcessor; + private Future initFuture; + + @Before + public void before() { + requestor = createStrictMock(FeatureRequestor.class); + streamProcessor = createStrictMock(StreamProcessor.class); + pollingProcessor = createStrictMock(PollingProcessor.class); + initFuture = createStrictMock(Future.class); + } + + @Test + public void testOfflineDoesNotConnect() throws IOException { + LDConfig config = new LDConfig.Builder() + .offline(true) + .build(); + + LDClient client = createMockClient(config, 0L); + replayAll(); + + assertDefaultValueIsReturned(client); + assertTrue(client.initialized()); + verifyAll(); + } + + @Test + public void testUseLddDoesNotConnect() throws IOException { + LDConfig config = new LDConfig.Builder() + .useLdd(true) + .build(); + + LDClient client = createMockClient(config, 0L); + replayAll(); + + assertDefaultValueIsReturned(client); + assertTrue(client.initialized()); + verifyAll(); + } + + @Test + public void testStreamingNoWait() throws IOException { + LDConfig config = new LDConfig.Builder() + .stream(true) + .build(); - private CloseableHttpClient httpClient = createMock(CloseableHttpClient.class); + expect(streamProcessor.start()).andReturn(initFuture); + expect(streamProcessor.initialized()).andReturn(false); + replayAll(); - private FeatureRequestor requestor = createMock(FeatureRequestor.class); + LDClient client = createMockClient(config, 0L); + assertDefaultValueIsReturned(client); - LDClient client = new LDClient("API_KEY") { + verifyAll(); + } + + @Test + public void testStreamingWait() throws Exception { + LDConfig config = new LDConfig.Builder() + .stream(true) + .build(); + + expect(streamProcessor.start()).andReturn(initFuture); + expect(initFuture.get(10L, TimeUnit.MILLISECONDS)).andThrow(new TimeoutException()); + replayAll(); + + LDClient client = createMockClient(config, 10L); + verifyAll(); + } + + @Test + public void testPollingNoWait() throws IOException { + LDConfig config = new LDConfig.Builder() + .stream(false) + .build(); + + expect(pollingProcessor.start()).andReturn(initFuture); + expect(pollingProcessor.initialized()).andReturn(false); + replayAll(); - @Override - protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { - return requestor; - } - }; + LDClient client = createMockClient(config, 0L); + assertDefaultValueIsReturned(client); + + verifyAll(); + } @Test - public void testExceptionThrownByHttpClientReturnsDefaultValue() throws IOException { + public void testPollingWait() throws Exception { + LDConfig config = new LDConfig.Builder() + .stream(false) + .build(); - expect(requestor.makeRequest(anyString(), anyBoolean())).andThrow(new IOException()); - replay(requestor); + expect(pollingProcessor.start()).andReturn(initFuture); + expect(initFuture.get(10L, TimeUnit.MILLISECONDS)).andThrow(new TimeoutException()); + replayAll(); + LDClient client = createMockClient(config, 10L); + verifyAll(); + } + + private void assertDefaultValueIsReturned(LDClient client) { boolean result = client.toggle("test", new LDUser("test.key"), true); assertEquals(true, result); - verify(requestor); + } + + private LDClient createMockClient( + LDConfig config, + Long waitForMillis + ) { + return new LDClient("API_KEY", config, waitForMillis) { + + @Override + protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { + return requestor; + } + + @Override + protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { + return streamProcessor; + } + + @Override + protected PollingProcessor createPollingProcessor(LDConfig config) { + return pollingProcessor; + } + }; } } diff --git a/src/test/java/com/launchdarkly/client/LDConfigTest.java b/src/test/java/com/launchdarkly/client/LDConfigTest.java index de22de994..d5078750c 100644 --- a/src/test/java/com/launchdarkly/client/LDConfigTest.java +++ b/src/test/java/com/launchdarkly/client/LDConfigTest.java @@ -1,13 +1,13 @@ package com.launchdarkly.client; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - import org.apache.http.client.methods.HttpPost; import org.junit.Test; import java.net.URI; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + public class LDConfigTest { @Test public void testConnectTimeoutSpecifiedInSeconds() { @@ -100,4 +100,16 @@ public void testCustomEventsUriIsConstructedProperly(){ HttpPost post = config.postEventsRequest("dummy-api-key", "/bulk"); assertEquals("http://localhost:3000/api/events/bulk", post.getURI().toString()); } + + @Test + public void testMinimumPollingIntervalIsEnforcedProperly(){ + LDConfig config = new LDConfig.Builder().pollingIntervalMillis(10L).build(); + assertEquals(1000L, config.pollingIntervalMillis); + } + + @Test + public void testPollingIntervalIsEnforcedProperly(){ + LDConfig config = new LDConfig.Builder().pollingIntervalMillis(10001L).build(); + assertEquals(10001L, config.pollingIntervalMillis); + } } \ No newline at end of file diff --git a/src/test/java/com/launchdarkly/client/PollingProcessorTest.java b/src/test/java/com/launchdarkly/client/PollingProcessorTest.java new file mode 100644 index 000000000..1b5cc05e6 --- /dev/null +++ b/src/test/java/com/launchdarkly/client/PollingProcessorTest.java @@ -0,0 +1,61 @@ +package com.launchdarkly.client; + +import org.easymock.EasyMockSupport; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.sql.Time; +import java.util.HashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +public class PollingProcessorTest extends EasyMockSupport { + @Test + public void testConnectionOk() throws Exception { + FeatureRequestor requestor = createStrictMock(FeatureRequestor.class); + PollingProcessor pollingProcessor = new PollingProcessor(LDConfig.DEFAULT, requestor); + + expect(requestor.makeAllRequest(true)) + .andReturn(new HashMap>()) + .once(); + replayAll(); + + Future initFuture = pollingProcessor.start(); + initFuture.get(100, TimeUnit.MILLISECONDS); + assertTrue(pollingProcessor.initialized()); + pollingProcessor.close(); + verifyAll(); + } + + @Test + public void testConnectionProblem() throws Exception { + FeatureRequestor requestor = createStrictMock(FeatureRequestor.class); + PollingProcessor pollingProcessor = new PollingProcessor(LDConfig.DEFAULT, requestor); + + expect(requestor.makeAllRequest(true)) + .andThrow(new IOException("This exception is part of a test and yes you should be seeing it.")) + .once(); + replayAll(); + + Future initFuture = pollingProcessor.start(); + try { + initFuture.get(100L, TimeUnit.MILLISECONDS); + fail("Expected Timeout, instead initFuture.get() returned."); + } catch (TimeoutException expected) { + } + assertFalse(initFuture.isDone()); + assertFalse(pollingProcessor.initialized()); + pollingProcessor.close(); + verifyAll(); + } +} From 8b0d5409ab0a7a02b0721056bc11b8c02045b06b Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Tue, 5 Apr 2016 20:31:46 -0700 Subject: [PATCH 03/13] Address PR comment. --- .../com/launchdarkly/client/LDClient.java | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index 3cf43ff77..75b2cf85a 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -28,8 +28,7 @@ public class LDClient implements Closeable { private final EventProcessor eventProcessor; private UpdateProcessor updateProcessor; protected static final String CLIENT_VERSION = getClientVersion(); - private volatile boolean offline = false; - + private final boolean offline; /** * Creates a new client instance that connects to LaunchDarkly with the default configuration. In most @@ -59,9 +58,10 @@ public LDClient(String apiKey, LDConfig config, Long waitForMillis) { if (config.offline || config.useLdd) { logger.info("Starting LaunchDarkly client in offline mode"); - setOffline(); + offline = true; return; } + offline = false; if (config.stream) { logger.info("Enabling streaming API"); @@ -230,24 +230,6 @@ public void flush() { this.eventProcessor.flush(); } - /** - * Puts the LaunchDarkly client in offline mode. - * In offline mode, all calls to {@link #toggle(String, LDUser, boolean)} will return the default value, and - * {@link #track(String, LDUser, com.google.gson.JsonElement)} will be a no-op. - * - */ - public void setOffline() { - this.offline = true; - } - - /** - * Puts the LaunchDarkly client in online mode. - * - */ - public void setOnline() { - this.offline = false; - } - /** * @return whether the client is in offline mode */ From 7f0fa1d1c44c6203e65ab3ea3ced4f721de8f125 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Tue, 5 Apr 2016 22:51:58 -0700 Subject: [PATCH 04/13] Adjust some Gradle things including adding the sonatype repo. --- build.gradle | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 899bb477b..cc7b25d2b 100644 --- a/build.gradle +++ b/build.gradle @@ -8,11 +8,14 @@ apply plugin: 'com.github.johnrengelman.shadow' repositories { mavenCentral() mavenLocal() + maven { + url "https://oss.sonatype.org/content/groups/public/" + } } allprojects { group = 'com.launchdarkly' - version = "0.20.0" + version = "0.21.0-SNAPSHOT" sourceCompatibility = 1.7 targetCompatibility = 1.7 } @@ -50,7 +53,7 @@ buildscript { } dependencies { classpath 'org.ajoberstar:gradle-git:0.12.0' - classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.2' + classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.3' } } @@ -125,12 +128,6 @@ uploadArchives { description 'Official LaunchDarkly SDK for Java' url 'https://github.com/launchdarkly/java-client' - scm { - connection 'scm:svn:http://foo.googlecode.com/svn/trunk/' - developerConnection 'scm:svn:https://foo.googlecode.com/svn/trunk/' - url 'http://foo.googlecode.com/svn/trunk/' - } - licenses { license { name 'The Apache License, Version 2.0' From 2d8df5ef0abbc758c5e0ae99fe76e702caf3de59 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 6 Apr 2016 10:38:26 -0700 Subject: [PATCH 05/13] Add comment to gradle file --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index cc7b25d2b..02fec00c9 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,8 @@ apply plugin: 'com.github.johnrengelman.shadow' repositories { mavenCentral() mavenLocal() + + // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: maven { url "https://oss.sonatype.org/content/groups/public/" } From be5cc255ac2b41248fa8dd884315c09c61d6edee Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 6 Apr 2016 13:45:55 -0700 Subject: [PATCH 06/13] Enhance log statement. --- src/main/java/com/launchdarkly/client/EventProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/launchdarkly/client/EventProcessor.java b/src/main/java/com/launchdarkly/client/EventProcessor.java index 713edb853..8243d70d7 100644 --- a/src/main/java/com/launchdarkly/client/EventProcessor.java +++ b/src/main/java/com/launchdarkly/client/EventProcessor.java @@ -104,7 +104,7 @@ private void postEvents(List events) { logger.debug("Successfully processed events"); } } catch (IOException e) { - logger.error("Unhandled exception in LaunchDarkly client", e); + logger.error("Unhandled exception in LaunchDarkly client attempting to connect to URI: " + config.eventsURI, e); } finally { try { if (response != null) response.close(); From 236075c4fe55055cb8c351525ce6bc143f22f8a6 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Wed, 6 Apr 2016 16:46:10 -0700 Subject: [PATCH 07/13] Address PR comments. --- build.gradle | 2 +- .../com/launchdarkly/client/LDClient.java | 42 ++++++++++++------ .../launchdarkly/client/PollingProcessor.java | 8 ++-- .../launchdarkly/client/StreamProcessor.java | 11 +++-- .../com/launchdarkly/client/LDClientTest.java | 44 +++++++++++++------ 5 files changed, 68 insertions(+), 39 deletions(-) diff --git a/build.gradle b/build.gradle index 02fec00c9..fafcd2b2b 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ dependencies { compile "org.slf4j:slf4j-api:1.7.7" compile "com.launchdarkly:okhttp-eventsource:0.1.2" compile "redis.clients:jedis:2.8.0" - testCompile "org.easymock:easymock:3.3" + testCompile "org.easymock:easymock:3.4" testCompile 'junit:junit:[4.10,)' testRuntime "org.slf4j:slf4j-simple:1.7.7" } diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index 75b2cf85a..6b93bf84d 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -28,7 +28,6 @@ public class LDClient implements Closeable { private final EventProcessor eventProcessor; private UpdateProcessor updateProcessor; protected static final String CLIENT_VERSION = getClientVersion(); - private final boolean offline; /** * Creates a new client instance that connects to LaunchDarkly with the default configuration. In most @@ -38,7 +37,7 @@ public class LDClient implements Closeable { * @param waitForMillis when set to greater than zero allows callers to block until the client * has connected to LaunchDarkly and is properly initialized */ - public LDClient(String apiKey, Long waitForMillis) { + public LDClient(String apiKey, long waitForMillis) { this(apiKey, LDConfig.DEFAULT, waitForMillis); } @@ -51,17 +50,20 @@ public LDClient(String apiKey, Long waitForMillis) { * @param waitForMillis when set to greater than zero allows callers to block until the client * has connected to LaunchDarkly and is properly initialized */ - public LDClient(String apiKey, LDConfig config, Long waitForMillis) { + public LDClient(String apiKey, LDConfig config, long waitForMillis) { this.config = config; this.requestor = createFeatureRequestor(apiKey, config); this.eventProcessor = createEventProcessor(apiKey, config); - if (config.offline || config.useLdd) { + if (config.offline) { logger.info("Starting LaunchDarkly client in offline mode"); - offline = true; return; } - offline = false; + + if (config.useLdd) { + logger.info("Starting LaunchDarkly in LDD mode. Skipping direct feature retrieval."); + return; + } if (config.stream) { logger.info("Enabling streaming API"); @@ -118,6 +120,9 @@ protected PollingProcessor createPollingProcessor(LDConfig config) { * @param data a JSON object containing additional data associated with the event */ public void track(String eventName, LDUser user, JsonElement data) { + if (isOffline()) { + return; + } boolean processed = eventProcessor.sendEvent(new CustomEvent(eventName, user, data)); if (!processed) { logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); @@ -131,7 +136,7 @@ public void track(String eventName, LDUser user, JsonElement data) { * @param user the user that performed the event */ public void track(String eventName, LDUser user) { - if (this.offline) { + if (isOffline()) { return; } track(eventName, user, null); @@ -142,7 +147,7 @@ public void track(String eventName, LDUser user) { * @param user the user to register */ public void identify(LDUser user) { - if (this.offline) { + if (isOffline()) { return; } boolean processed = eventProcessor.sendEvent(new IdentifyEvent(user)); @@ -152,6 +157,9 @@ public void identify(LDUser user) { } private void sendFlagRequestEvent(String featureKey, LDUser user, boolean value, boolean defaultValue) { + if (isOffline()) { + return; + } boolean processed = eventProcessor.sendEvent(new FeatureRequestEvent<>(featureKey, user, value, defaultValue)); if (!processed) { logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); @@ -181,28 +189,34 @@ public boolean getFlag(String featureKey, LDUser user, boolean defaultValue) { * @return whether or not the flag should be enabled, or {@code defaultValue} if the flag is disabled in the LaunchDarkly control panel */ public boolean toggle(String featureKey, LDUser user, boolean defaultValue) { + if (isOffline()) { + return defaultValue; + } + boolean value = evaluate(featureKey, user, defaultValue); + sendFlagRequestEvent(featureKey, user, value, defaultValue); + return value; + } + + private boolean evaluate(String featureKey, LDUser user, boolean defaultValue) { if (!initialized()) { return defaultValue; } + try { FeatureRep result = (FeatureRep) config.featureStore.get(featureKey); if (result == null) { - logger.warn("Unknown feature flag " + featureKey + "; returning default value"); - sendFlagRequestEvent(featureKey, user, defaultValue, defaultValue); + logger.warn("Unknown feature flag " + featureKey + "; returning default value: "); return defaultValue; } Boolean val = result.evaluate(user); if (val == null) { - sendFlagRequestEvent(featureKey, user, defaultValue, defaultValue); return defaultValue; } else { - sendFlagRequestEvent(featureKey, user, val, defaultValue); return val; } } catch (Exception e) { logger.error("Encountered exception in LaunchDarkly client", e); - sendFlagRequestEvent(featureKey, user, defaultValue, defaultValue); return defaultValue; } } @@ -234,7 +248,7 @@ public void flush() { * @return whether the client is in offline mode */ public boolean isOffline() { - return this.offline; + return config.offline; } private static String getClientVersion() { diff --git a/src/main/java/com/launchdarkly/client/PollingProcessor.java b/src/main/java/com/launchdarkly/client/PollingProcessor.java index abf31ae57..68b0417ee 100644 --- a/src/main/java/com/launchdarkly/client/PollingProcessor.java +++ b/src/main/java/com/launchdarkly/client/PollingProcessor.java @@ -8,6 +8,7 @@ import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public class PollingProcessor implements UpdateProcessor { private static final Logger logger = LoggerFactory.getLogger(PollingProcessor.class); @@ -15,7 +16,7 @@ public class PollingProcessor implements UpdateProcessor { private final FeatureRequestor requestor; private final LDConfig config; private final FeatureStore store; - private volatile boolean initialized = false; + private AtomicBoolean initialized = new AtomicBoolean(false); private ScheduledExecutorService scheduler = null; PollingProcessor(LDConfig config, FeatureRequestor requestor) { @@ -26,7 +27,7 @@ public class PollingProcessor implements UpdateProcessor { @Override public boolean initialized() { - return initialized && config.featureStore.initialized(); + return initialized.get() && config.featureStore.initialized(); } @Override @@ -46,9 +47,8 @@ public Future start() { public void run() { try { store.init(requestor.makeAllRequest(true)); - if (!initialized) { + if (!initialized.getAndSet(true)) { logger.info("Initialized LaunchDarkly client."); - initialized = true; initFuture.completed(null); } } catch (IOException e) { diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index 27da2f6d4..6af9f2c71 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -14,6 +14,7 @@ import java.net.URI; import java.util.Map; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; class StreamProcessor implements UpdateProcessor { private static final String PUT = "put"; @@ -28,7 +29,7 @@ class StreamProcessor implements UpdateProcessor { private final String apiKey; private final FeatureRequestor requestor; private EventSource es; - private volatile boolean initialized = false; + private AtomicBoolean initialized = new AtomicBoolean(false); StreamProcessor(String apiKey, LDConfig config, FeatureRequestor requestor) { @@ -62,8 +63,7 @@ public void onMessage(String name, MessageEvent event) throws Exception { Type type = new TypeToken>>(){}.getType(); Map> features = gson.fromJson(event.getData(), type); store.init(features); - if (!initialized) { - initialized = true; + if (!initialized.getAndSet(true)) { initFuture.completed(null); logger.info("Initialized LaunchDarkly client."); } @@ -80,8 +80,7 @@ else if (name.equals(INDIRECT_PUT)) { try { Map> features = requestor.makeAllRequest(true); store.init(features); - if (!initialized) { - initialized = true; + if (!initialized.getAndSet(true)) { initFuture.completed(null); logger.info("Initialized LaunchDarkly client."); } @@ -130,7 +129,7 @@ public void close() throws IOException { @Override public boolean initialized() { - return initialized && store.initialized(); + return initialized.get(); } FeatureRep getFeature(String key) { diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java index 26968d05e..2fef0cee0 100644 --- a/src/test/java/com/launchdarkly/client/LDClientTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientTest.java @@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.expect; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -17,40 +18,46 @@ public class LDClientTest extends EasyMockSupport { private FeatureRequestor requestor; private StreamProcessor streamProcessor; private PollingProcessor pollingProcessor; + private EventProcessor eventProcessor; private Future initFuture; + private LDClient client; @Before public void before() { requestor = createStrictMock(FeatureRequestor.class); streamProcessor = createStrictMock(StreamProcessor.class); pollingProcessor = createStrictMock(PollingProcessor.class); + eventProcessor = createStrictMock(EventProcessor.class); initFuture = createStrictMock(Future.class); } @Test - public void testOfflineDoesNotConnect() throws IOException { + public void testOffline() throws IOException { LDConfig config = new LDConfig.Builder() .offline(true) .build(); - LDClient client = createMockClient(config, 0L); + client = createMockClient(config, 0L); replayAll(); - assertDefaultValueIsReturned(client); + assertDefaultValueIsReturned(); assertTrue(client.initialized()); verifyAll(); } @Test - public void testUseLddDoesNotConnect() throws IOException { + public void testUseLdd() throws IOException { LDConfig config = new LDConfig.Builder() .useLdd(true) .build(); - LDClient client = createMockClient(config, 0L); + client = createMockClient(config, 0L); + // Asserting 2 things here: no pollingProcessor or streamingProcessor activity + // and sending of event: + expect(eventProcessor.sendEvent(anyObject(Event.class))).andReturn(true); replayAll(); - assertDefaultValueIsReturned(client); + assertDefaultValueIsReturned(); assertTrue(client.initialized()); verifyAll(); } @@ -63,10 +70,11 @@ public void testStreamingNoWait() throws IOException { expect(streamProcessor.start()).andReturn(initFuture); expect(streamProcessor.initialized()).andReturn(false); + expect(eventProcessor.sendEvent(anyObject(Event.class))).andReturn(true); replayAll(); - LDClient client = createMockClient(config, 0L); - assertDefaultValueIsReturned(client); + client = createMockClient(config, 0L); + assertDefaultValueIsReturned(); verifyAll(); } @@ -81,7 +89,7 @@ public void testStreamingWait() throws Exception { expect(initFuture.get(10L, TimeUnit.MILLISECONDS)).andThrow(new TimeoutException()); replayAll(); - LDClient client = createMockClient(config, 10L); + client = createMockClient(config, 10L); verifyAll(); } @@ -93,10 +101,11 @@ public void testPollingNoWait() throws IOException { expect(pollingProcessor.start()).andReturn(initFuture); expect(pollingProcessor.initialized()).andReturn(false); + expect(eventProcessor.sendEvent(anyObject(Event.class))).andReturn(true); replayAll(); - LDClient client = createMockClient(config, 0L); - assertDefaultValueIsReturned(client); + client = createMockClient(config, 0L); + assertDefaultValueIsReturned(); verifyAll(); } @@ -109,13 +118,16 @@ public void testPollingWait() throws Exception { expect(pollingProcessor.start()).andReturn(initFuture); expect(initFuture.get(10L, TimeUnit.MILLISECONDS)).andThrow(new TimeoutException()); + expect(eventProcessor.sendEvent(anyObject(Event.class))).andReturn(true); + expect(pollingProcessor.initialized()).andReturn(false); replayAll(); - LDClient client = createMockClient(config, 10L); + client = createMockClient(config, 10L); + assertDefaultValueIsReturned(); verifyAll(); } - private void assertDefaultValueIsReturned(LDClient client) { + private void assertDefaultValueIsReturned() { boolean result = client.toggle("test", new LDUser("test.key"), true); assertEquals(true, result); } @@ -125,7 +137,6 @@ private LDClient createMockClient( Long waitForMillis ) { return new LDClient("API_KEY", config, waitForMillis) { - @Override protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { return requestor; @@ -140,6 +151,11 @@ protected StreamProcessor createStreamProcessor(String apiKey, LDConfig config, protected PollingProcessor createPollingProcessor(LDConfig config) { return pollingProcessor; } + + @Override + protected EventProcessor createEventProcessor(String apiKey, LDConfig config) { + return eventProcessor; + } }; } } From 57f49058412dc72ffab36a15ec23509b99db2467 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Thu, 7 Apr 2016 17:02:00 -0700 Subject: [PATCH 08/13] Minor tweaks. Address PR comment. --- build.gradle | 4 +-- .../launchdarkly/client/PollingProcessor.java | 2 +- .../launchdarkly/client/StreamProcessor.java | 2 +- src/test/resources/simplelogger.properties | 34 +++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 src/test/resources/simplelogger.properties diff --git a/build.gradle b/build.gradle index fafcd2b2b..64dbe5e7c 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ repositories { allprojects { group = 'com.launchdarkly' - version = "0.21.0-SNAPSHOT" + version = "1.0.0-SNAPSHOT" sourceCompatibility = 1.7 targetCompatibility = 1.7 } @@ -29,7 +29,7 @@ dependencies { compile "com.google.code.gson:gson:2.2.4" compile "com.google.guava:guava:19.0" compile "org.slf4j:slf4j-api:1.7.7" - compile "com.launchdarkly:okhttp-eventsource:0.1.2" + compile "com.launchdarkly:okhttp-eventsource:0.1.3-SNAPSHOT" compile "redis.clients:jedis:2.8.0" testCompile "org.easymock:easymock:3.4" testCompile 'junit:junit:[4.10,)' diff --git a/src/main/java/com/launchdarkly/client/PollingProcessor.java b/src/main/java/com/launchdarkly/client/PollingProcessor.java index 68b0417ee..c8bfa4a02 100644 --- a/src/main/java/com/launchdarkly/client/PollingProcessor.java +++ b/src/main/java/com/launchdarkly/client/PollingProcessor.java @@ -27,7 +27,7 @@ public class PollingProcessor implements UpdateProcessor { @Override public boolean initialized() { - return initialized.get() && config.featureStore.initialized(); + return initialized.get(); } @Override diff --git a/src/main/java/com/launchdarkly/client/StreamProcessor.java b/src/main/java/com/launchdarkly/client/StreamProcessor.java index 6af9f2c71..e5ef339b7 100644 --- a/src/main/java/com/launchdarkly/client/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/client/StreamProcessor.java @@ -120,7 +120,7 @@ public void onError(Throwable throwable) { @Override public void close() throws IOException { if (es != null) { - es.stop(); + es.close(); } if (store != null) { store.close(); diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties new file mode 100644 index 000000000..ac0f70a70 --- /dev/null +++ b/src/test/resources/simplelogger.properties @@ -0,0 +1,34 @@ +# SLF4J's SimpleLogger configuration file +# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err. + +# Default logging detail level for all instances of SimpleLogger. +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, defaults to "info". +org.slf4j.simpleLogger.defaultLogLevel=debug + +# Logging detail level for a SimpleLogger instance named "xxxxx". +# Must be one of ("trace", "debug", "info", "warn", or "error"). +# If not specified, the default logging detail level is used. +#org.slf4j.simpleLogger.log.xxxxx= + +# Set to true if you want the current date and time to be included in output messages. +# Default is false, and will output the number of milliseconds elapsed since startup. +#org.slf4j.simpleLogger.showDateTime=false + +# The date and time format to be used in the output messages. +# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat. +# If the format is not specified or is invalid, the default format is used. +# The default format is yyyy-MM-dd HH:mm:ss:SSS Z. +#org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS Z + +# Set to true if you want to output the current thread name. +# Defaults to true. +#org.slf4j.simpleLogger.showThreadName=true + +# Set to true if you want the Logger instance name to be included in output messages. +# Defaults to true. +#org.slf4j.simpleLogger.showLogName=true + +# Set to true if you want the last component of the name to be included in output messages. +# Defaults to false. +#org.slf4j.simpleLogger.showShortLogName=false \ No newline at end of file From 426cd52dbff59f720b84424cbec0e03d0d718a4a Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Fri, 8 Apr 2016 13:25:32 -0700 Subject: [PATCH 09/13] Add some logging. --- src/main/java/com/launchdarkly/client/FeatureRequestor.java | 6 +++++- src/test/resources/simplelogger.properties | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/FeatureRequestor.java b/src/main/java/com/launchdarkly/client/FeatureRequestor.java index 2bab25df6..2dc949b50 100644 --- a/src/main/java/com/launchdarkly/client/FeatureRequestor.java +++ b/src/main/java/com/launchdarkly/client/FeatureRequestor.java @@ -68,6 +68,7 @@ Map> makeAllRequest(boolean latest) throws IOException { CloseableHttpResponse response = null; try { + logger.debug("Making request: " + request); response = client.execute(request, context); logCacheResponse(context.getCacheResponseStatus()); @@ -76,7 +77,10 @@ Map> makeAllRequest(boolean latest) throws IOException { Type type = new TypeToken>>() {}.getType(); - Map> result = gson.fromJson(EntityUtils.toString(response.getEntity()), type); + String json = EntityUtils.toString(response.getEntity()); + logger.debug("Got response: " + response.toString()); + logger.debug("Got Response body: " + json); + Map> result = gson.fromJson(json, type); return result; } finally { diff --git a/src/test/resources/simplelogger.properties b/src/test/resources/simplelogger.properties index ac0f70a70..413b5640e 100644 --- a/src/test/resources/simplelogger.properties +++ b/src/test/resources/simplelogger.properties @@ -4,7 +4,7 @@ # Default logging detail level for all instances of SimpleLogger. # Must be one of ("trace", "debug", "info", "warn", or "error"). # If not specified, defaults to "info". -org.slf4j.simpleLogger.defaultLogLevel=debug +org.slf4j.simpleLogger.defaultLogLevel=info # Logging detail level for a SimpleLogger instance named "xxxxx". # Must be one of ("trace", "debug", "info", "warn", or "error"). @@ -31,4 +31,4 @@ org.slf4j.simpleLogger.defaultLogLevel=debug # Set to true if you want the last component of the name to be included in output messages. # Defaults to false. -#org.slf4j.simpleLogger.showShortLogName=false \ No newline at end of file +org.slf4j.simpleLogger.showShortLogName=true \ No newline at end of file From ba7e4232b6765485daf359f779c154fb222075a1 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Fri, 8 Apr 2016 13:37:06 -0700 Subject: [PATCH 10/13] Add better checking for newer snapshot version. --- build.gradle | 7 ++- .../launchdarkly/client/IntegrationTest.java | 47 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/launchdarkly/client/IntegrationTest.java diff --git a/build.gradle b/build.gradle index 64dbe5e7c..abbe17be9 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,11 @@ apply plugin: 'signing' apply plugin: 'idea' apply plugin: 'com.github.johnrengelman.shadow' +configurations.all { + // check for updates every build + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' +} + repositories { mavenCentral() mavenLocal() @@ -29,7 +34,7 @@ dependencies { compile "com.google.code.gson:gson:2.2.4" compile "com.google.guava:guava:19.0" compile "org.slf4j:slf4j-api:1.7.7" - compile "com.launchdarkly:okhttp-eventsource:0.1.3-SNAPSHOT" + compile group: "com.launchdarkly", name: "okhttp-eventsource", version: "0.1.3-SNAPSHOT", changing: true compile "redis.clients:jedis:2.8.0" testCompile "org.easymock:easymock:3.4" testCompile 'junit:junit:[4.10,)' diff --git a/src/test/java/com/launchdarkly/client/IntegrationTest.java b/src/test/java/com/launchdarkly/client/IntegrationTest.java new file mode 100644 index 000000000..bbfbae3ad --- /dev/null +++ b/src/test/java/com/launchdarkly/client/IntegrationTest.java @@ -0,0 +1,47 @@ +package com.launchdarkly.client; + +import org.junit.Ignore; +import org.junit.Test; + +import java.io.IOException; +import java.net.URI; + +import static java.util.Collections.singletonList; + +@Ignore +public class IntegrationTest { + + @Test + public void testClient() throws IOException { + LDUser user = new LDUser.Builder("bob@example.com") + .firstName("Bob") + .lastName("Loblaw") + .customString("groups", singletonList("beta_testers")) + .build(); + + LDConfig config = new LDConfig.Builder() +// .streamURI(URI.create("https://ld-stg-stream.global.ssl.fastly.net")) +// .streamURI(URI.create("https://f6bff885.fanoutcdn.com")) + + .stream(false) + .build(); + +// String apiKey = "sdk-707fa2a8-f3be-4f14-a122-946ab580a648"; + + //Dan's staging test key: +// String apiKey = "sdk-6d82ac76-97ce-4877-a661-a5709ca18a63"; +// Dan's prod key: + String apiKey = "sdk-707fa2a8-f3be-4f14-a122-946ab580a648"; + LDClient client = new LDClient(apiKey, config, 30000L); + boolean showFeature = client.toggle("YOUR_FEATURE_KEY", user, false); + + if (showFeature) { + System.out.println("Showing your feature"); + } else { + System.out.println("Not showing your feature"); + } + + client.flush(); + while(true) {} + } +} From 3466fa9fd1f36bdcdd7c142ca62ad985c543134b Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Fri, 8 Apr 2016 15:12:05 -0700 Subject: [PATCH 11/13] Move start wait param into builder. Restore original constructor signatures. --- .../com/launchdarkly/client/LDClient.java | 16 ++++++-------- .../com/launchdarkly/client/LDConfig.java | 16 ++++++++++++++ .../com/launchdarkly/client/LDClientTest.java | 21 +++++++++---------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/LDClient.java b/src/main/java/com/launchdarkly/client/LDClient.java index 6b93bf84d..cc9a833c5 100644 --- a/src/main/java/com/launchdarkly/client/LDClient.java +++ b/src/main/java/com/launchdarkly/client/LDClient.java @@ -34,11 +34,9 @@ public class LDClient implements Closeable { * cases, you should use this constructor. * * @param apiKey the API key for your account - * @param waitForMillis when set to greater than zero allows callers to block until the client - * has connected to LaunchDarkly and is properly initialized */ - public LDClient(String apiKey, long waitForMillis) { - this(apiKey, LDConfig.DEFAULT, waitForMillis); + public LDClient(String apiKey) { + this(apiKey, LDConfig.DEFAULT); } /** @@ -47,10 +45,8 @@ public LDClient(String apiKey, long waitForMillis) { * * @param apiKey the API key for your account * @param config a client configuration object - * @param waitForMillis when set to greater than zero allows callers to block until the client - * has connected to LaunchDarkly and is properly initialized */ - public LDClient(String apiKey, LDConfig config, long waitForMillis) { + public LDClient(String apiKey, LDConfig config) { this.config = config; this.requestor = createFeatureRequestor(apiKey, config); this.eventProcessor = createEventProcessor(apiKey, config); @@ -75,10 +71,10 @@ public LDClient(String apiKey, LDConfig config, long waitForMillis) { Future startFuture = updateProcessor.start(); - if (waitForMillis > 0L) { - logger.info("Waiting up to " + waitForMillis + " milliseconds for LaunchDarkly client to start..."); + if (config.startWaitMillis > 0L) { + logger.info("Waiting up to " + config.startWaitMillis + " milliseconds for LaunchDarkly client to start..."); try { - startFuture.get(waitForMillis, TimeUnit.MILLISECONDS); + startFuture.get(config.startWaitMillis, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { logger.error("Timeout encountered waiting for LaunchDarkly client initialization"); } catch (Exception e) { diff --git a/src/main/java/com/launchdarkly/client/LDConfig.java b/src/main/java/com/launchdarkly/client/LDConfig.java index 0a7757991..6abca54a4 100644 --- a/src/main/java/com/launchdarkly/client/LDConfig.java +++ b/src/main/java/com/launchdarkly/client/LDConfig.java @@ -22,6 +22,7 @@ public final class LDConfig { private static final int DEFAULT_SOCKET_TIMEOUT = 10000; private static final int DEFAULT_FLUSH_INTERVAL = 5; private static final long DEFAULT_POLLING_INTERVAL_MILLIS = 1000L; + private static final long DEFAULT_START_WAIT_MILLIS = 0L; private static final Logger logger = LoggerFactory.getLogger(LDConfig.class); protected static final LDConfig DEFAULT = new Builder().build(); @@ -40,6 +41,7 @@ public final class LDConfig { final boolean useLdd; final boolean offline; final long pollingIntervalMillis; + final long startWaitMillis; protected LDConfig(Builder builder) { this.baseURI = builder.baseURI; @@ -60,6 +62,7 @@ protected LDConfig(Builder builder) { } else { this.pollingIntervalMillis = builder.pollingIntervalMillis; } + this.startWaitMillis = builder.startWaitMillis; } /** @@ -90,6 +93,7 @@ public static class Builder { private boolean offline = false; private long pollingIntervalMillis = DEFAULT_POLLING_INTERVAL_MILLIS; private FeatureStore featureStore = new InMemoryFeatureStore(); + public long startWaitMillis = DEFAULT_START_WAIT_MILLIS; /** * Creates a builder with all configuration parameters set to the default @@ -319,6 +323,18 @@ public Builder pollingIntervalMillis(long pollingIntervalMillis) { return this; } + /** + * Set how long the constructor will block awaiting a successful connection to LaunchDarkly. + * Default value of 0 will not block and cause the constructor to return immediately. + * + * @param startWaitMillis + * @return the builder + */ + public Builder startWaitMillis(long startWaitMillis) { + this.startWaitMillis = startWaitMillis; + return this; + } + HttpHost proxyHost() { if (this.proxyHost == null && this.proxyPort == -1 && this.proxyScheme == null) { return null; diff --git a/src/test/java/com/launchdarkly/client/LDClientTest.java b/src/test/java/com/launchdarkly/client/LDClientTest.java index 2fef0cee0..247519fac 100644 --- a/src/test/java/com/launchdarkly/client/LDClientTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientTest.java @@ -37,7 +37,7 @@ public void testOffline() throws IOException { .offline(true) .build(); - client = createMockClient(config, 0L); + client = createMockClient(config); replayAll(); assertDefaultValueIsReturned(); @@ -51,7 +51,7 @@ public void testUseLdd() throws IOException { .useLdd(true) .build(); - client = createMockClient(config, 0L); + client = createMockClient(config); // Asserting 2 things here: no pollingProcessor or streamingProcessor activity // and sending of event: expect(eventProcessor.sendEvent(anyObject(Event.class))).andReturn(true); @@ -73,7 +73,7 @@ public void testStreamingNoWait() throws IOException { expect(eventProcessor.sendEvent(anyObject(Event.class))).andReturn(true); replayAll(); - client = createMockClient(config, 0L); + client = createMockClient(config); assertDefaultValueIsReturned(); verifyAll(); @@ -82,6 +82,7 @@ public void testStreamingNoWait() throws IOException { @Test public void testStreamingWait() throws Exception { LDConfig config = new LDConfig.Builder() + .startWaitMillis(10L) .stream(true) .build(); @@ -89,7 +90,7 @@ public void testStreamingWait() throws Exception { expect(initFuture.get(10L, TimeUnit.MILLISECONDS)).andThrow(new TimeoutException()); replayAll(); - client = createMockClient(config, 10L); + client = createMockClient(config); verifyAll(); } @@ -104,7 +105,7 @@ public void testPollingNoWait() throws IOException { expect(eventProcessor.sendEvent(anyObject(Event.class))).andReturn(true); replayAll(); - client = createMockClient(config, 0L); + client = createMockClient(config); assertDefaultValueIsReturned(); verifyAll(); @@ -113,6 +114,7 @@ public void testPollingNoWait() throws IOException { @Test public void testPollingWait() throws Exception { LDConfig config = new LDConfig.Builder() + .startWaitMillis(10L) .stream(false) .build(); @@ -122,7 +124,7 @@ public void testPollingWait() throws Exception { expect(pollingProcessor.initialized()).andReturn(false); replayAll(); - client = createMockClient(config, 10L); + client = createMockClient(config); assertDefaultValueIsReturned(); verifyAll(); } @@ -132,11 +134,8 @@ private void assertDefaultValueIsReturned() { assertEquals(true, result); } - private LDClient createMockClient( - LDConfig config, - Long waitForMillis - ) { - return new LDClient("API_KEY", config, waitForMillis) { + private LDClient createMockClient(LDConfig config) { + return new LDClient("API_KEY", config) { @Override protected FeatureRequestor createFeatureRequestor(String apiKey, LDConfig config) { return requestor; From b7d87be79d5b084516f22d5c6e9324d70a1305b1 Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Fri, 8 Apr 2016 18:19:53 -0700 Subject: [PATCH 12/13] Revert accidentally added file. --- .../launchdarkly/client/IntegrationTest.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/test/java/com/launchdarkly/client/IntegrationTest.java diff --git a/src/test/java/com/launchdarkly/client/IntegrationTest.java b/src/test/java/com/launchdarkly/client/IntegrationTest.java deleted file mode 100644 index bbfbae3ad..000000000 --- a/src/test/java/com/launchdarkly/client/IntegrationTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.launchdarkly.client; - -import org.junit.Ignore; -import org.junit.Test; - -import java.io.IOException; -import java.net.URI; - -import static java.util.Collections.singletonList; - -@Ignore -public class IntegrationTest { - - @Test - public void testClient() throws IOException { - LDUser user = new LDUser.Builder("bob@example.com") - .firstName("Bob") - .lastName("Loblaw") - .customString("groups", singletonList("beta_testers")) - .build(); - - LDConfig config = new LDConfig.Builder() -// .streamURI(URI.create("https://ld-stg-stream.global.ssl.fastly.net")) -// .streamURI(URI.create("https://f6bff885.fanoutcdn.com")) - - .stream(false) - .build(); - -// String apiKey = "sdk-707fa2a8-f3be-4f14-a122-946ab580a648"; - - //Dan's staging test key: -// String apiKey = "sdk-6d82ac76-97ce-4877-a661-a5709ca18a63"; -// Dan's prod key: - String apiKey = "sdk-707fa2a8-f3be-4f14-a122-946ab580a648"; - LDClient client = new LDClient(apiKey, config, 30000L); - boolean showFeature = client.toggle("YOUR_FEATURE_KEY", user, false); - - if (showFeature) { - System.out.println("Showing your feature"); - } else { - System.out.println("Not showing your feature"); - } - - client.flush(); - while(true) {} - } -} From 63662d2bd6480733c819c6640072c0cf2c5a600d Mon Sep 17 00:00:00 2001 From: Dan Richelson Date: Fri, 8 Apr 2016 18:21:09 -0700 Subject: [PATCH 13/13] Add event sampling. Name threads. --- .../launchdarkly/client/EventProcessor.java | 35 ++++++++++--------- .../com/launchdarkly/client/LDConfig.java | 19 ++++++++++ .../launchdarkly/client/PollingProcessor.java | 11 +++--- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/launchdarkly/client/EventProcessor.java b/src/main/java/com/launchdarkly/client/EventProcessor.java index 8243d70d7..64169f2d7 100644 --- a/src/main/java/com/launchdarkly/client/EventProcessor.java +++ b/src/main/java/com/launchdarkly/client/EventProcessor.java @@ -1,5 +1,6 @@ package com.launchdarkly.client; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.gson.Gson; import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; @@ -14,22 +15,34 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Random; import java.util.concurrent.*; class EventProcessor implements Closeable { - private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory()); + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("LaunchDarkly-EventProcessor-%d") + .build(); + private final ScheduledExecutorService scheduler = + Executors.newSingleThreadScheduledExecutor(threadFactory); + private final Random random = new Random(); private final BlockingQueue queue; private final String apiKey; + private final LDConfig config; private final Consumer consumer; EventProcessor(String apiKey, LDConfig config) { this.apiKey = apiKey; this.queue = new ArrayBlockingQueue<>(config.capacity); this.consumer = new Consumer(config); + this.config = config; this.scheduler.scheduleAtFixedRate(consumer, 0, config.flushInterval, TimeUnit.SECONDS); } boolean sendEvent(Event e) { + if (config.samplingInterval > 0 && random.nextInt(config.samplingInterval) != 0) { + return true; + } return queue.offer(e); } @@ -43,18 +56,8 @@ public void flush() { this.consumer.flush(); } - static class DaemonThreadFactory implements ThreadFactory { - public Thread newThread(Runnable r) { - Thread thread = new Thread(r); - thread.setDaemon(true); - return thread; - } - } - class Consumer implements Runnable { private final Logger logger = LoggerFactory.getLogger(Consumer.class); - - private final CloseableHttpClient client; private final LDConfig config; @@ -78,6 +81,7 @@ public void flush() { } private void postEvents(List events) { + logger.debug("Posting " + events.size() + " event(s).."); CloseableHttpResponse response = null; Gson gson = new Gson(); String json = gson.toJson(events); @@ -95,13 +99,11 @@ private void postEvents(List events) { if (status >= 300) { if (status == HttpStatus.SC_UNAUTHORIZED) { logger.error("Invalid API key"); - } - else { + } else { logger.error("Unexpected status code: " + status); } - } - else { - logger.debug("Successfully processed events"); + } else { + logger.debug("Successfully posted " + events.size() + " event(s)."); } } catch (IOException e) { logger.error("Unhandled exception in LaunchDarkly client attempting to connect to URI: " + config.eventsURI, e); @@ -112,7 +114,6 @@ private void postEvents(List events) { logger.error("Unhandled exception in LaunchDarkly client", e); } } - } } } diff --git a/src/main/java/com/launchdarkly/client/LDConfig.java b/src/main/java/com/launchdarkly/client/LDConfig.java index 6abca54a4..8fbdce5cf 100644 --- a/src/main/java/com/launchdarkly/client/LDConfig.java +++ b/src/main/java/com/launchdarkly/client/LDConfig.java @@ -23,6 +23,7 @@ public final class LDConfig { private static final int DEFAULT_FLUSH_INTERVAL = 5; private static final long DEFAULT_POLLING_INTERVAL_MILLIS = 1000L; private static final long DEFAULT_START_WAIT_MILLIS = 0L; + private static final int DEFAULT_SAMPLING_INTERVAL = 0; private static final Logger logger = LoggerFactory.getLogger(LDConfig.class); protected static final LDConfig DEFAULT = new Builder().build(); @@ -42,6 +43,7 @@ public final class LDConfig { final boolean offline; final long pollingIntervalMillis; final long startWaitMillis; + final int samplingInterval; protected LDConfig(Builder builder) { this.baseURI = builder.baseURI; @@ -63,6 +65,7 @@ protected LDConfig(Builder builder) { this.pollingIntervalMillis = builder.pollingIntervalMillis; } this.startWaitMillis = builder.startWaitMillis; + this.samplingInterval = builder.samplingInterval; } /** @@ -94,6 +97,7 @@ public static class Builder { private long pollingIntervalMillis = DEFAULT_POLLING_INTERVAL_MILLIS; private FeatureStore featureStore = new InMemoryFeatureStore(); public long startWaitMillis = DEFAULT_START_WAIT_MILLIS; + public int samplingInterval = DEFAULT_SAMPLING_INTERVAL; /** * Creates a builder with all configuration parameters set to the default @@ -335,6 +339,21 @@ public Builder startWaitMillis(long startWaitMillis) { return this; } + /** + * Enable event sampling. When set to the default of zero, sampling is disabled and all events + * are sent back to LaunchDarkly. When set to greater than zero, there is a 1 in + * samplingInterval chance events will be will be sent. + * + *

Example: if you want 5% sampling rate, set samplingInterval to 20. + * + * @param samplingInterval the sampling interval. + * @return the builder + */ + public Builder samplingInterval(int samplingInterval) { + this.samplingInterval = samplingInterval; + return this; + } + HttpHost proxyHost() { if (this.proxyHost == null && this.proxyPort == -1 && this.proxyScheme == null) { return null; diff --git a/src/main/java/com/launchdarkly/client/PollingProcessor.java b/src/main/java/com/launchdarkly/client/PollingProcessor.java index c8bfa4a02..3b1541715 100644 --- a/src/main/java/com/launchdarkly/client/PollingProcessor.java +++ b/src/main/java/com/launchdarkly/client/PollingProcessor.java @@ -1,13 +1,11 @@ package com.launchdarkly.client; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; public class PollingProcessor implements UpdateProcessor { @@ -40,7 +38,10 @@ public Future start() { logger.info("Starting LaunchDarkly polling client with interval: " + config.pollingIntervalMillis + " milliseconds"); final VeryBasicFuture initFuture = new VeryBasicFuture(); - scheduler = Executors.newScheduledThreadPool(1); + ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat("LaunchDarkly-PollingProcessor-%d") + .build(); + scheduler = Executors.newScheduledThreadPool(1, threadFactory); scheduler.scheduleAtFixedRate(new Runnable() { @Override