diff --git a/core-httpclient-impl/README.md b/core-httpclient-impl/README.md new file mode 100644 index 000000000..77b9e456f --- /dev/null +++ b/core-httpclient-impl/README.md @@ -0,0 +1,189 @@ +# Java SDK Async Http Client + +This package provides default implementations of an Optimizely `EventHandler` and `ProjectConfigManager`. Also included +in this package is a factory class, `OptimizelyFactory`, which can be used to reliably instantiate the Optimizely SDK +with the default configuration of the `AsyncEventHandler` and `HttpProjectConfigManager`. + +## Installation + +### Gradle + +```groovy +compile 'com.optimizely.ab:core-httpclient-impl:{VERSION}' +``` + +### Maven +```xml + + com.optimizely.ab + core-httpclient-impl + {VERSION} + + +``` + +### Basic usage +```java +import com.optimizely.ab.Optimizely; +import com.optimizely.ab.OptimizelyFactory; + +public class App { + + public static void main(String[] args) { + String sdkKey = args[0]; + Optimizely optimizely = OptimizelyFactory.newDefaultInstance(sdkKey); + } +} + +``` + +### Advanced usage +```java +import com.optimizely.ab.Optimizely; +import com.optimizely.ab.config.HttpProjectConfigManager; +import com.optimizely.ab.event.AsyncEventHandler; + +import java.util.concurrent.TimeUnit; + +public class App { + + public static void main(String[] args) { + String sdkKey = args[0]; + + EventHandler eventHandler = AsyncEventHandler.builder() + .withQueueCapacity(20000) + .withNumWorkers(5) + .build(); + + ProjectConfigManager projectConfigManager = HttpProjectConfigManager.builder() + .withSdkKey(sdkKey) + .withPollingInterval(1, TimeUnit.MINUTES) + .build(); + + Optimizely optimizely = Optimizely.builder() + .withConfig(projectConfigManager) + .withEventHandler(eventHandler) + .build(); + } +} +``` + +## AsyncEventHandler + +The [`AsyncEventHandler`](https://github.com/optimizely/java-sdk/blob/master/core-httpclient-impl/src/main/java/com/optimizely/ab/event/AsyncEventHandler.java) +provides an implementation of the the [`EventHandler`](https://github.com/optimizely/java-sdk/blob/master/core-api/src/main/java/com/optimizely/ab/event/EventHandler.java) +backed by a `ThreadPoolExecutor`. When events are +triggered from the Optimizely SDK, they are immediately queued as discrete tasks to the executor and processed in the +order they were submitted. Each worker is responsible for making outbound http requests to the +Optimizely log endpoint for metric tracking. The default queue size and the number of workers are configurable via +global properties and can be overridden via the `AsyncEventHandler.Builder`. + +### Usage + +To use the AsyncEventHandler, an instance must be built via the `AsyncEventHandler.Builder` then passed to the `Optimizely.Builder` + +```java +EventHandler eventHandler = AsyncEventHandler.builder() + .withQueueCapacity(20000) + .withNumWorkers(5) + .build(); +``` + +#### Queue capacity + +The queue capacity can be set to initialize the backing queue for the executor service. If the queue fills up, then +events will be dropped and exception will be logged. Setting a higher queue value will prevent event loss, but will +use up more memory in the event the workers can not keep up if the production rate. + +#### Number of workers + +The number of workers determines the number of threads used by the thread pool. + +#### Advanced configurations + +|Property Name|Default Value|Description| +|---|---|---| +|async.event.handler.queue.capacity|10000|Queue size for pending LogEvents| +|async.event.handler.num.workers|2|Number of worker threads| +|async.event.handler.max.connections|200|Max number of connections| +|async.event.handler.event.max.per.route|20|Max number of connections per route| +|async.event.handler.validate.after|5000|Time in milliseconds to maintain idol connections| + + +## HttpProjectConfigManager + +The [`HttpProjectConfigManager`](https://github.com/optimizely/java-sdk/blob/master/core-httpclient-impl/src/main/java/com/optimizely/ab/config/HttpProjectConfigManager.java) +is an implementation of the abstract [`PollingProjectConfigManager`](https://github.com/optimizely/java-sdk/blob/master/core-api/src/main/java/com/optimizely/ab/config/PollingProjectConfigManager.java). +The `poll` method is extended and makes an http GET request to the configured url to asynchronously download the project data file +and initialize an instance of the ProjectConfig. By default, the `HttpProjectConfigManager` will block until the +first successful retrieval of the datafile, up to a configurable timeout. The frequency of the polling method and the +blocking timeout can be set via the `HttpProjectConfigManager.Builder` with the default values being pulled from global +properties. + +### Usage + +```java +ProjectConfigManager projectConfigManager = HttpProjectConfigManager.builder() + .withSdkKey(sdkKey) + .withPollingInterval(1, TimeUnit.MINUTES) + .build(); +``` + +#### SDK Key + +The SDK key is used to compose the outbound http request to the default datafile location hosted on the Optimizely CDN. + +#### Polling interval + +The polling interval is used to determine a fixed delay between consecutive http requests for the datafile. + +#### Initial Datafile + +An initial datafile can be provided via the builder to bootstrap the the `ProjectConfigManager` so that it can be used +immediately without blocking execution. + +#### Advanced configurations + +|Property Name|Default Value|Description| +|---|---|---| +|http.project.config.manager.polling.duration|5|Fixed delay between fetches for the datafile| +|http.project.config.manager.polling.unit|MINUTES|Time unit corresponding to polling interval| +|http.project.config.manager.blocking.duration|10|Max duration spent waiting for initial bootstrapping| +|http.project.config.manager.blocking.unit|SECONDS|Time unit corresponding to blocking duration| +|http.project.config.manager.sdk.key|null|Optimizely project SDK key| + + +## Optimizely properties file + +An Optimizely properties file, `optimizely.properties`, that is available within the runtime classpath can be used to configure +the default values of a given Optimizely resource. Refer to the resource implementation for available configuration +parameters. + +#### Example: +```properties +http.project.config.manager.polling.duration = 1 +http.project.config.manager.polling.unit = MINUTES + +async.event.handler.queue.capacity = 20000 +async.event.handler.num.workers = 5 +``` + +## OptimizelyFactory + +The [`OptimizelyFactory`](https://github.com/optimizely/java-sdk/blob/master/core-httpclient-impl/src/main/java/com/optimizely/ab/OptimizelyFactory.java) +included in this package provides basic utility to instantiate the Optimizely SDK +with a minimal number of provided configuration options. Configuration properties are sourced from Java system properties, +environment variables or from an `optimizely.properties` file, in that order. Not all configuration and initialization +are captured via the `OptimizelyFactory`, for those use cases the resources can be built via their respective builder +classes. + +### Usage +The SDK key is required to be provided at runtime either directly via the factory method: +```Java +Optimizely optimizely = OptimizelyFactory.newDefaultInstance(<>); +``` + +If the SDK is provided via a global property then the empty signature can be used: +```Java +Optimizely optimizely = OptimizelyFactory.newDefaultInstance(); +``` diff --git a/core-httpclient-impl/src/main/java/com/optimizely/ab/config/HttpProjectConfigManager.java b/core-httpclient-impl/src/main/java/com/optimizely/ab/config/HttpProjectConfigManager.java index 82cbf268c..37d60ca20 100644 --- a/core-httpclient-impl/src/main/java/com/optimizely/ab/config/HttpProjectConfigManager.java +++ b/core-httpclient-impl/src/main/java/com/optimizely/ab/config/HttpProjectConfigManager.java @@ -241,11 +241,15 @@ public HttpProjectConfigManager build(boolean defer) { if (period <= 0) { logger.warn("Invalid polling interval {}, {}. Defaulting to {}, {}", period, timeUnit, DEFAULT_POLLING_DURATION, DEFAULT_POLLING_UNIT); + period = DEFAULT_POLLING_DURATION; + timeUnit = DEFAULT_POLLING_UNIT; } if (blockingTimeoutPeriod <= 0) { logger.warn("Invalid polling interval {}, {}. Defaulting to {}, {}", blockingTimeoutPeriod, blockingTimeoutUnit, DEFAULT_BLOCKING_DURATION, DEFAULT_BLOCKING_UNIT); + blockingTimeoutPeriod = DEFAULT_BLOCKING_DURATION; + blockingTimeoutUnit = DEFAULT_BLOCKING_UNIT; } if (httpClient == null) { diff --git a/core-httpclient-impl/src/test/java/com/optimizely/ab/config/HttpProjectConfigManagerTest.java b/core-httpclient-impl/src/test/java/com/optimizely/ab/config/HttpProjectConfigManagerTest.java index 4b2e8acfd..aa5555de4 100644 --- a/core-httpclient-impl/src/test/java/com/optimizely/ab/config/HttpProjectConfigManagerTest.java +++ b/core-httpclient-impl/src/test/java/com/optimizely/ab/config/HttpProjectConfigManagerTest.java @@ -82,6 +82,11 @@ public void tearDown() { } projectConfigManager.close(); + + System.clearProperty("optimizely." + HttpProjectConfigManager.CONFIG_BLOCKING_UNIT); + System.clearProperty("optimizely." + HttpProjectConfigManager.CONFIG_BLOCKING_DURATION); + System.clearProperty("optimizely." + HttpProjectConfigManager.CONFIG_POLLING_UNIT); + System.clearProperty("optimizely." + HttpProjectConfigManager.CONFIG_POLLING_DURATION); } @Test @@ -236,6 +241,7 @@ public void testGetDatafileHttpResponse5XX() throws Exception { projectConfigManager.getDatafileFromResponse(getResponse); } + @Test public void testInvalidPayload() throws Exception { reset(mockHttpClient); CloseableHttpResponse invalidPayloadResponse = mock(CloseableHttpResponse.class); @@ -257,6 +263,35 @@ public void testInvalidPayload() throws Exception { assertNull(projectConfigManager.getConfig()); } + @Test + public void testInvalidPollingIntervalFromSystemProperties() throws Exception { + System.setProperty("optimizely." + HttpProjectConfigManager.CONFIG_POLLING_DURATION, "-1"); + projectConfigManager = builder() + .withOptimizelyHttpClient(mockHttpClient) + .withSdkKey("sdk-key") + .build(); + } + + @Test + public void testInvalidBlockingIntervalFromSystemProperties() throws Exception { + reset(mockHttpClient); + CloseableHttpResponse invalidPayloadResponse = mock(CloseableHttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + + when(statusLine.getStatusCode()).thenReturn(200); + when(invalidPayloadResponse.getStatusLine()).thenReturn(statusLine); + when(invalidPayloadResponse.getEntity()).thenReturn(new StringEntity("I am an invalid response!")); + + when(mockHttpClient.execute(any(HttpGet.class))) + .thenReturn(invalidPayloadResponse); + + System.setProperty("optimizely." + HttpProjectConfigManager.CONFIG_BLOCKING_DURATION, "-1"); + projectConfigManager = builder() + .withOptimizelyHttpClient(mockHttpClient) + .withSdkKey("sdk-key") + .build(); + } + @Test @Ignore public void testBasicFetch() throws Exception {