diff --git a/Makefile b/Makefile index 413483f3d..ffa1b6f1a 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ test: TEMP_TEST_OUTPUT=/tmp/sdk-test-service.log +# Add any extra sdk-test-harness parameters here, such as -skip for tests that are +# temporarily not working. +TEST_HARNESS_PARAMS= + build-contract-tests: @cd contract-tests && ../gradlew installDist @@ -21,8 +25,8 @@ start-contract-test-service-bg: @make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 & run-contract-tests: - @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v1.0.0/downloader/run.sh \ - | VERSION=v1 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh + @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ + | VERSION=v2 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests diff --git a/README.md b/README.md index d24105a2e..1f4da74cc 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,12 @@ This version of the LaunchDarkly SDK works with Java 8 and above. ## Distributions -Three variants of the SDK jar are published to Maven: +Two variants of the SDK jar are published to Maven: -* The default uberjar - this is accessible as `com.launchdarkly:launchdarkly-java-server-sdk:jar` and is the dependency used in the "[Getting started](https://docs.launchdarkly.com/sdk/server-side/java#getting-started)" section of the SDK reference guide as well as in the [`hello-java`](https://github.com/launchdarkly/hello-java) sample app. This variant contains the SDK classes, and all of the SDK's dependencies except for SLF4J (the SLF4J API is assumed to be brought in automatically as Maven dependencies, or otherwise made available in the classpath of the host application). All third-party bundled dependencies have shaded package names (and are not exported in OSGi), so they will not interfere with any other versions of the same packages. -* The extended uberjar - add `all` in Maven, or `:all` in Gradle. This is the same as the default uberjar except that SLF4J is also bundled, without shading (and is exported in OSGi). -* The "thin" jar - add `thin` in Maven, or `:thin` in Gradle. This contains _only_ the SDK classes. Applications using this jar must provide all of the dependencies that are in the SDK's `build.gradle`, so it is intended for use only in special cases. +* The default uberjar - this is accessible as `com.launchdarkly:launchdarkly-java-server-sdk:jar` and is the dependency used in the "[Getting started](https://docs.launchdarkly.com/sdk/server-side/java#getting-started)" section of the SDK reference guide as well as in the [`hello-java`](https://github.com/launchdarkly/hello-java) sample app. This variant contains the SDK classes and all of its required dependencies. All bundled dependencies that are not surfaced in the public API have shaded package names (and are not exported in OSGi), so they will not interfere with any other versions of the same packages. +* The "thin" jar - add `thin` in Maven, or `:thin` in Gradle. This contains only the SDK classes, without its dependencies. Applications using this jar must provide all of the dependencies that are in the SDK's `build.gradle`, so it is intended for use only in special cases. + +Previous SDK versions also included a third classifier, `all`, which was the same as the default uberjar but also contained the SLF4J API. This no longer exists because the SDK no longer requires the SLF4J API to be in the classpath. ## Getting started @@ -27,9 +28,11 @@ Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side ## Logging -By default, the LaunchDarkly SDK uses [SLF4J](https://www.slf4j.org/). SLF4J has its own configuration mechanisms for determining where output will go, and filtering by level and/or logger name. +By default, the LaunchDarkly SDK uses [SLF4J](https://www.slf4j.org/) _if_ the SLF4J API is present in the classpath. SLF4J has its own configuration mechanisms for determining where output will go, and filtering by level and/or logger name. + +If SLF4J is not in the classpath, the SDK's default logging destination is `System.err`. -The SDK can also be configured to use other adapters from the [com.launchdarkly.logging](https://github.com/launchdarkly/java-logging) facade instead of SLF4J. See `LoggingConfigurationBuilder`. This allows the logging behavior to be completely determined by the application, rather than by external SLF4J configuration. +The SDK can also be configured to use other adapters from the [com.launchdarkly.logging](https://github.com/launchdarkly/java-logging) facade. See `LoggingConfigurationBuilder`. This allows the logging behavior to be completely determined by the application, rather than by external SLF4J configuration. For an example of using the default SLF4J behavior with a simple console logging configuration, check out the [`slf4j-logging` branch](https://github.com/launchdarkly/hello-java/tree/slf4j-logging) of the [`hello-java`](https://github.com/launchdarkly/hello-java) project. The [main branch](https://github.com/launchdarkly/hello-java) of `hello-java` uses console logging that is programmatically configured without SLF4J. diff --git a/benchmarks/Makefile b/benchmarks/Makefile index d39fff5d9..06e8ff3b6 100644 --- a/benchmarks/Makefile +++ b/benchmarks/Makefile @@ -4,13 +4,13 @@ BASE_DIR:=$(shell pwd) PROJECT_DIR=$(shell cd .. && pwd) SDK_VERSION=$(shell grep "version=" $(PROJECT_DIR)/gradle.properties | cut -d '=' -f 2) -BENCHMARK_ALL_JAR=lib/launchdarkly-java-server-sdk-all.jar +BENCHMARK_SDK_JAR=lib/launchdarkly-java-server-sdk.jar BENCHMARK_TEST_JAR=lib/launchdarkly-java-server-sdk-test.jar SDK_JARS_DIR=$(PROJECT_DIR)/build/libs -SDK_ALL_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-all.jar +SDK_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION).jar SDK_TEST_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-test.jar -benchmark: $(BENCHMARK_ALL_JAR) $(BENCHMARK_TEST_JAR) +benchmark: $(BENCHMARK_SDK_JAR) $(BENCHMARK_TEST_JAR) rm -rf build/tmp ../gradlew jmh cat build/reports/jmh/human.txt @@ -21,7 +21,7 @@ clean: sdk: $(BENCHMARK_ALL_JAR) $(BENCHMARK_TEST_JAR) -$(BENCHMARK_ALL_JAR): $(SDK_ALL_JAR) +$(BENCHMARK_SDK_JAR): $(SDK_JAR) mkdir -p lib cp $< $@ @@ -29,8 +29,8 @@ $(BENCHMARK_TEST_JAR): $(SDK_TEST_JAR) mkdir -p lib cp $< $@ -$(SDK_ALL_JAR): - cd .. && ./gradlew shadowJarAll +$(SDK_JAR): + cd .. && ./gradlew shadowJar $(SDK_TEST_JAR): cd .. && ./gradlew testJar diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index 36abab03f..fa15e5d2b 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -20,7 +20,7 @@ ext.versions = [ ] dependencies { - implementation files("lib/launchdarkly-java-server-sdk-all.jar") + implementation files("lib/launchdarkly-java-server-sdk.jar") implementation files("lib/launchdarkly-java-server-sdk-test.jar") implementation "com.google.code.gson:gson:2.8.9" implementation "com.google.guava:guava:${versions.guava}" // required by SDK test code diff --git a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/EventProcessorBenchmarks.java b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/EventProcessorBenchmarks.java deleted file mode 100644 index 955788e53..000000000 --- a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/EventProcessorBenchmarks.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventProcessor; -import com.launchdarkly.sdk.server.interfaces.EventSender; -import com.launchdarkly.sdk.server.interfaces.EventSenderFactory; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; - -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; - -import java.io.IOException; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Random; -import java.util.concurrent.CountDownLatch; - -import static com.launchdarkly.sdk.server.TestValues.BASIC_USER; -import static com.launchdarkly.sdk.server.TestValues.CUSTOM_EVENT; -import static com.launchdarkly.sdk.server.TestValues.TEST_EVENTS_COUNT; - -public class EventProcessorBenchmarks { - private static final int EVENT_BUFFER_SIZE = 1000; - private static final int FLAG_COUNT = 10; - private static final int FLAG_VERSIONS = 3; - private static final int FLAG_VARIATIONS = 2; - - @State(Scope.Thread) - public static class BenchmarkInputs { - // Initialization of the things in BenchmarkInputs does not count as part of a benchmark. - final EventProcessor eventProcessor; - final MockEventSender eventSender; - final List featureRequestEventsWithoutTracking = new ArrayList<>(); - final List featureRequestEventsWithTracking = new ArrayList<>(); - final Random random; - - public BenchmarkInputs() { - // MockEventSender does no I/O - it discards every event payload. So we are benchmarking - // all of the event processing steps up to that point, including the formatting of the - // JSON data in the payload. - eventSender = new MockEventSender(); - - eventProcessor = Components.sendEvents() - .capacity(EVENT_BUFFER_SIZE) - .eventSender(new MockEventSenderFactory(eventSender)) - .createEventProcessor(TestComponents.clientContext(TestValues.SDK_KEY, LDConfig.DEFAULT)); - - random = new Random(); - - for (int i = 0; i < TEST_EVENTS_COUNT; i++) { - String flagKey = "flag" + random.nextInt(FLAG_COUNT); - int version = random.nextInt(FLAG_VERSIONS) + 1; - int variation = random.nextInt(FLAG_VARIATIONS); - for (boolean trackEvents: new boolean[] { false, true }) { - Event.FeatureRequest event = new Event.FeatureRequest( - System.currentTimeMillis(), - flagKey, - BASIC_USER, - version, - variation, - LDValue.of(variation), - LDValue.ofNull(), - null, - null, - trackEvents, - 0, - false - ); - (trackEvents ? featureRequestEventsWithTracking : featureRequestEventsWithoutTracking).add(event); - } - } - } - - public String randomFlagKey() { - return "flag" + random.nextInt(FLAG_COUNT); - } - - public int randomFlagVersion() { - return random.nextInt(FLAG_VERSIONS) + 1; - } - - public int randomFlagVariation() { - return random.nextInt(FLAG_VARIATIONS); - } - } - - @Benchmark - public void summarizeFeatureRequestEvents(BenchmarkInputs inputs) throws Exception { - for (Event.FeatureRequest event: inputs.featureRequestEventsWithoutTracking) { - inputs.eventProcessor.sendEvent(event); - } - inputs.eventProcessor.flush(); - inputs.eventSender.awaitEvents(); - } - - @Benchmark - public void featureRequestEventsWithFullTracking(BenchmarkInputs inputs) throws Exception { - for (Event.FeatureRequest event: inputs.featureRequestEventsWithTracking) { - inputs.eventProcessor.sendEvent(event); - } - inputs.eventProcessor.flush(); - inputs.eventSender.awaitEvents(); - } - - @Benchmark - public void customEvents(BenchmarkInputs inputs) throws Exception { - for (int i = 0; i < TEST_EVENTS_COUNT; i++) { - inputs.eventProcessor.sendEvent(CUSTOM_EVENT); - } - inputs.eventProcessor.flush(); - inputs.eventSender.awaitEvents(); - } - - private static final class MockEventSender implements EventSender { - private static final Result RESULT = new Result(true, false, null); - private static final CountDownLatch counter = new CountDownLatch(1); - - @Override - public void close() throws IOException {} - - @Override - public Result sendEventData(EventDataKind arg0, String arg1, int arg2, URI arg3) { - counter.countDown(); - return RESULT; - } - - public void awaitEvents() throws InterruptedException { - counter.await(); - } - } - - private static final class MockEventSenderFactory implements EventSenderFactory { - private final MockEventSender instance; - - MockEventSenderFactory(MockEventSender instance) { - this.instance = instance; - } - - @Override - public EventSender createEventSender(BasicConfiguration arg0, HttpConfiguration arg1) { - return instance; - } - } -} diff --git a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/LDClientEvaluationBenchmarks.java b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/LDClientEvaluationBenchmarks.java index 8f389caa6..9cba61306 100644 --- a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/LDClientEvaluationBenchmarks.java +++ b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/LDClientEvaluationBenchmarks.java @@ -1,10 +1,10 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import com.launchdarkly.sdk.server.subsystems.DataStore; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Scope; @@ -13,7 +13,7 @@ import java.util.Random; import static com.launchdarkly.sdk.server.TestComponents.initedDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static com.launchdarkly.sdk.server.TestValues.BOOLEAN_FLAG_KEY; import static com.launchdarkly.sdk.server.TestValues.CLAUSE_MATCH_VALUE_COUNT; @@ -22,8 +22,8 @@ import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_TARGET_LIST_KEY; import static com.launchdarkly.sdk.server.TestValues.INT_FLAG_KEY; import static com.launchdarkly.sdk.server.TestValues.JSON_FLAG_KEY; -import static com.launchdarkly.sdk.server.TestValues.NOT_MATCHED_VALUE_USER; -import static com.launchdarkly.sdk.server.TestValues.NOT_TARGETED_USER_KEY; +import static com.launchdarkly.sdk.server.TestValues.NOT_MATCHED_VALUE_CONTEXT; +import static com.launchdarkly.sdk.server.TestValues.NOT_TARGETED_CONTEXT_KEY; import static com.launchdarkly.sdk.server.TestValues.SDK_KEY; import static com.launchdarkly.sdk.server.TestValues.STRING_FLAG_KEY; import static com.launchdarkly.sdk.server.TestValues.TARGETED_USER_KEYS; @@ -41,7 +41,7 @@ public class LDClientEvaluationBenchmarks { public static class BenchmarkInputs { // Initialization of the things in BenchmarkInputs does not count as part of a benchmark. final LDClientInterface client; - final LDUser basicUser; + final LDContext basicUser; final Random random; public BenchmarkInputs() { @@ -51,14 +51,14 @@ public BenchmarkInputs() { } LDConfig config = new LDConfig.Builder() - .dataStore(specificDataStore(dataStore)) + .dataStore(specificComponent(dataStore)) .events(Components.noEvents()) .dataSource(Components.externalUpdatesOnly()) .logging(Components.noLogging()) .build(); client = new LDClient(SDK_KEY, config); - basicUser = new LDUser("userkey"); + basicUser = LDContext.create("userkey"); random = new Random(); } @@ -127,13 +127,13 @@ public void jsonVariationForUnknownFlag(BenchmarkInputs inputs) throws Exception @Benchmark public void userFoundInTargetList(BenchmarkInputs inputs) throws Exception { String userKey = TARGETED_USER_KEYS.get(inputs.random.nextInt(TARGETED_USER_KEYS.size())); - boolean result = inputs.client.boolVariation(FLAG_WITH_TARGET_LIST_KEY, new LDUser(userKey), false); + boolean result = inputs.client.boolVariation(FLAG_WITH_TARGET_LIST_KEY, LDContext.create(userKey), false); assertTrue(result); } @Benchmark public void userNotFoundInTargetList(BenchmarkInputs inputs) throws Exception { - boolean result = inputs.client.boolVariation(FLAG_WITH_TARGET_LIST_KEY, new LDUser(NOT_TARGETED_USER_KEY), false); + boolean result = inputs.client.boolVariation(FLAG_WITH_TARGET_LIST_KEY, LDContext.create(NOT_TARGETED_CONTEXT_KEY), false); assertFalse(result); } @@ -146,14 +146,14 @@ public void flagWithPrerequisite(BenchmarkInputs inputs) throws Exception { @Benchmark public void userValueFoundInClauseList(BenchmarkInputs inputs) throws Exception { int i = inputs.random.nextInt(CLAUSE_MATCH_VALUE_COUNT); - LDUser user = TestValues.CLAUSE_MATCH_VALUE_USERS.get(i); - boolean result = inputs.client.boolVariation(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY, user, false); + LDContext context = TestValues.CLAUSE_MATCH_VALUE_CONTEXTS.get(i); + boolean result = inputs.client.boolVariation(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY, context, false); assertTrue(result); } @Benchmark public void userValueNotFoundInClauseList(BenchmarkInputs inputs) throws Exception { - boolean result = inputs.client.boolVariation(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY, NOT_MATCHED_VALUE_USER, false); + boolean result = inputs.client.boolVariation(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY, NOT_MATCHED_VALUE_CONTEXT, false); assertFalse(result); } } diff --git a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/TestValues.java b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/TestValues.java index ab080b7f7..4d3a6755c 100644 --- a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/TestValues.java +++ b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/TestValues.java @@ -1,11 +1,10 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Target; -import com.launchdarkly.sdk.server.interfaces.Event; import java.util.ArrayList; import java.util.HashSet; @@ -21,7 +20,7 @@ private TestValues() {} public static final String SDK_KEY = "sdk-key"; - public static final LDUser BASIC_USER = new LDUser("userkey"); + public static final LDContext BASIC_CONTEXT = LDContext.create("userkey"); public static final String BOOLEAN_FLAG_KEY = "flag-bool"; public static final String INT_FLAG_KEY = "flag-int"; @@ -39,26 +38,26 @@ private TestValues() {} TARGETED_USER_KEYS.add("user-" + i); } } - public static final String NOT_TARGETED_USER_KEY = "no-match"; + public static final String NOT_TARGETED_CONTEXT_KEY = "no-match"; public static final String CLAUSE_MATCH_ATTRIBUTE = "clause-match-attr"; public static final int CLAUSE_MATCH_VALUE_COUNT = 1000; public static final List CLAUSE_MATCH_VALUES; - public static final List CLAUSE_MATCH_VALUE_USERS; + public static final List CLAUSE_MATCH_VALUE_CONTEXTS; static { // pre-generate all these values and matching users so this work doesn't count in the evaluation benchmark performance CLAUSE_MATCH_VALUES = new ArrayList<>(CLAUSE_MATCH_VALUE_COUNT); - CLAUSE_MATCH_VALUE_USERS = new ArrayList<>(CLAUSE_MATCH_VALUE_COUNT); + CLAUSE_MATCH_VALUE_CONTEXTS = new ArrayList<>(CLAUSE_MATCH_VALUE_COUNT); for (int i = 0; i < 1000; i++) { LDValue value = LDValue.of("value-" + i); - LDUser user = new LDUser.Builder("key").custom(CLAUSE_MATCH_ATTRIBUTE, value).build(); + LDContext context = LDContext.builder("key").set(CLAUSE_MATCH_ATTRIBUTE, value).build(); CLAUSE_MATCH_VALUES.add(value); - CLAUSE_MATCH_VALUE_USERS.add(user); + CLAUSE_MATCH_VALUE_CONTEXTS.add(context); } } public static final LDValue NOT_MATCHED_VALUE = LDValue.of("no-match"); - public static final LDUser NOT_MATCHED_VALUE_USER = - new LDUser.Builder("key").custom(CLAUSE_MATCH_ATTRIBUTE, NOT_MATCHED_VALUE).build(); + public static final LDContext NOT_MATCHED_VALUE_CONTEXT = + LDContext.builder("key").set(CLAUSE_MATCH_ATTRIBUTE, NOT_MATCHED_VALUE).build(); public static final String EMPTY_JSON_DATA = "{\"flags\":{},\"segments\":{}}"; @@ -72,7 +71,7 @@ public static List makeTestFlags() { FeatureFlag targetsFlag = flagBuilder(FLAG_WITH_TARGET_LIST_KEY) .on(true) - .targets(new Target(new HashSet(TARGETED_USER_KEYS), 1)) + .targets(new Target(null, new HashSet(TARGETED_USER_KEYS), 1)) .fallthroughVariation(0) .offVariation(0) .variations(LDValue.of(false), LDValue.of(true)) @@ -95,7 +94,7 @@ public static List makeTestFlags() { .build(); flags.add(flagWithPrereq); - UserAttribute matchAttr = UserAttribute.forName(CLAUSE_MATCH_ATTRIBUTE); + AttributeRef matchAttr = AttributeRef.fromLiteral(CLAUSE_MATCH_ATTRIBUTE); FeatureFlag flagWithMultiValueClause = flagBuilder(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY) .on(true) .fallthroughVariation(0) @@ -103,7 +102,7 @@ public static List makeTestFlags() { .variations(LDValue.of(false), LDValue.of(true)) .rules( ruleBuilder() - .clauses(new DataModel.Clause(matchAttr, DataModel.Operator.in, CLAUSE_MATCH_VALUES, false)) + .clauses(new DataModel.Clause(null, matchAttr, DataModel.Operator.in, CLAUSE_MATCH_VALUES, false)) .build() ) .build(); @@ -115,12 +114,4 @@ public static List makeTestFlags() { public static final int TEST_EVENTS_COUNT = 1000; public static final LDValue CUSTOM_EVENT_DATA = LDValue.of("data"); - - public static final Event.Custom CUSTOM_EVENT = new Event.Custom( - System.currentTimeMillis(), - "event-key", - BASIC_USER, - CUSTOM_EVENT_DATA, - null - ); } diff --git a/build.gradle b/build.gradle index acac74318..2a93647ee 100644 --- a/build.gradle +++ b/build.gradle @@ -72,10 +72,11 @@ ext.versions = [ "gson": "2.8.9", "guava": "30.1-jre", "jackson": "2.11.2", - "launchdarklyJavaSdkCommon": "1.3.0", + "launchdarklyJavaSdkCommon": "2.0.0", + "launchdarklyJavaSdkInternal": "1.0.0", "launchdarklyLogging": "1.1.0", "okhttp": "4.9.3", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource - "okhttpEventsource": "2.6.2", + "okhttpEventsource": "3.0.0", "slf4j": "1.7.21", "snakeyaml": "1.32", "jedis": "2.9.0" @@ -112,6 +113,7 @@ ext.versions = [ // headers for it. libraries.internal = [ "com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}", + "com.launchdarkly:launchdarkly-java-sdk-internal:${versions.launchdarklyJavaSdkInternal}", "com.launchdarkly:launchdarkly-logging:${versions.launchdarklyLogging}", "commons-codec:commons-codec:${versions.commonsCodec}", "com.google.code.gson:gson:${versions.gson}", @@ -121,23 +123,6 @@ libraries.internal = [ "org.yaml:snakeyaml:${versions.snakeyaml}", ] -// Add dependencies to "libraries.external" that are exposed in our public API, or that have -// global state that must be shared between the SDK and the caller. Putting dependencies -// here has the following effects: -// -// 1. They are embedded only in the "all" jar. -// -// 2. They are not renamed/shaded, and references to them (in any jar) are not modified. -// -// 3. They *do* appear as dependencies in pom.xml. -// -// 4. In OSGi manifests, they are declared as package imports-- and, in the "all" jar, -// also as package exports (i.e. it provides them if a newer version is not available -// from an import). -libraries.external = [ - "org.slf4j:slf4j-api:${versions.slf4j}" -] - // Add dependencies to "libraries.optional" that are not exposed in our public API and are // *not* embedded in the SDK jar. These are for optional things that will only work if // they are already in the application classpath; we do not want show them as a dependency @@ -154,7 +139,8 @@ libraries.external = [ // 4. In OSGi manifests, they are declared as optional package imports. libraries.optional = [ "com.fasterxml.jackson.core:jackson-core:${versions.jackson}", - "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}" + "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}", + "org.slf4j:slf4j-api:${versions.slf4j}" ] // Add dependencies to "libraries.test" that are used only in unit tests. @@ -162,10 +148,9 @@ libraries.test = [ "org.hamcrest:hamcrest-all:1.3", "org.easymock:easymock:3.4", "junit:junit:4.12", - "ch.qos.logback:logback-classic:1.1.7", "com.fasterxml.jackson.core:jackson-core:${versions.jackson}", "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}", - "com.launchdarkly:test-helpers:1.1.0" + "com.launchdarkly:test-helpers:1.3.0" ] configurations { @@ -173,29 +158,18 @@ configurations { // "implementation", because "implementation" has special behavior in Gradle that prevents us // from referencing it the way we do in shadeDependencies(). internal.extendsFrom implementation - external.extendsFrom api optional - imports } dependencies { implementation libraries.internal - api libraries.external - testImplementation libraries.test, libraries.internal, libraries.external + testImplementation libraries.test, libraries.internal optional libraries.optional internal libraries.internal - external libraries.external commonClasses "com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}" commonDoc "com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}:sources" - - // We are *not* using the special "shadow" configuration that the Shadow plugin defines. - // It's meant to provide similar behavior to what we've defined for "external", but it - // also has other behaviors that we don't want (it would cause dependencies to appear in - // pom.xml and also in a Class-Path attribute). - - imports libraries.external } checkstyle { @@ -209,11 +183,6 @@ task generateJava(type: Copy) { filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [VERSION: version.toString()]) } -tasks.compileJava { - // See note in build-shared.gradle on the purpose of "privateImplementation" - classpath = configurations.internal + configurations.external -} - compileJava.dependsOn 'generateJava' jar { @@ -232,18 +201,14 @@ jar { } // This builds the default uberjar that contains all of our dependencies in shaded form, -// as well as com.launchdarkly.logging in unshaded form, but does not contain SLF4J. The -// user is expected to provide SLF4J. +// as well as com.launchdarkly.logging in unshaded form. It does not contain SLF4J; the +// application is expected to provide SLF4J in the classpath if desired. shadowJar { // No classifier means that the shaded jar becomes the default artifact classifier = '' configurations = [ project.configurations.internal ] - dependencies { - exclude(dependency('org.slf4j:.*:.*')) - } - // Kotlin metadata for shaded classes should not be included - it confuses IDEs exclude '**/*.kotlin_metadata' exclude '**/*.kotlin_module' @@ -259,7 +224,7 @@ shadowJar { shadeDependencies(project.tasks.shadowJar) // Note that "configurations.shadow" is the same as "libraries.external", except it contains // objects with detailed information about the resolved dependencies. - addOsgiManifest(project.tasks.shadowJar, [ project.configurations.imports ], []) + addOsgiManifest(project.tasks.shadowJar, [], []) } doLast { @@ -267,43 +232,6 @@ shadowJar { } } -// This builds the "-all"/"fat" jar, which is the same as the default uberjar except that -// SLF4J is also bundled and exposed (unshaded). -task shadowJarAll(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - classifier = 'all' - group = "shadow" - description = "Builds a Shaded fat jar including SLF4J" - from(project.convention.getPlugin(JavaPluginConvention).sourceSets.main.output) - - configurations = [ project.configurations.internal, project.configurations.external ] - - exclude('META-INF/INDEX.LIST', 'META-INF/*.SF', 'META-INF/*.DSA', 'META-INF/*.RSA') - exclude '**/*.kotlin_metadata' - exclude '**/*.kotlin_module' - exclude '**/*.kotlin_builtins' - - exclude '**/module-info.class' - - dependencies { - // We don't need to exclude anything here, because we want everything to be - // embedded in the "all" jar. - } - - // doFirst causes the following steps to be run during Gradle's execution phase rather than the - // configuration phase; this is necessary because they access the build products - doFirst { - shadeDependencies(project.tasks.shadowJarAll) - // The "all" jar exposes its bundled SLF4j and launchdarkly-logging dependencies as - // exports - but, like the default jar, it *also* imports them ("self-wiring"), which - // allows the bundle to use a higher version if one is provided by another bundle. - addOsgiManifest(project.tasks.shadowJarAll, [ project.configurations.imports ], [ project.configurations.imports ]) - } - - doLast { - replaceUnshadedClasses(project.tasks.shadowJarAll) - } -} - task testJar(type: Jar, dependsOn: testClasses) { classifier = 'test' from sourceSets.test.output @@ -379,15 +307,11 @@ def getPackagesInDependencyJar(jarFile) { } } -// Used by shadowJar and shadowJarAll to specify which packages should be renamed. +// Used by shadowJar to specify which packages should be renamed. // // The SDK's own packages should not be renamed (even though code in those packages will be // modified to update any references to classes that are being renamed). // -// Dependencies that are specified in the "external" configuration should not be renamed. -// These are things that (in some distributions) might be included in our uberjar, but need -// to retain their original names so they can be seen by application code. -// // Dependencies that are specified in the "optional" configuration should not be renamed. // These are things that we will not be including in our uberjar anyway, but we want to make // sure we can reference them by their original names if they are in the application @@ -397,19 +321,13 @@ def getPackagesInDependencyJar(jarFile) { // phase; instead we have to run it after configuration, with the "afterEvaluate" block below. def shadeDependencies(jarTask) { def excludePackages = getAllSdkPackages() + - configurations.external.collectMany { getPackagesInDependencyJar(it) } + configurations.optional.collectMany { getPackagesInDependencyJar(it) } - def topLevelPackages = + def referencedPackages = configurations.internal.collectMany { - getPackagesInDependencyJar(it).collect { it.contains(".") ? it.substring(0, it.indexOf(".")) : it } + getPackagesInDependencyJar(it) }. unique() - topLevelPackages.forEach { top -> - // This special-casing for javax.annotation is hacky, but the issue is that Guava pulls in a jsr305 - // implementation jar that provides javax.annotation, and we *do* want to embed and shade those classes - // so that Guava won't fail to find them and they won't conflict with anyone else's version - but we do - // *not* want references to any classes from javax.net, javax.security, etc. to be munged. - def packageToRelocate = (top == "javax") ? "javax.annotation" : top + referencedPackages.forEach { packageToRelocate -> jarTask.relocate(packageToRelocate, "com.launchdarkly.shaded." + packageToRelocate) { excludePackages.forEach { exclude(it + ".*") } } @@ -474,7 +392,8 @@ def addOsgiManifest(jarTask, List importConfigs, List optImports += p } imports += (optImports.join(";") + ";resolution:=optional" ) @@ -533,7 +452,7 @@ def getOsgiPackageExportsFromJar(file) { } artifacts { - archives jar, sourcesJar, javadocJar, shadowJar, shadowJarAll + archives jar, sourcesJar, javadocJar, shadowJar } test { @@ -655,31 +574,11 @@ publishing { artifact jar artifact sourcesJar artifact javadocJar - artifact shadowJarAll artifact testJar pom.withXml { def root = asNode() root.appendNode('description', 'Official LaunchDarkly SDK for Java') - - // Here we need to add dependencies explicitly to the pom. The mechanism - // that the Shadow plugin provides-- adding dependencies to a configuration - // called "shadow"-- is not quite what we want, for the reasons described - // in the comments for "libraries.external" etc. (and also because of - // the known issue https://github.com/johnrengelman/shadow/issues/321). - // So we aren't using that. - if (root.getAt('dependencies') == null) { - root.appendNode('dependencies') - } - def dependenciesNode = root.getAt('dependencies').get(0) - configurations.external.getAllDependencies().each { dep -> - def dependencyNode = dependenciesNode.appendNode('dependency') - dependencyNode.appendNode('groupId', dep.group) - dependencyNode.appendNode('artifactId', dep.name) - dependencyNode.appendNode('version', dep.version) - dependencyNode.appendNode('scope', 'compile') - } - root.children().last() + pomConfig } } @@ -716,8 +615,7 @@ def shouldSkipSigning() { // dependencies of the SDK, so they can be put on the classpath as needed during tests. task exportDependencies(type: Copy, dependsOn: compileJava) { into "packaging-test/temp/dependencies-all" - from (configurations.internal.resolvedConfiguration.resolvedArtifacts.collect { it.file } - + configurations.external.resolvedConfiguration.resolvedArtifacts.collect { it.file }) + from (configurations.internal.resolvedConfiguration.resolvedArtifacts.collect { it.file }) } gitPublish { diff --git a/contract-tests/service/src/main/java/sdktest/BigSegmentStoreFixture.java b/contract-tests/service/src/main/java/sdktest/BigSegmentStoreFixture.java index c542c4156..f19bc61ff 100644 --- a/contract-tests/service/src/main/java/sdktest/BigSegmentStoreFixture.java +++ b/contract-tests/service/src/main/java/sdktest/BigSegmentStoreFixture.java @@ -1,10 +1,10 @@ package sdktest; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.Membership; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.StoreMetadata; -import com.launchdarkly.sdk.server.interfaces.ClientContext; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.Membership; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.StoreMetadata; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; import java.io.IOException; @@ -12,7 +12,7 @@ import sdktest.CallbackRepresentations.BigSegmentStoreGetMembershipResponse; import sdktest.CallbackRepresentations.BigSegmentStoreGetMetadataResponse; -public class BigSegmentStoreFixture implements BigSegmentStore, BigSegmentStoreFactory { +public class BigSegmentStoreFixture implements BigSegmentStore, ComponentConfigurer { private final CallbackService service; public BigSegmentStoreFixture(CallbackService service) { @@ -25,9 +25,9 @@ public void close() throws IOException { } @Override - public Membership getMembership(String userHash) { + public Membership getMembership(String contextHash) { BigSegmentStoreGetMembershipParams params = new BigSegmentStoreGetMembershipParams(); - params.userHash = userHash; + params.contextHash = contextHash; BigSegmentStoreGetMembershipResponse resp = service.post("/getMembership", params, BigSegmentStoreGetMembershipResponse.class); return new Membership() { @@ -46,7 +46,7 @@ public StoreMetadata getMetadata() { } @Override - public BigSegmentStore createBigSegmentStore(ClientContext context) { + public BigSegmentStore build(ClientContext context) { return this; } } diff --git a/contract-tests/service/src/main/java/sdktest/CallbackRepresentations.java b/contract-tests/service/src/main/java/sdktest/CallbackRepresentations.java index aea243f54..e4791a165 100644 --- a/contract-tests/service/src/main/java/sdktest/CallbackRepresentations.java +++ b/contract-tests/service/src/main/java/sdktest/CallbackRepresentations.java @@ -8,7 +8,7 @@ public static class BigSegmentStoreGetMetadataResponse { } public static class BigSegmentStoreGetMembershipParams { - String userHash; + String contextHash; } public static class BigSegmentStoreGetMembershipResponse { diff --git a/contract-tests/service/src/main/java/sdktest/Representations.java b/contract-tests/service/src/main/java/sdktest/Representations.java index 250f87f99..524887932 100644 --- a/contract-tests/service/src/main/java/sdktest/Representations.java +++ b/contract-tests/service/src/main/java/sdktest/Representations.java @@ -1,10 +1,13 @@ package sdktest; +import com.google.gson.annotations.SerializedName; import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import java.net.URI; +import java.util.Map; public abstract class Representations { public static class Status { @@ -41,7 +44,6 @@ public static class SdkConfigEventParams { boolean enableDiagnostics; String[] globalPrivateAttributes; Long flushIntervalMs; - boolean inlineUsers; } public static class SdkConfigBigSegmentsParams { @@ -69,11 +71,14 @@ public static class CommandParams { EvaluateAllFlagsParams evaluateAll; IdentifyEventParams identifyEvent; CustomEventParams customEvent; - AliasEventParams aliasEvent; + ContextBuildParams contextBuild; + ContextConvertParams contextConvert; + SecureModeHashParams secureModeHash; } public static class EvaluateFlagParams { String flagKey; + LDContext context; LDUser user; String valueType; LDValue value; @@ -88,6 +93,7 @@ public static class EvaluateFlagResponse { } public static class EvaluateAllFlagsParams { + LDContext context; LDUser user; boolean clientSideOnly; boolean detailsOnlyForTrackedFlags; @@ -99,24 +105,53 @@ public static class EvaluateAllFlagsResponse { } public static class IdentifyEventParams { + LDContext context; LDUser user; } public static class CustomEventParams { String eventKey; + LDContext context; LDUser user; LDValue data; boolean omitNullData; Double metricValue; } - public static class AliasEventParams { - LDUser user; - LDUser previousUser; - } - public static class GetBigSegmentsStoreStatusResponse { boolean available; boolean stale; } + + public static class ContextBuildParams { + ContextBuildSingleParams single; + ContextBuildSingleParams[] multi; + } + + public static class ContextBuildSingleParams { + public String kind; + public String key; + public String name; + public Boolean anonymous; + @SerializedName("private") public String[] privateAttrs; + public Map custom; + } + + public static class ContextBuildResponse { + String output; + String error; + } + + public static class ContextConvertParams { + String input; + } + + public static class SecureModeHashParams { + LDContext context; + LDUser user; + } + + public static class SecureModeHashResponse { + String result; + } } diff --git a/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java b/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java index 89658be12..ae78338d0 100644 --- a/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java +++ b/contract-tests/service/src/main/java/sdktest/SdkClientEntity.java @@ -1,6 +1,9 @@ package sdktest; +import com.launchdarkly.sdk.ContextBuilder; +import com.launchdarkly.sdk.ContextMultiBuilder; import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.json.JsonSerialization; import com.launchdarkly.sdk.server.Components; @@ -21,9 +24,13 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; -import sdktest.Representations.AliasEventParams; import sdktest.Representations.CommandParams; +import sdktest.Representations.ContextBuildParams; +import sdktest.Representations.ContextBuildResponse; +import sdktest.Representations.ContextBuildSingleParams; +import sdktest.Representations.ContextConvertParams; import sdktest.Representations.CreateInstanceParams; import sdktest.Representations.CustomEventParams; import sdktest.Representations.EvaluateAllFlagsParams; @@ -33,6 +40,8 @@ import sdktest.Representations.GetBigSegmentsStoreStatusResponse; import sdktest.Representations.IdentifyEventParams; import sdktest.Representations.SdkConfigParams; +import sdktest.Representations.SecureModeHashParams; +import sdktest.Representations.SecureModeHashResponse; public class SdkClientEntity { private final LDClient client; @@ -62,9 +71,6 @@ public Object doCommand(CommandParams params) throws TestService.BadRequestExcep case "customEvent": doCustomEvent(params.customEvent); return null; - case "aliasEvent": - doAliasEvent(params.aliasEvent); - return null; case "flushEvents": client.flush(); return null; @@ -74,6 +80,12 @@ public Object doCommand(CommandParams params) throws TestService.BadRequestExcep resp.available = status.isAvailable(); resp.stale = status.isStale(); return resp; + case "contextBuild": + return doContextBuild(params.contextBuild); + case "contextConvert": + return doContextConvert(params.contextConvert); + case "secureModeHash": + return doSecureModeHash(params.secureModeHash); default: throw new TestService.BadRequestException("unknown command: " + params.command); } @@ -85,32 +97,37 @@ private EvaluateFlagResponse doEvaluateFlag(EvaluateFlagParams params) { EvaluationDetail genericResult; switch (params.valueType) { case "bool": - EvaluationDetail boolResult = client.boolVariationDetail(params.flagKey, - params.user, params.defaultValue.booleanValue()); + EvaluationDetail boolResult = params.user == null ? + client.boolVariationDetail(params.flagKey, params.context, params.defaultValue.booleanValue()) : + client.boolVariationDetail(params.flagKey, params.user, params.defaultValue.booleanValue()); resp.value = LDValue.of(boolResult.getValue()); genericResult = boolResult; break; case "int": - EvaluationDetail intResult = client.intVariationDetail(params.flagKey, - params.user, params.defaultValue.intValue()); + EvaluationDetail intResult = params.user == null ? + client.intVariationDetail(params.flagKey, params.context, params.defaultValue.intValue()) : + client.intVariationDetail(params.flagKey, params.user, params.defaultValue.intValue()); resp.value = LDValue.of(intResult.getValue()); genericResult = intResult; break; case "double": - EvaluationDetail doubleResult = client.doubleVariationDetail(params.flagKey, - params.user, params.defaultValue.doubleValue()); + EvaluationDetail doubleResult = params.user == null ? + client.doubleVariationDetail(params.flagKey, params.context, params.defaultValue.doubleValue()) : + client.doubleVariationDetail(params.flagKey, params.user, params.defaultValue.doubleValue()); resp.value = LDValue.of(doubleResult.getValue()); genericResult = doubleResult; break; case "string": - EvaluationDetail stringResult = client.stringVariationDetail(params.flagKey, - params.user, params.defaultValue.stringValue()); + EvaluationDetail stringResult = params.user == null ? + client.stringVariationDetail(params.flagKey, params.context, params.defaultValue.stringValue()) : + client.stringVariationDetail(params.flagKey, params.user, params.defaultValue.stringValue()); resp.value = LDValue.of(stringResult.getValue()); genericResult = stringResult; break; default: - EvaluationDetail anyResult = client.jsonValueVariationDetail(params.flagKey, - params.user, params.defaultValue); + EvaluationDetail anyResult = params.user == null ? + client.jsonValueVariationDetail(params.flagKey, params.context, params.defaultValue) : + client.jsonValueVariationDetail(params.flagKey, params.user, params.defaultValue); resp.value = anyResult.getValue(); genericResult = anyResult; break; @@ -121,19 +138,29 @@ private EvaluateFlagResponse doEvaluateFlag(EvaluateFlagParams params) { } else { switch (params.valueType) { case "bool": - resp.value = LDValue.of(client.boolVariation(params.flagKey, params.user, params.defaultValue.booleanValue())); + resp.value = LDValue.of(params.user == null ? + client.boolVariation(params.flagKey, params.context, params.defaultValue.booleanValue()) : + client.boolVariation(params.flagKey, params.user, params.defaultValue.booleanValue())); break; case "int": - resp.value = LDValue.of(client.intVariation(params.flagKey, params.user, params.defaultValue.intValue())); + resp.value = LDValue.of(params.user == null ? + client.intVariation(params.flagKey, params.context, params.defaultValue.intValue()) : + client.intVariation(params.flagKey, params.user, params.defaultValue.intValue())); break; case "double": - resp.value = LDValue.of(client.doubleVariation(params.flagKey, params.user, params.defaultValue.doubleValue())); + resp.value = LDValue.of(params.user == null ? + client.doubleVariation(params.flagKey, params.context, params.defaultValue.doubleValue()) : + client.doubleVariation(params.flagKey, params.user, params.defaultValue.doubleValue())); break; case "string": - resp.value = LDValue.of(client.stringVariation(params.flagKey, params.user, params.defaultValue.stringValue())); + resp.value = LDValue.of(params.user == null ? + client.stringVariation(params.flagKey, params.context, params.defaultValue.stringValue()) : + client.stringVariation(params.flagKey, params.user, params.defaultValue.stringValue())); break; default: - resp.value = client.jsonValueVariation(params.flagKey, params.user, params.defaultValue); + resp.value = params.user == null ? + client.jsonValueVariation(params.flagKey, params.context, params.defaultValue) : + client.jsonValueVariation(params.flagKey, params.user, params.defaultValue); break; } } @@ -151,28 +178,100 @@ private EvaluateAllFlagsResponse doEvaluateAll(EvaluateAllFlagsParams params) { if (params.withReasons) { options.add(FlagsStateOption.WITH_REASONS); } - FeatureFlagsState state = client.allFlagsState(params.user, options.toArray(new FlagsStateOption[0])); + FeatureFlagsState state; + if (params.user == null) { + state = client.allFlagsState(params.context, options.toArray(new FlagsStateOption[0])); + } else { + state = client.allFlagsState(params.user, options.toArray(new FlagsStateOption[0])); + } EvaluateAllFlagsResponse resp = new EvaluateAllFlagsResponse(); resp.state = LDValue.parse(JsonSerialization.serialize(state)); return resp; } private void doIdentifyEvent(IdentifyEventParams params) { - client.identify(params.user); + if (params.user == null) { + client.identify(params.context); + } else { + client.identify(params.user); + } } private void doCustomEvent(CustomEventParams params) { if ((params.data == null || params.data.isNull()) && params.omitNullData && params.metricValue == null) { - client.track(params.eventKey, params.user); + if (params.user == null) { + client.track(params.eventKey, params.context); + } else { + client.track(params.eventKey, params.user); + } } else if (params.metricValue == null) { - client.trackData(params.eventKey, params.user, params.data); + if (params.user == null) { + client.trackData(params.eventKey, params.context, params.data); + } else { + client.trackData(params.eventKey, params.user, params.data); + } } else { - client.trackMetric(params.eventKey, params.user, params.data, params.metricValue.doubleValue()); + if (params.user == null) { + client.trackMetric(params.eventKey, params.context, params.data, params.metricValue.doubleValue()); + } else { + client.trackMetric(params.eventKey, params.user, params.data, params.metricValue.doubleValue()); + } } } - private void doAliasEvent(AliasEventParams params) { - client.alias(params.user, params.previousUser); + private ContextBuildResponse doContextBuild(ContextBuildParams params) { + LDContext c; + if (params.multi == null) { + c = doContextBuildSingle(params.single); + } else { + ContextMultiBuilder b = LDContext.multiBuilder(); + for (ContextBuildSingleParams s: params.multi) { + b.add(doContextBuildSingle(s)); + } + c = b.build(); + } + ContextBuildResponse resp = new ContextBuildResponse(); + if (c.isValid()) { + resp.output = JsonSerialization.serialize(c); + } else { + resp.error = c.getError(); + } + return resp; + } + + private LDContext doContextBuildSingle(ContextBuildSingleParams params) { + ContextBuilder b = LDContext.builder(params.key) + .kind(params.kind) + .name(params.name); + if (params.anonymous != null) { + b.anonymous(params.anonymous.booleanValue()); + } + if (params.custom != null) { + for (Map.Entry kv: params.custom.entrySet()) { + b.set(kv.getKey(), kv.getValue()); + } + } + if (params.privateAttrs != null) { + b.privateAttributes(params.privateAttrs); + } + return b.build(); + } + + private ContextBuildResponse doContextConvert(ContextConvertParams params) { + ContextBuildResponse resp = new ContextBuildResponse(); + try { + LDContext c = JsonSerialization.deserialize(params.input, LDContext.class); + resp.output = JsonSerialization.serialize(c); + } catch (Exception e) { + resp.error = e.getMessage(); + } + return resp; + } + + private SecureModeHashResponse doSecureModeHash(SecureModeHashParams params) { + SecureModeHashResponse resp = new SecureModeHashResponse(); + resp.result = params.user == null ? client.secureModeHash(params.context) : client.secureModeHash(params.user); + return resp; } public void close() { @@ -193,9 +292,11 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) { builder.startWait(Duration.ofMillis(params.startWaitTimeMs.longValue())); } + ServiceEndpointsBuilder endpoints = Components.serviceEndpoints(); + if (params.streaming != null) { - StreamingDataSourceBuilder dataSource = Components.streamingDataSource() - .baseURI(params.streaming.baseUri); + endpoints.streaming(params.streaming.baseUri); + StreamingDataSourceBuilder dataSource = Components.streamingDataSource(); if (params.streaming.initialRetryDelayMs > 0) { dataSource.initialReconnectDelay(Duration.ofMillis(params.streaming.initialRetryDelayMs)); } @@ -205,10 +306,9 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) { if (params.events == null) { builder.events(Components.noEvents()); } else { + endpoints.events(params.events.baseUri); EventProcessorBuilder eb = Components.sendEvents() - .baseURI(params.events.baseUri) - .allAttributesPrivate(params.events.allAttributesPrivate) - .inlineUsersInEvents(params.events.inlineUsers); + .allAttributesPrivate(params.events.allAttributesPrivate); if (params.events.capacity > 0) { eb.capacity(params.events.capacity); } @@ -216,7 +316,7 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) { eb.flushInterval(Duration.ofMillis(params.events.flushIntervalMs.longValue())); } if (params.events.globalPrivateAttributes != null) { - eb.privateAttributeNames(params.events.globalPrivateAttributes); + eb.privateAttributes(params.events.globalPrivateAttributes); } builder.events(eb); builder.diagnosticOptOut(!params.events.enableDiagnostics); @@ -252,13 +352,17 @@ private LDConfig buildSdkConfig(SdkConfigParams params, String tag) { } if (params.serviceEndpoints != null) { - builder.serviceEndpoints( - Components.serviceEndpoints() - .streaming(params.serviceEndpoints.streaming) - .polling(params.serviceEndpoints.polling) - .events(params.serviceEndpoints.events) - ); + if (params.serviceEndpoints.streaming != null) { + endpoints.streaming(params.serviceEndpoints.streaming); + } + if (params.serviceEndpoints.polling != null) { + endpoints.polling(params.serviceEndpoints.polling); + } + if (params.serviceEndpoints.events != null) { + endpoints.events(params.serviceEndpoints.events); + } } + builder.serviceEndpoints(endpoints); return builder.build(); } diff --git a/contract-tests/service/src/main/java/sdktest/TestService.java b/contract-tests/service/src/main/java/sdktest/TestService.java index dfc8dd89f..c4a8bd6c3 100644 --- a/contract-tests/service/src/main/java/sdktest/TestService.java +++ b/contract-tests/service/src/main/java/sdktest/TestService.java @@ -2,6 +2,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.launchdarkly.sdk.server.LDClient; +import com.launchdarkly.sdk.server.LDConfig; import com.launchdarkly.testhelpers.httptest.Handlers; import com.launchdarkly.testhelpers.httptest.HttpServer; import com.launchdarkly.testhelpers.httptest.RequestContext; @@ -27,8 +29,10 @@ public class TestService { "all-flags-details-only-for-tracked-flags", "all-flags-with-reasons", "big-segments", + "context-type", + "service-endpoints", "tags", - "service-endpoints" + "user-type" }; static final Gson gson = new GsonBuilder().serializeNulls().create(); @@ -37,6 +41,15 @@ public class TestService { private final Map clients = new ConcurrentHashMap(); private final AtomicInteger clientCounter = new AtomicInteger(0); + private final String clientVersion; + + private TestService() { + LDClient dummyClient = new LDClient("", new LDConfig.Builder().offline(true).build()); + clientVersion = dummyClient.version(); + try { + dummyClient.close(); + } catch (Exception e) {} + } @SuppressWarnings("serial") public static class BadRequestException extends Exception { @@ -67,6 +80,7 @@ public static void main(String[] args) { private Status getStatus() { Status rep = new Status(); rep.capabilities = CAPABILITIES; + rep.clientVersion = clientVersion; return rep; } diff --git a/gradle.properties b/gradle.properties index 29efee2eb..613b1b182 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=5.10.3 +version=6.0.0-SNAPSHOT # The following empty ossrh properties are used by LaunchDarkly's internal integration testing framework # and should not be needed for typical development purposes (including by third-party developers). ossrhUsername= diff --git a/packaging-test/Makefile b/packaging-test/Makefile index 32dafe245..bd48043a7 100644 --- a/packaging-test/Makefile +++ b/packaging-test/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean test-all-jar test-default-jar test-thin-jar test-all-jar-classes test-default-jar-classes test-thin-jar-classes +.PHONY: all clean test-default-jar test-thin-jar test-default-jar-classes test-thin-jar-classes # Running "make all" will verify the following things about the SDK jar distributions: # - Each jar contains the expected set of classes, broadly speaking. @@ -41,18 +41,17 @@ export TEMP_BUNDLE_DIR=$(FELIX_DIR)/app-bundles # the OSGi test). Note that we're assuming that all of the SDK's dependencies have built-in support # for OSGi, which is currently true; if that weren't true, we would have to do something different # to put them on the system classpath in the OSGi test. -RUN_JARS_test-all-jar=$(TEST_APP_JAR) $(SDK_ALL_JAR) \ - $(shell ls $(TEMP_DIR)/dependencies-external/gson*.jar 2>/dev/null) \ - $(shell ls $(TEMP_DIR)/dependencies-external/jackson*.jar 2>/dev/null) RUN_JARS_test-default-jar=$(TEST_APP_JAR) $(SDK_DEFAULT_JAR) \ $(shell ls $(TEMP_DIR)/dependencies-external/*.jar 2>/dev/null) RUN_JARS_test-thin-jar=$(TEST_APP_JAR) $(SDK_THIN_JAR) \ $(shell ls $(TEMP_DIR)/dependencies-internal/*.jar 2>/dev/null) \ $(shell ls $(TEMP_DIR)/dependencies-external/*.jar 2>/dev/null) +# grep variants with lookahead aren't universally available +PGREP=$(if $(shell echo a | grep -P . 2>/dev/null),grep -P,$(if $(shell which ggrep),ggrep -P,echo This makefile requires grep -P or ggrep -P && exit 1;)) classes_prepare=echo " checking $(1)..." && $(JAR) tf $(1) | grep '\.class$$' >$(TEMP_OUTPUT) -classes_should_contain=echo " should contain $(2)" && grep "^$(1)/.*\.class$$" $(TEMP_OUTPUT) >/dev/null -classes_should_not_contain=echo " should not contain $(2)" && ! grep "^$(1)/.*\.class$$" $(TEMP_OUTPUT) +classes_should_contain=echo " should contain $(2)" && $(PGREP) "^$(1)/.*\.class$$" $(TEMP_OUTPUT) >/dev/null +classes_should_not_contain=echo " should not contain $(2)" && ! $(PGREP) "^$(1)/.*\.class$$" $(TEMP_OUTPUT) should_not_have_module_info=echo " should not have module-info.class" && ! grep "module-info\.class$$" $(TEMP_OUTPUT) verify_sdk_classes= \ @@ -69,7 +68,7 @@ manifest_should_not_have_classpath= \ caption=echo "" && echo "$(1)" -all: test-all-jar test-default-jar test-thin-jar test-pom +all: test-default-jar test-thin-jar test-pom clean: rm -rf $(TEMP_DIR)/* @@ -78,38 +77,23 @@ clean: # SECONDEXPANSION is needed so we can use "$@" inside a variable in the prerequisite list of the test targets .SECONDEXPANSION: -test-all-jar test-default-jar test-thin-jar: $$@-classes $(TEST_APP_JAR) get-sdk-dependencies $$(RUN_JARS_$$@) $(FELIX_DIR) +test-default-jar test-thin-jar: $$@-classes $(TEST_APP_JAR) get-sdk-dependencies $$(RUN_JARS_$$@) $(FELIX_DIR) @$(call caption,$@) @./run-non-osgi-test.sh $(RUN_JARS_$@) @./run-osgi-test.sh $(RUN_JARS_$@) -test-all-jar-classes: $(SDK_ALL_JAR) $(TEMP_DIR) - @$(call caption,$@) - @$(call classes_prepare,$<) - @$(call verify_sdk_classes) - @$(call classes_should_contain,com/launchdarkly/logging,unshaded com.launchdarkly.logging classes) - @$(call classes_should_contain,org/slf4j,unshaded SLF4j) - @$(call classes_should_not_contain,com/launchdarkly/shaded/com/launchdarkly/sdk,shaded SDK classes) - @$(call classes_should_contain,com/launchdarkly/shaded/com/google/gson,shaded Gson) - @$(call classes_should_not_contain,com/google/gson,unshaded Gson) - @$(call classes_should_not_contain,com/fasterxml/jackson,unshaded Jackson) - @$(call classes_should_not_contain,com/launchdarkly/shaded/com/fasterxml/jackson,shaded Jackson) - @$(call classes_should_not_contain,com/launchdarkly/shaded/org/slf4j,shaded SLF4j) - @$(call should_not_have_module_info) - @$(call manifest_should_not_have_classpath,$<) - test-default-jar-classes: $(SDK_DEFAULT_JAR) $(TEMP_DIR) @$(call caption,$@) @$(call classes_prepare,$<) @$(call verify_sdk_classes) @$(call classes_should_contain,com/launchdarkly/logging,unshaded com.launchdarkly.logging classes) - @$(call classes_should_not_contain,com/launchdarkly/shaded/com/launchdarkly/sdk,shaded SDK classes) + @$(call classes_should_not_contain,com/launchdarkly/shaded/com/launchdarkly/sdk(?!/internal),shaded non-internal SDK classes) @$(call classes_should_contain,com/launchdarkly/shaded/com/google/gson,shaded Gson) - @$(call classes_should_not_contain,com/launchdarkly/shaded/org/slf4j,shaded SLF4j) @$(call classes_should_not_contain,com/google/gson,unshaded Gson) + @$(call classes_should_not_contain,org/slf4j,unshaded SLF4j) + @$(call classes_should_not_contain,com/launchdarkly/shaded/org/slf4j,shaded SLF4j) @$(call classes_should_not_contain,com/fasterxml/jackson,unshaded Jackson) @$(call classes_should_not_contain,com/launchdarkly/shaded/com/fasterxml/jackson,shaded Jackson) - @$(call classes_should_not_contain,org/slf4j,unshaded SLF4j) @$(call should_not_have_module_info) @$(call manifest_should_not_have_classpath,$<) @@ -127,10 +111,8 @@ test-pom: $(POM_XML) @echo "=== contents of $<" @cat $< @echo "===" - @echo " should have SLF4J dependency" - @grep 'slf4j-api' >/dev/null $< || (echo " FAILED" && exit 1) - @echo " should not have any dependencies other than SLF4J" - @! grep '' $< | grep -v slf4j | grep -v launchdarkly || (echo " FAILED" && exit 1) + @echo " should not have any dependencies" + @! grep '' $< || (echo " FAILED" && exit 1) $(SDK_DEFAULT_JAR) $(SDK_ALL_JAR) $(SDK_THIN_JAR) $(POM_XML): cd .. && ./gradlew publishToMavenLocal -P version=$(LOCAL_VERSION) -P LD_SKIP_SIGNING=1 diff --git a/packaging-test/test-app/src/main/java/testapp/JsonSerializationTestData.java b/packaging-test/test-app/src/main/java/testapp/JsonSerializationTestData.java index 8bd8a9493..d1c037f3e 100644 --- a/packaging-test/test-app/src/main/java/testapp/JsonSerializationTestData.java +++ b/packaging-test/test-app/src/main/java/testapp/JsonSerializationTestData.java @@ -28,8 +28,8 @@ private TestItem(Object objectToSerialize, String expectedJson) { "{\"kind\":\"OFF\"}" ), new TestItem( - new LDUser.Builder("userkey").build(), - "{\"key\":\"userkey\"}" + LDContext.create("userkey"), + "{\"kind\":\"user\",\"key\":\"userkey\"}" ) }; diff --git a/src/main/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapper.java b/src/main/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapper.java index 3a6d14451..7a682c41c 100644 --- a/src/main/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapper.java +++ b/src/main/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapper.java @@ -1,17 +1,15 @@ package com.launchdarkly.sdk.server; -import static com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.createMembershipFromSegmentRefs; - import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider.Status; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider.StatusListener; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.Membership; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.StoreMetadata; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.Membership; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.StoreMetadata; import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; import org.apache.commons.codec.digest.DigestUtils; @@ -26,6 +24,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import static com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.createMembershipFromSegmentRefs; + class BigSegmentStoreWrapper implements Closeable { private final BigSegmentStore store; private final Duration staleAfter; diff --git a/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java b/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java index c3c7ded4f..77089564a 100644 --- a/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/ClientContextImpl.java @@ -1,11 +1,12 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.logging.Logs; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.sdk.server.interfaces.LoggingConfiguration; +import com.launchdarkly.sdk.internal.events.DiagnosticStore; +import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreUpdateSink; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -21,99 +22,95 @@ * implementation of {@link ClientContext}, which might have been created for instance in application * test code). */ -final class ClientContextImpl implements ClientContext { +final class ClientContextImpl extends ClientContext { private static volatile ScheduledExecutorService fallbackSharedExecutor = null; - private final BasicConfiguration basicConfiguration; - private final HttpConfiguration httpConfiguration; - private final LoggingConfiguration loggingConfiguration; final ScheduledExecutorService sharedExecutor; - final DiagnosticAccumulator diagnosticAccumulator; - final DiagnosticEvent.Init diagnosticInitEvent; + final DiagnosticStore diagnosticStore; + final DataSourceUpdateSink dataSourceUpdateSink; + final DataStoreUpdateSink dataStoreUpdateSink; private ClientContextImpl( - BasicConfiguration basicConfiguration, - HttpConfiguration httpConfiguration, - LoggingConfiguration loggingConfiguration, + ClientContext baseContext, ScheduledExecutorService sharedExecutor, - DiagnosticAccumulator diagnosticAccumulator, - DiagnosticEvent.Init diagnosticInitEvent + DiagnosticStore diagnosticStore ) { - this.basicConfiguration = basicConfiguration; - this.httpConfiguration = httpConfiguration; - this.loggingConfiguration = loggingConfiguration; + super(baseContext.getSdkKey(), baseContext.getApplicationInfo(), baseContext.getHttp(), + baseContext.getLogging(), baseContext.isOffline(), baseContext.getServiceEndpoints(), + baseContext.getThreadPriority()); this.sharedExecutor = sharedExecutor; - this.diagnosticAccumulator = diagnosticAccumulator; - this.diagnosticInitEvent = diagnosticInitEvent; + this.diagnosticStore = diagnosticStore; + this.dataSourceUpdateSink = null; + this.dataStoreUpdateSink = null; } - ClientContextImpl( + private ClientContextImpl( + ClientContextImpl copyFrom, + DataSourceUpdateSink dataSourceUpdateSink, + DataStoreUpdateSink dataStoreUpdateSink + ) { + super(copyFrom); + this.dataSourceUpdateSink = dataSourceUpdateSink; + this.dataStoreUpdateSink = dataStoreUpdateSink; + this.diagnosticStore = copyFrom.diagnosticStore; + this.sharedExecutor = copyFrom.sharedExecutor; + } + + ClientContextImpl withDataSourceUpdateSink(DataSourceUpdateSink newDataSourceUpdateSink) { + return new ClientContextImpl(this, newDataSourceUpdateSink, this.dataStoreUpdateSink); + } + + ClientContextImpl withDataStoreUpdateSink(DataStoreUpdateSink newDataStoreUpdateSink) { + return new ClientContextImpl(this, this.dataSourceUpdateSink, newDataStoreUpdateSink); + } + + @Override + public DataSourceUpdateSink getDataSourceUpdateSink() { + return dataSourceUpdateSink; + } + + @Override + public DataStoreUpdateSink getDataStoreUpdateSink() { + return dataStoreUpdateSink; + } + + static ClientContextImpl fromConfig( String sdkKey, - LDConfig configuration, - ScheduledExecutorService sharedExecutor, - DiagnosticAccumulator diagnosticAccumulator - ) { - // There is some temporarily over-elaborate logic here because the component factory interfaces can't - // be updated to make the dependencies more sensible till the next major version. - BasicConfiguration tempBasic = new BasicConfiguration(sdkKey, configuration.offline, configuration.threadPriority, - configuration.applicationInfo, configuration.serviceEndpoints, LDLogger.none()); - this.loggingConfiguration = configuration.loggingConfigFactory.createLoggingConfiguration(tempBasic); - LDLogger baseLogger = LDLogger.withAdapter( - loggingConfiguration.getLogAdapter() == null ? Logs.none() : loggingConfiguration.getLogAdapter(), - loggingConfiguration.getBaseLoggerName() == null ? Loggers.BASE_LOGGER_NAME : - loggingConfiguration.getBaseLoggerName() - ); + LDConfig config, + ScheduledExecutorService sharedExecutor + ) { + ClientContext minimalContext = new ClientContext(sdkKey, config.applicationInfo, null, + null, config.offline, config.serviceEndpoints, config.threadPriority); + LoggingConfiguration loggingConfig = config.logging.build(minimalContext); - this.basicConfiguration = new BasicConfiguration( - sdkKey, - configuration.offline, - configuration.threadPriority, - configuration.applicationInfo, - configuration.serviceEndpoints, - baseLogger - ); + ClientContext contextWithLogging = new ClientContext(sdkKey, config.applicationInfo, null, + loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority); + HttpConfiguration httpConfig = config.http.build(contextWithLogging); - this.httpConfiguration = configuration.httpConfigFactory.createHttpConfiguration(basicConfiguration); - - - if (this.httpConfiguration.getProxy() != null) { - baseLogger.info("Using proxy: {} {} authentication.", - this.httpConfiguration.getProxy(), - this.httpConfiguration.getProxyAuthentication() == null ? "without" : "with"); + if (httpConfig.getProxy() != null) { + contextWithLogging.getBaseLogger().info("Using proxy: {} {} authentication.", + httpConfig.getProxy(), + httpConfig.getProxyAuthentication() == null ? "without" : "with"); } - this.sharedExecutor = sharedExecutor; + ClientContext contextWithHttpAndLogging = new ClientContext(sdkKey, config.applicationInfo, httpConfig, + loggingConfig, config.offline, config.serviceEndpoints, config.threadPriority); - if (!configuration.diagnosticOptOut && diagnosticAccumulator != null) { - this.diagnosticAccumulator = diagnosticAccumulator; - this.diagnosticInitEvent = new DiagnosticEvent.Init( - diagnosticAccumulator.dataSinceDate, - diagnosticAccumulator.diagnosticId, - configuration, - basicConfiguration, - httpConfiguration - ); - } else { - this.diagnosticAccumulator = null; - this.diagnosticInitEvent = null; + // Create a diagnostic store only if diagnostics are enabled. Diagnostics are enabled as long as 1. the + // opt-out property was not set in the config, and 2. we are using the standard event processor. + DiagnosticStore diagnosticStore = null; + if (!config.diagnosticOptOut && config.events instanceof EventProcessorBuilder) { + diagnosticStore = new DiagnosticStore( + ServerSideDiagnosticEvents.getSdkDiagnosticParams(contextWithHttpAndLogging, config)); } + + return new ClientContextImpl( + contextWithHttpAndLogging, + sharedExecutor, + diagnosticStore + ); } - @Override - public BasicConfiguration getBasic() { - return basicConfiguration; - } - - @Override - public HttpConfiguration getHttp() { - return httpConfiguration; - } - - @Override - public LoggingConfiguration getLogging() { - return loggingConfiguration; - } - /** * This mechanism is a convenience for internal components to access the package-private fields of the * context if it is a ClientContextImpl, and to receive null values for those fields if it is not. @@ -130,13 +127,6 @@ static ClientContextImpl get(ClientContext context) { fallbackSharedExecutor = Executors.newSingleThreadScheduledExecutor(); } } - return new ClientContextImpl( - context.getBasic(), - context.getHttp(), - context.getLogging(), - fallbackSharedExecutor, - null, - null - ); + return new ClientContextImpl(context, fallbackSharedExecutor, null); } } diff --git a/src/main/java/com/launchdarkly/sdk/server/Components.java b/src/main/java/com/launchdarkly/sdk/server/Components.java index 0b43e7531..c7111c030 100644 --- a/src/main/java/com/launchdarkly/sdk/server/Components.java +++ b/src/main/java/com/launchdarkly/sdk/server/Components.java @@ -1,7 +1,5 @@ package com.launchdarkly.sdk.server; -import static com.launchdarkly.sdk.server.ComponentsImpl.NULL_EVENT_PROCESSOR_FACTORY; - import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.Logs; import com.launchdarkly.sdk.server.ComponentsImpl.EventProcessorBuilderImpl; @@ -23,12 +21,15 @@ import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder; import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder; import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; -import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory; import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; + +import static com.launchdarkly.sdk.server.ComponentsImpl.NULL_EVENT_PROCESSOR_FACTORY; /** * Provides configurable factories for the standard implementations of LaunchDarkly component interfaces. @@ -38,7 +39,7 @@ * analytics events. For the latter, the standard way to specify a configuration is to call one of the * static methods in {@link Components} (such as {@link #streamingDataSource()}), apply any desired * configuration change to the object that that method returns (such as {@link StreamingDataSourceBuilder#initialReconnectDelay(java.time.Duration)}, - * and then use the corresponding method in {@link LDConfig.Builder} (such as {@link LDConfig.Builder#dataSource(DataSourceFactory)}) + * and then use the corresponding method in {@link LDConfig.Builder} (such as {@link LDConfig.Builder#dataSource(ComponentConfigurer)}) * to use that configured component in the SDK. * * @since 4.0.0 @@ -53,7 +54,7 @@ private Components() {} * LaunchDarkly documentation. *

* After configuring this object, use - * {@link LDConfig.Builder#bigSegments(BigSegmentsConfigurationBuilder)} to store it in your SDK + * {@link LDConfig.Builder#bigSegments(ComponentConfigurer)} to store it in your SDK * configuration. For example, using the Redis integration: * *


@@ -67,17 +68,18 @@ private Components() {}
    * You must always specify the {@code storeFactory} parameter, to tell the SDK what database you
    * are using. Several database integrations exist for the LaunchDarkly SDK, each with its own
    * behavior and options specific to that database; this is described via some implementation of
-   * {@link BigSegmentStoreFactory}. The {@link BigSegmentsConfigurationBuilder} adds configuration
+   * {@link BigSegmentStore}. The {@link BigSegmentsConfigurationBuilder} adds configuration
    * options for aspects of SDK behavior that are independent of the database. In the example above,
    * {@code prefix} is an option specifically for the Redis integration, whereas
    * {@code userCacheSize} is an option that can be used for any data store type.
    *
-   * @param storeFactory the factory for the underlying data store
+   * @param storeConfigurer the factory for the underlying data store
    * @return a {@link BigSegmentsConfigurationBuilder}
    * @since 5.7.0
+   * @see Components#bigSegments(ComponentConfigurer)
    */
-  public static BigSegmentsConfigurationBuilder bigSegments(BigSegmentStoreFactory storeFactory) {
-    return new BigSegmentsConfigurationBuilder(storeFactory);
+  public static BigSegmentsConfigurationBuilder bigSegments(ComponentConfigurer storeConfigurer) {
+    return new BigSegmentsConfigurationBuilder(storeConfigurer);
   }
 
   /**
@@ -87,10 +89,10 @@ public static BigSegmentsConfigurationBuilder bigSegments(BigSegmentStoreFactory
    * a data store instance for testing purposes.
    * 
    * @return a factory object
-   * @see LDConfig.Builder#dataStore(DataStoreFactory)
+   * @see LDConfig.Builder#dataStore(ComponentConfigurer)
    * @since 4.12.0
    */
-  public static DataStoreFactory inMemoryDataStore() {
+  public static ComponentConfigurer inMemoryDataStore() {
     return InMemoryDataStoreFactory.INSTANCE;
   }
 
@@ -117,13 +119,13 @@ public static DataStoreFactory inMemoryDataStore() {
    * For more information on the available persistent data store implementations, see the reference
    * guide on Using a persistent feature store.
    *  
-   * @param storeFactory the factory/builder for the specific kind of persistent data store
+   * @param storeConfigurer the factory/builder for the specific kind of persistent data store
    * @return a {@link PersistentDataStoreBuilder}
-   * @see LDConfig.Builder#dataStore(DataStoreFactory)
+   * @see LDConfig.Builder#dataStore(ComponentConfigurer)
    * @since 4.12.0
    */
-  public static PersistentDataStoreBuilder persistentDataStore(PersistentDataStoreFactory storeFactory) {
-    return new PersistentDataStoreBuilderImpl(storeFactory);
+  public static PersistentDataStoreBuilder persistentDataStore(ComponentConfigurer storeConfigurer) {
+    return new PersistentDataStoreBuilderImpl(storeConfigurer);
   }
   
   /**
@@ -131,7 +133,7 @@ public static PersistentDataStoreBuilder persistentDataStore(PersistentDataStore
    * 

* The default configuration has events enabled with default settings. If you want to * customize this behavior, call this method to obtain a builder, change its properties - * with the {@link EventProcessorBuilder} properties, and pass it to {@link LDConfig.Builder#events(EventProcessorFactory)}: + * with the {@link EventProcessorBuilder} properties, and pass it to {@link LDConfig.Builder#events(ComponentConfigurer)}: *


    *     LDConfig config = new LDConfig.Builder()
    *         .events(Components.sendEvents().capacity(5000).flushIntervalSeconds(2))
@@ -154,7 +156,7 @@ public static EventProcessorBuilder sendEvents() {
   /**
    * Returns a configuration object that disables analytics events.
    * 

- * Passing this to {@link LDConfig.Builder#events(EventProcessorFactory)} causes the SDK + * Passing this to {@link LDConfig.Builder#events(ComponentConfigurer)} causes the SDK * to discard all analytics events and not send them to LaunchDarkly, regardless of any other configuration. *


    *     LDConfig config = new LDConfig.Builder()
@@ -164,26 +166,20 @@ public static EventProcessorBuilder sendEvents() {
    * 
    * @return a factory object
    * @see #sendEvents()
-   * @see LDConfig.Builder#events(EventProcessorFactory)
+   * @see LDConfig.Builder#events(ComponentConfigurer)
    * @since 4.12.0
    */
-  public static EventProcessorFactory noEvents() {
+  public static ComponentConfigurer noEvents() {
     return NULL_EVENT_PROCESSOR_FACTORY;
   }
 
-  // package-private method for verifying that the given EventProcessorFactory is the same kind that is
-  // returned by noEvents() - we can use reference equality here because we know we're using a static instance
-  static boolean isNullImplementation(EventProcessorFactory f) {
-    return f == NULL_EVENT_PROCESSOR_FACTORY;
-  }
-  
   /**
    * 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(DataSourceFactory)}: + * {@link StreamingDataSourceBuilder} methods, and pass it to {@link LDConfig.Builder#dataSource(ComponentConfigurer)}: *

 
    *     LDConfig config = new LDConfig.Builder()
    *         .dataSource(Components.streamingDataSource().initialReconnectDelayMillis(500))
@@ -194,7 +190,7 @@ static boolean isNullImplementation(EventProcessorFactory f) {
    * disable network requests.
    * 
    * @return a builder for setting streaming connection properties
-   * @see LDConfig.Builder#dataSource(DataSourceFactory)
+   * @see LDConfig.Builder#dataSource(ComponentConfigurer)
    * @since 4.12.0
    */
   public static StreamingDataSourceBuilder streamingDataSource() {
@@ -210,7 +206,7 @@ public static StreamingDataSourceBuilder streamingDataSource() {
    * 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(DataSourceFactory)}: + * {@link PollingDataSourceBuilder} methods, and pass it to {@link LDConfig.Builder#dataSource(ComponentConfigurer)}: *


    *     LDConfig config = new LDConfig.Builder()
    *         .dataSource(Components.pollingDataSource().pollIntervalMillis(45000))
@@ -221,7 +217,7 @@ public static StreamingDataSourceBuilder streamingDataSource() {
    * disable network requests.
    * 
    * @return a builder for setting polling properties
-   * @see LDConfig.Builder#dataSource(DataSourceFactory)
+   * @see LDConfig.Builder#dataSource(ComponentConfigurer)
    * @since 4.12.0
    */
   public static PollingDataSourceBuilder pollingDataSource() {
@@ -236,7 +232,7 @@ static PollingDataSourceBuilderImpl pollingDataSourceInternal() {
   /**
    * Returns a configuration object that disables a direct connection with LaunchDarkly for feature flag updates.
    * 

- * Passing this to {@link LDConfig.Builder#dataSource(DataSourceFactory)} causes the SDK + * Passing this to {@link LDConfig.Builder#dataSource(ComponentConfigurer)} 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 @@ -253,16 +249,16 @@ static PollingDataSourceBuilderImpl pollingDataSourceInternal() { * * @return a factory object * @since 4.12.0 - * @see LDConfig.Builder#dataSource(DataSourceFactory) + * @see LDConfig.Builder#dataSource(ComponentConfigurer) */ - public static DataSourceFactory externalUpdatesOnly() { + public static ComponentConfigurer externalUpdatesOnly() { return NullDataSourceFactory.INSTANCE; } /** * Returns a configuration builder for the SDK's networking configuration. *

- * Passing this to {@link LDConfig.Builder#http(com.launchdarkly.sdk.server.interfaces.HttpConfigurationFactory)} + * Passing this to {@link LDConfig.Builder#http(ComponentConfigurer)} * applies this configuration to all HTTP/HTTPS requests made by the SDK. *


    *     LDConfig config = new LDConfig.Builder()
@@ -276,7 +272,7 @@ public static DataSourceFactory externalUpdatesOnly() {
    * 
    * @return a factory object
    * @since 4.13.0
-   * @see LDConfig.Builder#http(com.launchdarkly.sdk.server.interfaces.HttpConfigurationFactory)
+   * @see LDConfig.Builder#http(ComponentConfigurer)
    */
   public static HttpConfigurationBuilder httpConfiguration() {
     return new HttpConfigurationBuilderImpl();
@@ -307,7 +303,7 @@ public static HttpAuthentication httpBasicAuthentication(String username, String
   /**
    * Returns a configuration builder for the SDK's logging configuration.
    * 

- * Passing this to {@link LDConfig.Builder#logging(com.launchdarkly.sdk.server.interfaces.LoggingConfigurationFactory)}, + * Passing this to {@link LDConfig.Builder#logging(ComponentConfigurer)}, * after setting any desired properties on the builder, applies this configuration to the SDK. *


    *     LDConfig config = new LDConfig.Builder()
@@ -320,7 +316,7 @@ public static HttpAuthentication httpBasicAuthentication(String username, String
    * 
    * @return a configuration builder
    * @since 5.0.0
-   * @see LDConfig.Builder#logging(com.launchdarkly.sdk.server.interfaces.LoggingConfigurationFactory)
+   * @see LDConfig.Builder#logging(ComponentConfigurer)
    */
   public static LoggingConfigurationBuilder logging() {
     return new LoggingConfigurationBuilderImpl();
@@ -332,15 +328,20 @@ public static LoggingConfigurationBuilder logging() {
    * 

* This is a shortcut for Components.logging().adapter(logAdapter). The * com.launchdarkly.logging - * API defines the {@link LDLogAdapter} interface to specify where log output should be sent. By default, - * it is set to {@link com.launchdarkly.logging.LDSLF4J#adapter()}, meaning that output will be sent to - * SLF4J and controlled by the SLF4J configuration. You may use - * the {@link com.launchdarkly.logging.Logs} factory methods, or a custom implementation, to handle log - * output differently. For instance, you may specify {@link com.launchdarkly.logging.Logs#basic()} for - * simple console output, or {@link com.launchdarkly.logging.Logs#toJavaUtilLogging()} to use the - * java.util.logging framework. + * API defines the {@link LDLogAdapter} interface to specify where log output should be sent. + *

+ * The default logging destination, if no adapter is specified, depends on whether + * SLF4J is present in the classpath. If it is, then the SDK uses + * {@link com.launchdarkly.logging.LDSLF4J#adapter()}, causing output to go to SLF4J; what happens to + * the output then is determined by the SLF4J configuration. If SLF4J is not present in the classpath, + * the SDK uses {@link Logs#toConsole()} instead, causing output to go to the {@code System.err} stream. + *

+ * You may use the {@link com.launchdarkly.logging.Logs} factory methods, or a custom implementation, + * to handle log output differently. For instance, you may specify + * {@link com.launchdarkly.logging.Logs#toJavaUtilLogging()} to use the java.util.logging + * framework. *

- * Passing this to {@link LDConfig.Builder#logging(com.launchdarkly.sdk.server.interfaces.LoggingConfigurationFactory)}, + * Passing this to {@link LDConfig.Builder#logging(ComponentConfigurer)}, * after setting any desired properties on the builder, applies this configuration to the SDK. *


    *     LDConfig config = new LDConfig.Builder()
@@ -353,7 +354,7 @@ public static LoggingConfigurationBuilder logging() {
    * @param logAdapter the log adapter
    * @return a configuration builder
    * @since 5.10.0
-   * @see LDConfig.Builder#logging(com.launchdarkly.sdk.server.interfaces.LoggingConfigurationFactory)
+   * @see LDConfig.Builder#logging(ComponentConfigurer)
    * @see LoggingConfigurationBuilder#adapter(LDLogAdapter)
    */
   public static LoggingConfigurationBuilder logging(LDLogAdapter logAdapter) {
@@ -363,7 +364,7 @@ public static LoggingConfigurationBuilder logging(LDLogAdapter logAdapter) {
   /**
    * Returns a configuration builder that turns off SDK logging.
    * 

- * Passing this to {@link LDConfig.Builder#logging(com.launchdarkly.sdk.server.interfaces.LoggingConfigurationFactory)} + * Passing this to {@link LDConfig.Builder#logging(ComponentConfigurer)} * applies this configuration to the SDK. *

* It is equivalent to Components.logging(com.launchdarkly.logging.Logs.none()). diff --git a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java index 3e87d7103..e4a82499b 100644 --- a/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/ComponentsImpl.java @@ -6,8 +6,14 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LDSLF4J; import com.launchdarkly.logging.Logs; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.DiagnosticEvent.ConfigProperty; +import com.launchdarkly.sdk.internal.events.DefaultEventSender; +import com.launchdarkly.sdk.internal.events.DiagnosticConfigProperty; +import com.launchdarkly.sdk.internal.events.EventSender; +import com.launchdarkly.sdk.internal.events.EventsConfiguration; +import com.launchdarkly.sdk.internal.http.HttpProperties; import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder; @@ -15,27 +21,18 @@ import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder; import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder; import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataSource; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; -import com.launchdarkly.sdk.server.interfaces.DataStoreUpdates; -import com.launchdarkly.sdk.server.interfaces.DiagnosticDescription; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventProcessor; -import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory; -import com.launchdarkly.sdk.server.interfaces.EventSender; -import com.launchdarkly.sdk.server.interfaces.EventSenderFactory; import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.sdk.server.interfaces.LoggingConfiguration; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStore; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DiagnosticDescription; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import java.io.IOException; import java.net.InetSocketAddress; @@ -54,20 +51,20 @@ abstract class ComponentsImpl { private ComponentsImpl() {} - static final class InMemoryDataStoreFactory implements DataStoreFactory, DiagnosticDescription { - static final DataStoreFactory INSTANCE = new InMemoryDataStoreFactory(); + static final class InMemoryDataStoreFactory implements ComponentConfigurer, DiagnosticDescription { + static final InMemoryDataStoreFactory INSTANCE = new InMemoryDataStoreFactory(); @Override - public DataStore createDataStore(ClientContext context, DataStoreUpdates dataStoreUpdates) { + public DataStore build(ClientContext context) { return new InMemoryDataStore(); } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfiguration) { + public LDValue describeConfiguration(ClientContext clientContext) { return LDValue.of("memory"); } } - static final EventProcessorFactory NULL_EVENT_PROCESSOR_FACTORY = context -> NullEventProcessor.INSTANCE; + static final ComponentConfigurer NULL_EVENT_PROCESSOR_FACTORY = context -> NullEventProcessor.INSTANCE; /** * Stub implementation of {@link EventProcessor} for when we don't want to send any events. @@ -78,47 +75,52 @@ static final class NullEventProcessor implements EventProcessor { private NullEventProcessor() {} @Override - public void sendEvent(Event e) { - } + public void flush() {} @Override - public void flush() { - } - + public void close() {} + @Override - public void close() { - } + public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation, LDValue value, + EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey, boolean requireFullEvent, + Long debugEventsUntilDate) {} + + @Override + public void recordIdentifyEvent(LDContext context) {} + + @Override + public void recordCustomEvent(LDContext context, String eventKey, LDValue data, Double metricValue) {} } - static final class NullDataSourceFactory implements DataSourceFactory, DiagnosticDescription { + static final class NullDataSourceFactory implements ComponentConfigurer, DiagnosticDescription { static final NullDataSourceFactory INSTANCE = new NullDataSourceFactory(); @Override - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) { - LDLogger logger = context.getBasic().getBaseLogger(); - if (context.getBasic().isOffline()) { + public DataSource build(ClientContext context) { + LDLogger logger = context.getBaseLogger(); + if (context.isOffline()) { // If they have explicitly called offline(true) to disable everything, we'll log this slightly // more specific message. logger.info("Starting LaunchDarkly client in offline mode"); } else { logger.info("LaunchDarkly client will not connect to Launchdarkly for feature flag data"); } - dataSourceUpdates.updateStatus(DataSourceStatusProvider.State.VALID, null); + context.getDataSourceUpdateSink().updateStatus(DataSourceStatusProvider.State.VALID, null); return NullDataSource.INSTANCE; } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfiguration) { + public LDValue describeConfiguration(ClientContext clientContext) { // The difference between "offline" and "using the Relay daemon" is irrelevant from the data source's // point of view, but we describe them differently in diagnostic events. This is easy because if we were // configured to be completely offline... we wouldn't be sending any diagnostic events. Therefore, if // Components.externalUpdatesOnly() was specified as the data source and we are sending a diagnostic // event, we can assume usingRelayDaemon should be true. return LDValue.buildObject() - .put(ConfigProperty.CUSTOM_BASE_URI.name, false) - .put(ConfigProperty.CUSTOM_STREAM_URI.name, false) - .put(ConfigProperty.STREAMING_DISABLED.name, false) - .put(ConfigProperty.USING_RELAY_DAEMON.name, true) + .put(DiagnosticConfigProperty.CUSTOM_BASE_URI.name, false) + .put(DiagnosticConfigProperty.CUSTOM_STREAM_URI.name, false) + .put(DiagnosticConfigProperty.STREAMING_DISABLED.name, false) + .put(DiagnosticConfigProperty.USING_RELAY_DAEMON.name, true) .build(); } } @@ -143,24 +145,23 @@ public void close() throws IOException {} static final class StreamingDataSourceBuilderImpl extends StreamingDataSourceBuilder implements DiagnosticDescription { @Override - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) { - LDLogger baseLogger = context.getBasic().getBaseLogger(); + public DataSource build(ClientContext context) { + LDLogger baseLogger = context.getBaseLogger(); LDLogger logger = baseLogger.subLogger(Loggers.DATA_SOURCE_LOGGER_NAME); logger.info("Enabling streaming API"); URI streamUri = StandardEndpoints.selectBaseUri( - context.getBasic().getServiceEndpoints().getStreamingBaseUri(), - baseURI, + context.getServiceEndpoints().getStreamingBaseUri(), StandardEndpoints.DEFAULT_STREAMING_BASE_URI, "streaming", baseLogger ); return new StreamProcessor( - context.getHttp(), - dataSourceUpdates, - context.getBasic().getThreadPriority(), - ClientContextImpl.get(context).diagnosticAccumulator, + toHttpProperties(context.getHttp()), + context.getDataSourceUpdateSink(), + context.getThreadPriority(), + ClientContextImpl.get(context).diagnosticStore, streamUri, initialReconnectDelay, logger @@ -168,17 +169,16 @@ public DataSource createDataSource(ClientContext context, DataSourceUpdates data } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfiguration) { + public LDValue describeConfiguration(ClientContext clientContext) { return LDValue.buildObject() - .put(ConfigProperty.STREAMING_DISABLED.name, false) - .put(ConfigProperty.CUSTOM_BASE_URI.name, false) - .put(ConfigProperty.CUSTOM_STREAM_URI.name, + .put(DiagnosticConfigProperty.STREAMING_DISABLED.name, false) + .put(DiagnosticConfigProperty.CUSTOM_BASE_URI.name, false) + .put(DiagnosticConfigProperty.CUSTOM_STREAM_URI.name, StandardEndpoints.isCustomBaseUri( - basicConfiguration.getServiceEndpoints().getStreamingBaseUri(), - baseURI, + clientContext.getServiceEndpoints().getStreamingBaseUri(), StandardEndpoints.DEFAULT_STREAMING_BASE_URI)) - .put(ConfigProperty.RECONNECT_TIME_MILLIS.name, initialReconnectDelay.toMillis()) - .put(ConfigProperty.USING_RELAY_DAEMON.name, false) + .put(DiagnosticConfigProperty.RECONNECT_TIME_MILLIS.name, initialReconnectDelay.toMillis()) + .put(DiagnosticConfigProperty.USING_RELAY_DAEMON.name, false) .build(); } } @@ -191,25 +191,24 @@ PollingDataSourceBuilderImpl pollIntervalWithNoMinimum(Duration pollInterval) { } @Override - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) { - LDLogger baseLogger = context.getBasic().getBaseLogger(); + public DataSource build(ClientContext context) { + LDLogger baseLogger = context.getBaseLogger(); LDLogger logger = baseLogger.subLogger(Loggers.DATA_SOURCE_LOGGER_NAME); logger.info("Disabling streaming API"); logger.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support"); URI pollUri = StandardEndpoints.selectBaseUri( - context.getBasic().getServiceEndpoints().getPollingBaseUri(), - baseURI, + context.getServiceEndpoints().getPollingBaseUri(), StandardEndpoints.DEFAULT_POLLING_BASE_URI, "polling", baseLogger ); - DefaultFeatureRequestor requestor = new DefaultFeatureRequestor(context.getHttp(), pollUri, logger); + DefaultFeatureRequestor requestor = new DefaultFeatureRequestor(toHttpProperties(context.getHttp()), pollUri, logger); return new PollingProcessor( requestor, - dataSourceUpdates, + context.getDataSourceUpdateSink(), ClientContextImpl.get(context).sharedExecutor, pollInterval, logger @@ -217,17 +216,16 @@ public DataSource createDataSource(ClientContext context, DataSourceUpdates data } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfiguration) { + public LDValue describeConfiguration(ClientContext clientContext) { return LDValue.buildObject() - .put(ConfigProperty.STREAMING_DISABLED.name, true) - .put(ConfigProperty.CUSTOM_BASE_URI.name, + .put(DiagnosticConfigProperty.STREAMING_DISABLED.name, true) + .put(DiagnosticConfigProperty.CUSTOM_BASE_URI.name, StandardEndpoints.isCustomBaseUri( - basicConfiguration.getServiceEndpoints().getPollingBaseUri(), - baseURI, + clientContext.getServiceEndpoints().getPollingBaseUri(), StandardEndpoints.DEFAULT_POLLING_BASE_URI)) - .put(ConfigProperty.CUSTOM_STREAM_URI.name, false) - .put(ConfigProperty.POLLING_INTERVAL_MILLIS.name, pollInterval.toMillis()) - .put(ConfigProperty.USING_RELAY_DAEMON.name, false) + .put(DiagnosticConfigProperty.CUSTOM_STREAM_URI.name, false) + .put(DiagnosticConfigProperty.POLLING_INTERVAL_MILLIS.name, pollInterval.toMillis()) + .put(DiagnosticConfigProperty.USING_RELAY_DAEMON.name, false) .build(); } } @@ -235,74 +233,104 @@ public LDValue describeConfiguration(BasicConfiguration basicConfiguration) { static final class EventProcessorBuilderImpl extends EventProcessorBuilder implements DiagnosticDescription { @Override - public EventProcessor createEventProcessor(ClientContext context) { - LDLogger baseLogger = context.getBasic().getBaseLogger(); - LDLogger logger = baseLogger.subLogger(Loggers.EVENTS_LOGGER_NAME); - EventSenderFactory senderFactory = - eventSenderFactory == null ? new DefaultEventSender.Factory() : eventSenderFactory; - EventSender eventSender = senderFactory.createEventSender( - context.getBasic(), - context.getHttp(), - logger - ); + public EventProcessor build(ClientContext context) { + EventSender eventSender; + if (eventSenderConfigurer == null) { + eventSender = new DefaultEventSender( + toHttpProperties(context.getHttp()), + null, // use default request path for server-side events + null, // use default request path for client-side events + 0, // 0 means default retry delay + context.getBaseLogger().subLogger(Loggers.EVENTS_LOGGER_NAME) + ); + } else { + eventSender = new EventSenderWrapper(eventSenderConfigurer.build(context)); + } URI eventsUri = StandardEndpoints.selectBaseUri( - context.getBasic().getServiceEndpoints().getEventsBaseUri(), - baseURI, + context.getServiceEndpoints().getEventsBaseUri(), StandardEndpoints.DEFAULT_EVENTS_BASE_URI, "events", - baseLogger + context.getBaseLogger() ); - return new DefaultEventProcessor( - new EventsConfiguration( - allAttributesPrivate, - capacity, - eventSender, - eventsUri, - flushInterval, - inlineUsersInEvents, - privateAttributes, - userKeysCapacity, - userKeysFlushInterval, - diagnosticRecordingInterval - ), - ClientContextImpl.get(context).sharedExecutor, - context.getBasic().getThreadPriority(), - ClientContextImpl.get(context).diagnosticAccumulator, - ClientContextImpl.get(context).diagnosticInitEvent, - logger + EventsConfiguration eventsConfig = new EventsConfiguration( + allAttributesPrivate, + capacity, + new ServerSideEventContextDeduplicator(userKeysCapacity, userKeysFlushInterval), + diagnosticRecordingInterval.toMillis(), + ClientContextImpl.get(context).diagnosticStore, + eventSender, + EventsConfiguration.DEFAULT_EVENT_SENDING_THREAD_POOL_SIZE, + eventsUri, + flushInterval.toMillis(), + false, + false, + privateAttributes ); + return new DefaultEventProcessorWrapper(context, eventsConfig); } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfiguration) { + public LDValue describeConfiguration(ClientContext clientContext) { return LDValue.buildObject() - .put(ConfigProperty.ALL_ATTRIBUTES_PRIVATE.name, allAttributesPrivate) - .put(ConfigProperty.CUSTOM_EVENTS_URI.name, + .put(DiagnosticConfigProperty.ALL_ATTRIBUTES_PRIVATE.name, allAttributesPrivate) + .put(DiagnosticConfigProperty.CUSTOM_EVENTS_URI.name, StandardEndpoints.isCustomBaseUri( - basicConfiguration.getServiceEndpoints().getEventsBaseUri(), - baseURI, + clientContext.getServiceEndpoints().getEventsBaseUri(), StandardEndpoints.DEFAULT_EVENTS_BASE_URI)) - .put(ConfigProperty.DIAGNOSTIC_RECORDING_INTERVAL_MILLIS.name, diagnosticRecordingInterval.toMillis()) - .put(ConfigProperty.EVENTS_CAPACITY.name, capacity) - .put(ConfigProperty.EVENTS_FLUSH_INTERVAL_MILLIS.name, flushInterval.toMillis()) - .put(ConfigProperty.INLINE_USERS_IN_EVENTS.name, inlineUsersInEvents) - .put(ConfigProperty.SAMPLING_INTERVAL.name, 0) - .put(ConfigProperty.USER_KEYS_CAPACITY.name, userKeysCapacity) - .put(ConfigProperty.USER_KEYS_FLUSH_INTERVAL_MILLIS.name, userKeysFlushInterval.toMillis()) + .put(DiagnosticConfigProperty.DIAGNOSTIC_RECORDING_INTERVAL_MILLIS.name, diagnosticRecordingInterval.toMillis()) + .put(DiagnosticConfigProperty.EVENTS_CAPACITY.name, capacity) + .put(DiagnosticConfigProperty.EVENTS_FLUSH_INTERVAL_MILLIS.name, flushInterval.toMillis()) + .put(DiagnosticConfigProperty.SAMPLING_INTERVAL.name, 0) + .put(DiagnosticConfigProperty.USER_KEYS_CAPACITY.name, userKeysCapacity) + .put(DiagnosticConfigProperty.USER_KEYS_FLUSH_INTERVAL_MILLIS.name, userKeysFlushInterval.toMillis()) .build(); } + + static final class EventSenderWrapper implements EventSender { + private final com.launchdarkly.sdk.server.subsystems.EventSender wrappedSender; + + EventSenderWrapper(com.launchdarkly.sdk.server.subsystems.EventSender wrappedSender) { + this.wrappedSender = wrappedSender; + } + + @Override + public void close() throws IOException { + wrappedSender.close(); + } + + @Override + public Result sendAnalyticsEvents(byte[] data, int eventCount, URI eventsBaseUri) { + return transformResult(wrappedSender.sendAnalyticsEvents(data, eventCount, eventsBaseUri)); + } + + @Override + public Result sendDiagnosticEvent(byte[] data, URI eventsBaseUri) { + return transformResult(wrappedSender.sendDiagnosticEvent(data, eventsBaseUri)); + } + + private Result transformResult(com.launchdarkly.sdk.server.subsystems.EventSender.Result result) { + switch (result) { + case FAILURE: + return new Result(false, false, null); + case STOP: + return new Result(false, true, null); + default: + return new Result(true, false, null); + } + } + } } static final class HttpConfigurationBuilderImpl extends HttpConfigurationBuilder { @Override - public HttpConfiguration createHttpConfiguration(BasicConfiguration basicConfiguration) { - LDLogger logger = basicConfiguration.getBaseLogger(); + public HttpConfiguration build(ClientContext clientContext) { + LDLogger logger = clientContext.getBaseLogger(); // Build the default headers ImmutableMap.Builder headers = ImmutableMap.builder(); - headers.put("Authorization", basicConfiguration.getSdkKey()); + headers.put("Authorization", clientContext.getSdkKey()); headers.put("User-Agent", "JavaClient/" + Version.SDK_VERSION); - if (basicConfiguration.getApplicationInfo() != null) { - String tagHeader = Util.applicationTagHeader(basicConfiguration.getApplicationInfo(), logger); + if (clientContext.getApplicationInfo() != null) { + String tagHeader = Util.applicationTagHeader(clientContext.getApplicationInfo(), logger); if (!tagHeader.isEmpty()) { headers.put("X-LaunchDarkly-Tags", tagHeader); } @@ -314,15 +342,15 @@ public HttpConfiguration createHttpConfiguration(BasicConfiguration basicConfigu Proxy proxy = proxyHost == null ? null : new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); - return new HttpConfigurationImpl( + return new HttpConfiguration( connectTimeout, + headers.build(), proxy, proxyAuth, - socketTimeout, socketFactory, + socketTimeout, sslSocketFactory, - trustManager, - headers.build() + trustManager ); } } @@ -343,47 +371,54 @@ public String provideAuthorization(Iterable challenges) { } static final class PersistentDataStoreBuilderImpl extends PersistentDataStoreBuilder implements DiagnosticDescription { - public PersistentDataStoreBuilderImpl(PersistentDataStoreFactory persistentDataStoreFactory) { - super(persistentDataStoreFactory); + public PersistentDataStoreBuilderImpl(ComponentConfigurer storeConfigurer) { + super(storeConfigurer); } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfiguration) { - if (persistentDataStoreFactory instanceof DiagnosticDescription) { - return ((DiagnosticDescription)persistentDataStoreFactory).describeConfiguration(basicConfiguration); + public LDValue describeConfiguration(ClientContext clientContext) { + if (persistentDataStoreConfigurer instanceof DiagnosticDescription) { + return ((DiagnosticDescription)persistentDataStoreConfigurer).describeConfiguration(clientContext); } return LDValue.of("custom"); } - /** - * Called by the SDK to create the data store instance. - */ @Override - public DataStore createDataStore(ClientContext context, DataStoreUpdates dataStoreUpdates) { - PersistentDataStore core = persistentDataStoreFactory.createPersistentDataStore(context); + public DataStore build(ClientContext context) { + PersistentDataStore core = persistentDataStoreConfigurer.build(context); return new PersistentDataStoreWrapper( core, cacheTime, staleValuesPolicy, recordCacheStats, - dataStoreUpdates, + context.getDataStoreUpdateSink(), ClientContextImpl.get(context).sharedExecutor, - context.getBasic().getBaseLogger().subLogger(Loggers.DATA_STORE_LOGGER_NAME) + context.getBaseLogger().subLogger(Loggers.DATA_STORE_LOGGER_NAME) ); } } static final class LoggingConfigurationBuilderImpl extends LoggingConfigurationBuilder { @Override - public LoggingConfiguration createLoggingConfiguration(BasicConfiguration basicConfiguration) { - LDLogAdapter adapter = logAdapter == null ? LDSLF4J.adapter() : logAdapter; + public LoggingConfiguration build(ClientContext clientContext) { + LDLogAdapter adapter = logAdapter == null ? getDefaultLogAdapter() : logAdapter; LDLogAdapter filteredAdapter = Logs.level(adapter, minimumLevel == null ? LDLogLevel.INFO : minimumLevel); // If the adapter is for a framework like SLF4J or java.util.logging that has its own external // configuration system, then calling Logs.level here has no effect and filteredAdapter will be // just the same as adapter. String name = baseName == null ? Loggers.BASE_LOGGER_NAME : baseName; - return new LoggingConfigurationImpl(name, filteredAdapter, logDataSourceOutageAsErrorAfter); + return new LoggingConfiguration(name, filteredAdapter, logDataSourceOutageAsErrorAfter); + } + + private static LDLogAdapter getDefaultLogAdapter() { + // If SLF4J is present in the classpath, use that by default; otherwise use the console. + try { + Class.forName("org.slf4j.LoggerFactory"); + return LDSLF4J.adapter(); + } catch (ClassNotFoundException e) { + return Logs.toConsole(); + } } } @@ -405,4 +440,22 @@ public ServiceEndpoints createServiceEndpoints() { return new ServiceEndpoints(streamingBaseUri, pollingBaseUri, eventsBaseUri); } } + + static HttpProperties toHttpProperties(HttpConfiguration httpConfig) { + okhttp3.Authenticator proxyAuth = null; + if (httpConfig.getProxyAuthentication() != null) { + proxyAuth = Util.okhttpAuthenticatorFromHttpAuthStrategy(httpConfig.getProxyAuthentication()); + } + return new HttpProperties( + httpConfig.getConnectTimeout().toMillis(), + ImmutableMap.copyOf(httpConfig.getDefaultHeaders()), + null, + httpConfig.getProxy(), + proxyAuth, + httpConfig.getSocketFactory(), + httpConfig.getSocketTimeout().toMillis(), + httpConfig.getSslSocketFactory(), + httpConfig.getTrustManager() + ); + } } diff --git a/src/main/java/com/launchdarkly/sdk/server/DataModel.java b/src/main/java/com/launchdarkly/sdk/server/DataModel.java index fb32db264..42cb321e3 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataModel.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataModel.java @@ -2,18 +2,21 @@ import com.google.common.collect.ImmutableList; import com.google.gson.annotations.JsonAdapter; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; import com.launchdarkly.sdk.server.DataModelPreprocessing.FlagPreprocessed; import com.launchdarkly.sdk.server.DataModelPreprocessing.FlagRulePreprocessed; import com.launchdarkly.sdk.server.DataModelPreprocessing.PrerequisitePreprocessed; import com.launchdarkly.sdk.server.DataModelPreprocessing.TargetPreprocessed; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import static java.util.Collections.emptyList; @@ -29,19 +32,20 @@ // implementing a custom component such as a data store. But beyond the mere fact of there being these kinds of // data, applications should not be considered with their structure. // -// - For all classes that can be deserialized from JSON, there must be an empty constructor, and the fields -// cannot be final. This is because of how Gson works: it creates an instance first, then sets the fields. If -// we are able to move away from using Gson reflective deserialization in the future, we can make them final. +// - For classes that can be deserialized from JSON, if we are relying on Gson's reflective behavior (i.e. if +// the class does not have a custom TypeAdapter), there must be an empty constructor, and the fields cannot +// be final. This is because of how Gson works: it creates an instance first, then sets the fields; that also +// means we cannot do any transformation/validation of the fields in the constructor. But if we have a custom +// deserializer, then we should use final fields. // -// - There should also be a constructor that takes all the fields; we should use that whenever we need to -// create these objects programmatically (so that if we are able at some point to make the fields final, that +// - In any case, there should be a constructor that takes all the fields; we should use that whenever we need +// to create these objects programmatically (so that if we are able at some point to make the fields final, that // won't break anything). // -// - For properties that have a collection type such as List, the getter method should always include a null -// guard and return an empty collection if the field is null (so that we don't have to worry about null guards -// every time we might want to iterate over these collections). Semantically there is no difference in the data -// model between an empty list and a null list, and in some languages (particularly Go) it is easy for an -// uninitialized list to be serialized to JSON as null. +// - For properties that have a collection type such as List, we should ensure that a null is always changed to +// an empty list (in the constructor, if the field can be made final; otherwise in the getter). Semantically +// there is no difference in the data model between an empty list and a null list, and in some languages +// (particularly Go) it is easy for an uninitialized list to be serialized to JSON as null. // // - Some classes have a "preprocessed" field containing types defined in DataModelPreprocessing. These fields // must always be marked transient, so Gson will not serialize them. They are populated when we deserialize a @@ -127,6 +131,7 @@ static final class FeatureFlag implements VersionedData, JsonHelpers.PostProcess private List prerequisites; private String salt; private List targets; + private List contextTargets; private List rules; private VariationOrRollout fallthrough; private Integer offVariation; //optional @@ -143,8 +148,8 @@ static final class FeatureFlag implements VersionedData, JsonHelpers.PostProcess FeatureFlag() {} FeatureFlag(String key, int version, boolean on, List prerequisites, String salt, List targets, - List rules, VariationOrRollout fallthrough, Integer offVariation, List variations, - boolean clientSide, boolean trackEvents, boolean trackEventsFallthrough, + List contextTargets, List rules, VariationOrRollout fallthrough, Integer offVariation, + List variations, boolean clientSide, boolean trackEvents, boolean trackEventsFallthrough, Long debugEventsUntilDate, boolean deleted) { this.key = key; this.version = version; @@ -152,6 +157,7 @@ static final class FeatureFlag implements VersionedData, JsonHelpers.PostProcess this.prerequisites = prerequisites; this.salt = salt; this.targets = targets; + this.contextTargets = contextTargets; this.rules = rules; this.fallthrough = fallthrough; this.offVariation = offVariation; @@ -204,6 +210,11 @@ List getTargets() { return targets == null ? emptyList() : targets; } + // Guaranteed non-null + List getContextTargets() { + return contextTargets == null ? emptyList() : contextTargets; + } + // Guaranteed non-null List getRules() { return rules == null ? emptyList() : rules; @@ -254,6 +265,7 @@ int getVariation() { } static final class Target { + private ContextKind contextKind; private Set values; private int variation; @@ -261,11 +273,16 @@ static final class Target { Target() {} - Target(Set values, int variation) { + Target(ContextKind contextKind, Set values, int variation) { + this.contextKind = contextKind; this.values = values; this.variation = variation; } + ContextKind getContextKind() { + return contextKind; + } + // Guaranteed non-null Collection getValues() { return values == null ? emptySet() : values; @@ -313,25 +330,29 @@ boolean isTrackEvents() { } } + @JsonAdapter(DataModelSerialization.ClauseTypeAdapter.class) static final class Clause { - private UserAttribute attribute; - private Operator op; - private List values; //interpreted as an OR of values - private boolean negate; + private final ContextKind contextKind; + private final AttributeRef attribute; + private final Operator op; + private final List values; //interpreted as an OR of values + private final boolean negate; transient ClausePreprocessed preprocessed; - Clause() { - } - - Clause(UserAttribute attribute, Operator op, List values, boolean negate) { + Clause(ContextKind contextKind, AttributeRef attribute, Operator op, List values, boolean negate) { + this.contextKind = contextKind; this.attribute = attribute; this.op = op; - this.values = values; + this.values = values == null ? emptyList() : values;; this.negate = negate; } - UserAttribute getAttribute() { + ContextKind getContextKind() { + return contextKind; + } + + AttributeRef getAttribute() { return attribute; } @@ -341,7 +362,7 @@ Operator getOp() { // Guaranteed non-null List getValues() { - return values == null ? emptyList() : values; + return values; } boolean isNegate() { @@ -349,34 +370,32 @@ boolean isNegate() { } } + @JsonAdapter(DataModelSerialization.RolloutTypeAdapter.class) static final class Rollout { - private List variations; - private UserAttribute bucketBy; - private RolloutKind kind; - private Integer seed; + private final ContextKind contextKind; + private final List variations; + private final AttributeRef bucketBy; + private final RolloutKind kind; + private final Integer seed; - Rollout() {} - - Rollout(List variations, UserAttribute bucketBy, RolloutKind kind) { - this.variations = variations; + Rollout(ContextKind contextKind, List variations, AttributeRef bucketBy, RolloutKind kind, Integer seed) { + this.contextKind = contextKind; + this.variations = variations == null ? emptyList() : variations; this.bucketBy = bucketBy; this.kind = kind; - this.seed = null; + this.seed = seed; } - Rollout(List variations, UserAttribute bucketBy, RolloutKind kind, Integer seed) { - this.variations = variations; - this.bucketBy = bucketBy; - this.kind = kind; - this.seed = seed; + ContextKind getContextKind() { + return contextKind; } // Guaranteed non-null List getVariations() { - return variations == null ? emptyList() : variations; + return variations; } - UserAttribute getBucketBy() { + AttributeRef getBucketBy() { return bucketBy; } @@ -448,11 +467,14 @@ static final class Segment implements VersionedData, JsonHelpers.PostProcessingD private String key; private Set included; private Set excluded; + private List includedContexts; + private List excludedContexts; private String salt; private List rules; private int version; private boolean deleted; private boolean unbounded; + private ContextKind unboundedContextKind; private Integer generation; Segment() {} @@ -460,20 +482,26 @@ static final class Segment implements VersionedData, JsonHelpers.PostProcessingD Segment(String key, Set included, Set excluded, + List includedContexts, + List excludedContexts, String salt, List rules, int version, boolean deleted, boolean unbounded, + ContextKind unboundedContextKind, Integer generation) { this.key = key; this.included = included; this.excluded = excluded; + this.includedContexts = includedContexts; + this.excludedContexts = excludedContexts; this.salt = salt; this.rules = rules; this.version = version; this.deleted = deleted; this.unbounded = unbounded; + this.unboundedContextKind = unboundedContextKind; this.generation = generation; } @@ -491,6 +519,16 @@ Collection getExcluded() { return excluded == null ? emptySet() : excluded; } + // Guaranteed non-null + List getIncludedContexts() { + return includedContexts == null ? emptyList() : includedContexts; + } + + // Guaranteed non-null + List getExcludedContexts() { + return excludedContexts == null ? emptyList() : excludedContexts; + } + String getSalt() { return salt; } @@ -512,6 +550,10 @@ public boolean isUnbounded() { return unbounded; } + public ContextKind getUnboundedContextKind() { + return unboundedContextKind; + } + public Integer getGeneration() { return generation; } @@ -521,53 +563,135 @@ public void afterDeserialized() { } } + @JsonAdapter(DataModelSerialization.SegmentRuleTypeAdapter.class) static final class SegmentRule { private final List clauses; private final Integer weight; - private final UserAttribute bucketBy; + private final ContextKind rolloutContextKind; + private final AttributeRef bucketBy; - SegmentRule(List clauses, Integer weight, UserAttribute bucketBy) { - this.clauses = clauses; + SegmentRule(List clauses, Integer weight, ContextKind rolloutContextKind, AttributeRef bucketBy) { + this.clauses = clauses == null ? emptyList() : clauses; this.weight = weight; + this.rolloutContextKind = rolloutContextKind; this.bucketBy = bucketBy; } // Guaranteed non-null List getClauses() { - return clauses == null ? emptyList() : clauses; + return clauses; } Integer getWeight() { return weight; } - UserAttribute getBucketBy() { + ContextKind getRolloutContextKind() { + return rolloutContextKind; + } + + AttributeRef getBucketBy() { return bucketBy; } } + static class SegmentTarget { + private ContextKind contextKind; + private Set values; + + SegmentTarget(ContextKind contextKind, Set values) { + this.contextKind = contextKind; + this.values = values; + } + + ContextKind getContextKind() { + return contextKind; + } + + Set getValues() { // guaranteed non-null + return values == null ? emptySet() : values; + } + } + /** - * This enum can be directly deserialized from JSON, avoiding the need for a mapping of strings to - * operators. The implementation of each operator is in EvaluatorOperators. + * This is an enum-like type rather than an enum because we don't want unrecognized operators to + * cause parsing of the whole JSON environment to fail. The implementation of each operator is in + * EvaluatorOperators. */ - static enum Operator { - in, - endsWith, - startsWith, - matches, - contains, - lessThan, - lessThanOrEqual, - greaterThan, - greaterThanOrEqual, - before, - after, - semVerEqual, - semVerLessThan, - semVerGreaterThan, - segmentMatch - } + static class Operator { + private final String name; + private final boolean builtin; + private final int hashCode; + + private static final Map builtins = new HashMap<>(); + + private Operator(String name, boolean builtin) { + this.name = name; + this.builtin = builtin; + + // Precompute the hash code for fast map lookups - String.hashCode() does memoize this value, + // sort of, but we shouldn't have to rely on that + this.hashCode = name.hashCode(); + } + + private static Operator builtin(String name) { + Operator op = new Operator(name, true); + builtins.put(name, op); + return op; + } + + static final Operator in = builtin("in"); + static final Operator startsWith = builtin("startsWith"); + static final Operator endsWith = builtin("endsWith"); + static final Operator matches = builtin("matches"); + static final Operator contains = builtin("contains"); + static final Operator lessThan = builtin("lessThan"); + static final Operator lessThanOrEqual = builtin("lessThanOrEqual"); + static final Operator greaterThan = builtin("greaterThan"); + static final Operator greaterThanOrEqual = builtin("greaterThanOrEqual"); + static final Operator before = builtin("before"); + static final Operator after = builtin("after"); + static final Operator semVerEqual = builtin("semVerEqual"); + static final Operator semVerLessThan = builtin("semVerLessThan"); + static final Operator semVerGreaterThan = builtin("semVerGreaterThan"); + static final Operator segmentMatch = builtin("segmentMatch"); + + static Operator forName(String name) { + // Normally we will only see names that are in the builtins map. Anything else is something + // the SDK doesn't recognize, but we still need to allow it to exist rather than throwing + // an error. + Operator op = builtins.get(name); + return op == null ? new Operator(name, false) : op; + } + + static Iterable getBuiltins() { + return builtins.values(); + } + String name() { + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object other) { + if (this.builtin) { + // reference equality is OK for the builtin ones, because we intern them + return this == other; + } + return other instanceof Operator && ((Operator)other).name.equals(this.name); + } + + @Override + public int hashCode() { + return hashCode; + } + } + /** * This enum is all lowercase so that when it is automatically deserialized from JSON, * the lowercase properties properly map to these enumerations. diff --git a/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java b/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java index a69a50bc4..46cb04e6e 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataModelDependencies.java @@ -6,10 +6,10 @@ import com.google.common.collect.Iterables; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.DataModel.Operator; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import java.util.Comparator; import java.util.HashMap; @@ -28,7 +28,7 @@ /** * Implements a dependency graph ordering for data to be stored in a data store. *

- * We use this to order the data that we pass to {@link com.launchdarkly.sdk.server.interfaces.DataStore#init(FullDataSet)}, + * We use this to order the data that we pass to {@link com.launchdarkly.sdk.server.subsystems.DataStore#init(FullDataSet)}, * and also to determine which flags are affected by a change if the application is listening for flag change events. *

* Dependencies are defined as follows: there is a dependency from flag F to flag G if F is a prerequisite flag for @@ -84,27 +84,37 @@ public static Set computeDependenciesFrom(DataKind fromKind, ItemDes Iterable segmentKeys = concat( transform( flag.getRules(), - rule -> concat( - Iterables.>transform( - rule.getClauses(), - clause -> clause.getOp() == Operator.segmentMatch ? - transform(clause.getValues(), LDValue::stringValue) : - emptyList() - ) - ) - ) + rule -> segmentKeysFromClauses(rule.getClauses())) ); return ImmutableSet.copyOf( - concat( - transform(prereqFlagKeys, key -> new KindAndKey(FEATURES, key)), - transform(segmentKeys, key -> new KindAndKey(SEGMENTS, key)) - ) + concat(kindAndKeys(FEATURES, prereqFlagKeys), kindAndKeys(SEGMENTS, segmentKeys)) ); + } else if (fromKind == SEGMENTS) { + DataModel.Segment segment = (DataModel.Segment)fromItem.getItem(); + + Iterable nestedSegmentKeys = concat( + transform( + segment.getRules(), + rule -> segmentKeysFromClauses(rule.getClauses()))); + return ImmutableSet.copyOf(kindAndKeys(SEGMENTS, nestedSegmentKeys)); } return emptySet(); } + private static Iterable kindAndKeys(DataKind kind, Iterable keys) { + return transform(keys, key -> new KindAndKey(kind, key)); + } + + private static Iterable segmentKeysFromClauses(Iterable clauses) { + return concat(Iterables.>transform( + clauses, + clause -> clause.getOp() == Operator.segmentMatch ? + transform(clause.getValues(), LDValue::stringValue) : + emptyList() + )); + } + /** * Returns a copy of the input data set that guarantees that if you iterate through it the outer list and * the inner list in the order provided, any object that depends on another object will be updated after it. diff --git a/src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java b/src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java index af49227db..5a1c48e2b 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataModelPreprocessing.java @@ -21,6 +21,14 @@ import java.util.function.Function; import java.util.regex.Pattern; +import static com.launchdarkly.sdk.server.DataModel.Operator.after; +import static com.launchdarkly.sdk.server.DataModel.Operator.before; +import static com.launchdarkly.sdk.server.DataModel.Operator.in; +import static com.launchdarkly.sdk.server.DataModel.Operator.matches; +import static com.launchdarkly.sdk.server.DataModel.Operator.semVerEqual; +import static com.launchdarkly.sdk.server.DataModel.Operator.semVerGreaterThan; +import static com.launchdarkly.sdk.server.DataModel.Operator.semVerLessThan; + /** * Additional information that we attach to our data model to reduce the overhead of feature flag * evaluations. The methods that create these objects are called by the afterDeserialized() methods @@ -142,6 +150,9 @@ static void preprocessFlag(FeatureFlag f) { for (Target t: f.getTargets()) { preprocessTarget(t, f); } + for (Target t: f.getContextTargets()) { + preprocessTarget(t, f); + } List rules = f.getRules(); int n = rules.size(); for (int i = 0; i < n; i++) { @@ -198,8 +209,7 @@ static void preprocessClause(Clause c) { if (op == null) { return; } - switch (op) { - case in: + if (op == in) { // This is a special case where the clause is testing for an exact match against any of the // clause values. Converting the value list to a Set allows us to do a fast lookup instead of // a linear search. We do not do this for other operators (or if there are fewer than two @@ -207,27 +217,18 @@ static void preprocessClause(Clause c) { if (values.size() > 1) { c.preprocessed = new ClausePreprocessed(ImmutableSet.copyOf(values), null); } - break; - case matches: + } else if (op == matches) { c.preprocessed = preprocessClauseValues(c.getValues(), v -> new ClausePreprocessed.ValueData(null, EvaluatorTypeConversion.valueToRegex(v), null) ); - break; - case after: - case before: + } else if (op == after || op == before) { c.preprocessed = preprocessClauseValues(c.getValues(), v -> new ClausePreprocessed.ValueData(EvaluatorTypeConversion.valueToDateTime(v), null, null) ); - break; - case semVerEqual: - case semVerGreaterThan: - case semVerLessThan: + } else if (op == semVerEqual || op == semVerGreaterThan || op == semVerLessThan) { c.preprocessed = preprocessClauseValues(c.getValues(), v -> new ClausePreprocessed.ValueData(null, null, EvaluatorTypeConversion.valueToSemVer(v)) ); - break; - default: - break; } } diff --git a/src/main/java/com/launchdarkly/sdk/server/DataModelSerialization.java b/src/main/java/com/launchdarkly/sdk/server/DataModelSerialization.java index dd55cb879..387e0ee6f 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataModelSerialization.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataModelSerialization.java @@ -3,24 +3,38 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonElement; +import com.google.gson.TypeAdapter; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Operator; +import com.launchdarkly.sdk.server.DataModel.Rollout; +import com.launchdarkly.sdk.server.DataModel.RolloutKind; import com.launchdarkly.sdk.server.DataModel.Segment; +import com.launchdarkly.sdk.server.DataModel.SegmentRule; import com.launchdarkly.sdk.server.DataModel.VersionedData; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.server.DataModel.WeightedVariation; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import java.io.IOException; import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static com.launchdarkly.sdk.server.DataModel.FEATURES; import static com.launchdarkly.sdk.server.DataModel.SEGMENTS; import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstance; +import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstanceWithNullsAllowed; /** * JSON conversion logic specifically for our data model types. @@ -138,4 +152,216 @@ static FullDataSet parseFullDataSet(JsonReader jr) throws Serial throw new SerializationException(e); } } + + // Custom deserialization logic for Clause because the attribute field is treated differently + // depending on the contextKind field (if contextKind is null, we always parse attribute as a + // literal attribute name and not a reference). + static class ClauseTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, Clause c) throws IOException { + out.beginObject(); + if (c.getContextKind() != null) { + out.name("contextKind").value(c.getContextKind().toString()); + } + out.name("attribute").value(c.getAttribute() == null ? null : c.getAttribute().toString()); + out.name("op").value(c.getOp() == null ? null : c.getOp().name()); + out.name("values").beginArray(); + for (LDValue v: c.getValues()) { + gsonInstanceWithNullsAllowed().toJson(v, LDValue.class, out); + } + out.endArray(); + out.name("negate").value(c.isNegate()); + out.endObject(); + } + + @Override + public Clause read(JsonReader in) throws IOException { + ContextKind contextKind = null; + String attrString = null; + Operator op = null; + List values = new ArrayList<>(); + boolean negate = false; + in.beginObject(); + while (in.hasNext()) { + switch (in.nextName()) { + case "contextKind": + contextKind = ContextKind.of(in.nextString()); + break; + case "attribute": + attrString = in.nextString(); + break; + case "op": + op = Operator.forName(in.nextString()); + break; + case "values": + if (in.peek() == JsonToken.NULL) { + in.skipValue(); + } else { + in.beginArray(); + while (in.hasNext()) { + LDValue value = gsonInstanceWithNullsAllowed().fromJson(in, LDValue.class); + values.add(value); + } + in.endArray(); + } + break; + case "negate": + negate = in.nextBoolean(); + break; + default: + in.skipValue(); + } + } + in.endObject(); + AttributeRef attribute = attributeNameOrPath(attrString, contextKind); + return new Clause(contextKind, attribute, op, values, negate); + } + } + + // Custom deserialization logic for Rollout for a similar reason to Clause. + static class RolloutTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, Rollout r) throws IOException { + out.beginObject(); + if (r.getContextKind() != null) { + out.name("contextKind").value(r.getContextKind().toString()); + } + out.name("variations").beginArray(); + for (WeightedVariation wv: r.getVariations()) { + gsonInstanceWithNullsAllowed().toJson(wv, WeightedVariation.class, out); + } + out.endArray(); + if (r.getBucketBy() != null) { + out.name("bucketBy").value(r.getBucketBy().toString()); + } + if (r.getKind() != RolloutKind.rollout) { + out.name("kind").value(r.getKind().name()); + } + if (r.getSeed() != null) { + out.name("seed").value(r.getSeed()); + } + out.endObject(); + } + + @Override + public Rollout read(JsonReader in) throws IOException { + ContextKind contextKind = null; + List variations = new ArrayList<>(); + String bucketByString = null; + RolloutKind kind = RolloutKind.rollout; + Integer seed = null; + in.beginObject(); + while (in.hasNext()) { + switch (in.nextName()) { + case "contextKind": + contextKind = ContextKind.of(in.nextString()); + break; + case "variations": + if (in.peek() == JsonToken.NULL) { + in.skipValue(); + } else { + in.beginArray(); + while (in.hasNext()) { + WeightedVariation wv = gsonInstanceWithNullsAllowed().fromJson(in, WeightedVariation.class); + variations.add(wv); + } + in.endArray(); + } + break; + case "bucketBy": + bucketByString = in.nextString(); + break; + case "kind": + kind = RolloutKind.experiment.name().equals(in.nextString()) ? RolloutKind.experiment : + RolloutKind.rollout; + break; + case "seed": + seed = readNullableInt(in); + break; + default: + in.skipValue(); + } + } + in.endObject(); + AttributeRef bucketBy = attributeNameOrPath(bucketByString, contextKind); + return new Rollout(contextKind, variations, bucketBy, kind, seed); + } + } + + // Custom deserialization logic for SegmentRule for a similar reason to Clause. + static class SegmentRuleTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, SegmentRule sr) throws IOException { + out.beginObject(); + out.name("clauses").beginArray(); + for (Clause c: sr.getClauses()) { + gsonInstanceWithNullsAllowed().toJson(c, Clause.class, out); + } + out.endArray(); + if (sr.getWeight() != null) { + out.name("weight").value(sr.getWeight()); + } + if (sr.getRolloutContextKind() != null) { + out.name("rolloutContextKind").value(sr.getRolloutContextKind().toString()); + } + if (sr.getBucketBy() != null) { + out.name("bucketBy").value(sr.getBucketBy().toString()); + } + out.endObject(); + } + + @Override + public SegmentRule read(JsonReader in) throws IOException { + List clauses = new ArrayList<>(); + Integer weight = null; + ContextKind rolloutContextKind = null; + String bucketByString = null; + in.beginObject(); + while (in.hasNext()) { + switch (in.nextName()) { + case "clauses": + if (in.peek() == JsonToken.NULL) { + in.skipValue(); + } else { + in.beginArray(); + while (in.hasNext()) { + Clause c = gsonInstanceWithNullsAllowed().fromJson(in, Clause.class); + clauses.add(c); + } + in.endArray(); + } + break; + case "weight": + weight = readNullableInt(in); + break; + case "rolloutContextKind": + rolloutContextKind = ContextKind.of(in.nextString()); + break; + case "bucketBy": + bucketByString = in.nextString(); + break; + default: + in.skipValue(); + } + } + in.endObject(); + AttributeRef bucketBy = attributeNameOrPath(bucketByString, rolloutContextKind); + return new SegmentRule(clauses, weight, rolloutContextKind, bucketBy); + } + } + + static Integer readNullableInt(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.skipValue(); + return null; + } + return in.nextInt(); + } + + static AttributeRef attributeNameOrPath(String attrString, ContextKind contextKind) { + if (attrString == null) { + return null; + } + return contextKind == null ? AttributeRef.fromLiteral(attrString) : AttributeRef.fromPath(attrString); + } } diff --git a/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java b/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java index e681e5147..ba9c261d6 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataSourceUpdatesImpl.java @@ -11,13 +11,13 @@ import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.Status; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.StatusListener; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; @@ -48,7 +48,7 @@ * * @since 4.11.0 */ -final class DataSourceUpdatesImpl implements DataSourceUpdates { +final class DataSourceUpdatesImpl implements DataSourceUpdateSink { private final DataStore store; private final EventBroadcasterImpl flagChangeEventNotifier; private final EventBroadcasterImpl dataSourceStatusNotifier; diff --git a/src/main/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImpl.java b/src/main/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImpl.java index 207ee24e3..7695ca792 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImpl.java @@ -1,7 +1,7 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; +import com.launchdarkly.sdk.server.subsystems.DataStore; final class DataStoreStatusProviderImpl implements DataStoreStatusProvider { private final DataStore store; diff --git a/src/main/java/com/launchdarkly/sdk/server/DataStoreUpdatesImpl.java b/src/main/java/com/launchdarkly/sdk/server/DataStoreUpdatesImpl.java index 93b01eb38..21a1cabd6 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DataStoreUpdatesImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/DataStoreUpdatesImpl.java @@ -1,11 +1,11 @@ package com.launchdarkly.sdk.server; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStoreUpdates; +import com.launchdarkly.sdk.server.subsystems.DataStoreUpdateSink; import java.util.concurrent.atomic.AtomicReference; -class DataStoreUpdatesImpl implements DataStoreUpdates { +class DataStoreUpdatesImpl implements DataStoreUpdateSink { // package-private because it's convenient to use these from DataStoreStatusProviderImpl final EventBroadcasterImpl statusBroadcaster; final AtomicReference lastStatus; diff --git a/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessor.java b/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessor.java deleted file mode 100644 index a96170cbe..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessor.java +++ /dev/null @@ -1,649 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.logging.LogValues; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.server.EventSummarizer.EventSummary; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventProcessor; -import com.launchdarkly.sdk.server.interfaces.EventSender; -import com.launchdarkly.sdk.server.interfaces.EventSender.EventDataKind; - -import java.io.IOException; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; - -final class DefaultEventProcessor implements EventProcessor { - @VisibleForTesting final EventDispatcher dispatcher; - private final BlockingQueue inbox; - private final ScheduledExecutorService scheduler; - private final AtomicBoolean closed = new AtomicBoolean(false); - private final List> scheduledTasks = new ArrayList<>(); - private volatile boolean inputCapacityExceeded = false; - private final LDLogger logger; - - DefaultEventProcessor( - EventsConfiguration eventsConfig, - ScheduledExecutorService sharedExecutor, - int threadPriority, - DiagnosticAccumulator diagnosticAccumulator, - DiagnosticEvent.Init diagnosticInitEvent, - LDLogger logger - ) { - inbox = new ArrayBlockingQueue<>(eventsConfig.capacity); - - scheduler = sharedExecutor; - this.logger = logger; - - dispatcher = new EventDispatcher( - eventsConfig, - sharedExecutor, - threadPriority, - inbox, - closed, - diagnosticAccumulator, - diagnosticInitEvent, - logger - ); - - Runnable flusher = () -> { - postMessageAsync(MessageType.FLUSH, null); - }; - scheduledTasks.add(this.scheduler.scheduleAtFixedRate(flusher, eventsConfig.flushInterval.toMillis(), - eventsConfig.flushInterval.toMillis(), TimeUnit.MILLISECONDS)); - Runnable userKeysFlusher = () -> { - postMessageAsync(MessageType.FLUSH_USERS, null); - }; - scheduledTasks.add(this.scheduler.scheduleAtFixedRate(userKeysFlusher, eventsConfig.userKeysFlushInterval.toMillis(), - eventsConfig.userKeysFlushInterval.toMillis(), TimeUnit.MILLISECONDS)); - if (diagnosticAccumulator != null) { - Runnable diagnosticsTrigger = () -> { - postMessageAsync(MessageType.DIAGNOSTIC, null); - }; - scheduledTasks.add(this.scheduler.scheduleAtFixedRate(diagnosticsTrigger, eventsConfig.diagnosticRecordingInterval.toMillis(), - eventsConfig.diagnosticRecordingInterval.toMillis(), TimeUnit.MILLISECONDS)); - } - } - - @Override - public void sendEvent(Event e) { - if (!closed.get()) { - postMessageAsync(MessageType.EVENT, e); - } - } - - @Override - public void flush() { - if (!closed.get()) { - postMessageAsync(MessageType.FLUSH, null); - } - } - - @Override - public void close() throws IOException { - if (closed.compareAndSet(false, true)) { - scheduledTasks.forEach(task -> task.cancel(false)); - postMessageAsync(MessageType.FLUSH, null); - postMessageAndWait(MessageType.SHUTDOWN, null); - } - } - - @VisibleForTesting - void waitUntilInactive() throws IOException { - postMessageAndWait(MessageType.SYNC, null); - } - - @VisibleForTesting - void postDiagnostic() { - postMessageAsync(MessageType.DIAGNOSTIC, null); - } - - private void postMessageAsync(MessageType type, Event event) { - postToChannel(new EventProcessorMessage(type, event, false)); - } - - private void postMessageAndWait(MessageType type, Event event) { - EventProcessorMessage message = new EventProcessorMessage(type, event, true); - if (postToChannel(message)) { - // COVERAGE: There is no way to reliably cause this to fail in tests - message.waitForCompletion(); - } - } - - private boolean postToChannel(EventProcessorMessage message) { - if (inbox.offer(message)) { - return true; - } - // If the inbox is full, it means the EventDispatcher thread is seriously backed up with not-yet-processed - // events. This is unlikely, but if it happens, it means the application is probably doing a ton of flag - // evaluations across many threads-- so if we wait for a space in the inbox, we risk a very serious slowdown - // of the app. To avoid that, we'll just drop the event. The log warning about this will only be shown once. - boolean alreadyLogged = inputCapacityExceeded; // possible race between this and the next line, but it's of no real consequence - we'd just get an extra log line - inputCapacityExceeded = true; - // COVERAGE: There is no way to reliably cause this condition in tests - if (!alreadyLogged) { - logger.warn("Events are being produced faster than they can be processed; some events will be dropped"); - } - return false; - } - - private static enum MessageType { - EVENT, - FLUSH, - FLUSH_USERS, - DIAGNOSTIC, - SYNC, - SHUTDOWN - } - - private static final class EventProcessorMessage { - private final MessageType type; - private final Event event; - private final Semaphore reply; - - private EventProcessorMessage(MessageType type, Event event, boolean sync) { - this.type = type; - this.event = event; - reply = sync ? new Semaphore(0) : null; - } - - void completed() { - if (reply != null) { - reply.release(); - } - } - - void waitForCompletion() { - if (reply == null) { // COVERAGE: there is no way to make this happen from test code - return; - } - while (true) { - try { - reply.acquire(); - return; - } - catch (InterruptedException ex) { // COVERAGE: there is no way to make this happen from test code. - } - } - } - -// intentionally commented out so this doesn't affect coverage reports when we're not debugging -// @Override -// public String toString() { // for debugging only -// return ((event == null) ? type.toString() : (type + ": " + event.getClass().getSimpleName())) + -// (reply == null ? "" : " (sync)"); -// } - } - - /** - * Takes messages from the input queue, updating the event buffer and summary counters - * on its own thread. - */ - static final class EventDispatcher { - private static final int MAX_FLUSH_THREADS = 5; - private static final int MESSAGE_BATCH_SIZE = 50; - - @VisibleForTesting final EventsConfiguration eventsConfig; - private final BlockingQueue inbox; - private final AtomicBoolean closed; - private final List flushWorkers; - private final AtomicInteger busyFlushWorkersCount; - private final AtomicLong lastKnownPastTime = new AtomicLong(0); - private final AtomicBoolean disabled = new AtomicBoolean(false); - @VisibleForTesting final DiagnosticAccumulator diagnosticAccumulator; - private final ExecutorService sharedExecutor; - private final SendDiagnosticTaskFactory sendDiagnosticTaskFactory; - private final LDLogger logger; - - private long deduplicatedUsers = 0; - - private EventDispatcher( - EventsConfiguration eventsConfig, - ExecutorService sharedExecutor, - int threadPriority, - BlockingQueue inbox, - AtomicBoolean closed, - DiagnosticAccumulator diagnosticAccumulator, - DiagnosticEvent.Init diagnosticInitEvent, - LDLogger logger - ) { - this.eventsConfig = eventsConfig; - this.inbox = inbox; - this.closed = closed; - this.sharedExecutor = sharedExecutor; - this.diagnosticAccumulator = diagnosticAccumulator; - this.busyFlushWorkersCount = new AtomicInteger(0); - this.logger = logger; - - ThreadFactory threadFactory = new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("LaunchDarkly-event-delivery-%d") - .setPriority(threadPriority) - .build(); - - // This queue only holds one element; it represents a flush task that has not yet been - // picked up by any worker, so if we try to push another one and are refused, it means - // all the workers are busy. - final BlockingQueue payloadQueue = new ArrayBlockingQueue<>(1); - - final EventBuffer outbox = new EventBuffer(eventsConfig.capacity, logger); - final SimpleLRUCache userKeys = new SimpleLRUCache(eventsConfig.userKeysCapacity); - - Thread mainThread = threadFactory.newThread(() -> { - runMainLoop(inbox, outbox, userKeys, payloadQueue); - }); - mainThread.setDaemon(true); - - mainThread.setUncaughtExceptionHandler(this::onUncaughtException); - - mainThread.start(); - - flushWorkers = new ArrayList<>(); - EventResponseListener listener = this::handleResponse; - for (int i = 0; i < MAX_FLUSH_THREADS; i++) { - SendEventsTask task = new SendEventsTask( - eventsConfig, - listener, - payloadQueue, - busyFlushWorkersCount, - threadFactory, - logger - ); - flushWorkers.add(task); - } - - if (diagnosticAccumulator != null) { - // Set up diagnostics - this.sendDiagnosticTaskFactory = new SendDiagnosticTaskFactory(eventsConfig, this::handleResponse); - sharedExecutor.submit(sendDiagnosticTaskFactory.createSendDiagnosticTask(diagnosticInitEvent)); - } else { - sendDiagnosticTaskFactory = null; - } - } - - private void onUncaughtException(Thread thread, Throwable e) { - // The thread's main loop catches all exceptions, so we'll only get here if an Error was thrown. - // In that case, the application is probably already in a bad state, but we can try to degrade - // relatively gracefully by performing an orderly shutdown of the event processor, so the - // application won't end up blocking on a queue that's no longer being consumed. - // COVERAGE: there is no way to make this happen from test code. - - logger.error("Event processor thread was terminated by an unrecoverable error. No more analytics events will be sent. {} {}", - LogValues.exceptionSummary(e), LogValues.exceptionTrace(e)); - // Note that this is a rare case where we always log the exception stacktrace, instead of only - // logging it at debug level. That's because an exception of this kind should never happen and, - // if it happens, may be difficult to debug. - - // Flip the switch to prevent DefaultEventProcessor from putting any more messages on the queue - closed.set(true); - // Now discard everything that was on the queue, but also make sure no one was blocking on a message - List messages = new ArrayList(); - inbox.drainTo(messages); - for (EventProcessorMessage m: messages) { - m.completed(); - } - } - - /** - * This task drains the input queue as quickly as possible. Everything here is done on a single - * thread so we don't have to synchronize on our internal structures; when it's time to flush, - * triggerFlush will hand the events off to another task. - */ - private void runMainLoop(BlockingQueue inbox, - EventBuffer outbox, SimpleLRUCache userKeys, - BlockingQueue payloadQueue) { - List batch = new ArrayList(MESSAGE_BATCH_SIZE); - while (true) { - try { - batch.clear(); - batch.add(inbox.take()); // take() blocks until a message is available - inbox.drainTo(batch, MESSAGE_BATCH_SIZE - 1); // this nonblocking call allows us to pick up more messages if available - for (EventProcessorMessage message: batch) { - switch (message.type) { // COVERAGE: adding a default branch does not prevent coverage warnings here due to compiler issues - case EVENT: - processEvent(message.event, userKeys, outbox); - break; - case FLUSH: - triggerFlush(outbox, payloadQueue); - break; - case FLUSH_USERS: - userKeys.clear(); - break; - case DIAGNOSTIC: - sendAndResetDiagnostics(outbox); - break; - case SYNC: // this is used only by unit tests - waitUntilAllFlushWorkersInactive(); - break; - case SHUTDOWN: - doShutdown(); - message.completed(); - return; // deliberately exit the thread loop - } - message.completed(); - } - } catch (InterruptedException e) { - } catch (Exception e) { // COVERAGE: there is no way to cause this condition in tests - logger.error("Unexpected error in event processor: {}", e.toString()); - logger.debug(e.toString(), e); - } - } - } - - private void sendAndResetDiagnostics(EventBuffer outbox) { - if (disabled.get()) { - return; - } - long droppedEvents = outbox.getAndClearDroppedCount(); - // We pass droppedEvents and deduplicatedUsers as parameters here because they are updated frequently in the main loop so we want to avoid synchronization on them. - DiagnosticEvent diagnosticEvent = diagnosticAccumulator.createEventAndReset(droppedEvents, deduplicatedUsers); - deduplicatedUsers = 0; - sharedExecutor.submit(sendDiagnosticTaskFactory.createSendDiagnosticTask(diagnosticEvent)); - } - - private void doShutdown() { - waitUntilAllFlushWorkersInactive(); - disabled.set(true); // In case there are any more messages, we want to ignore them - for (SendEventsTask task: flushWorkers) { - task.stop(); - } - try { - eventsConfig.eventSender.close(); - } catch (IOException e) { - logger.error("Unexpected error when closing event sender: {}", LogValues.exceptionSummary(e)); - logger.debug(LogValues.exceptionTrace(e)); - } - } - - private void waitUntilAllFlushWorkersInactive() { - while (true) { - try { - synchronized(busyFlushWorkersCount) { - if (busyFlushWorkersCount.get() == 0) { - return; - } else { - busyFlushWorkersCount.wait(); - } - } - } catch (InterruptedException e) {} // COVERAGE: there is no way to cause this condition in tests - } - } - - private void processEvent(Event e, SimpleLRUCache userKeys, EventBuffer outbox) { - if (disabled.get()) { - return; - } - - // Always record the event in the summarizer. - outbox.addToSummary(e); - - // Decide whether to add the event to the payload. Feature events may be added twice, once for - // the event (if tracked) and once for debugging. - boolean addIndexEvent = false, - addFullEvent = false; - Event debugEvent = null; - - if (e instanceof Event.FeatureRequest) { - Event.FeatureRequest fe = (Event.FeatureRequest)e; - addFullEvent = fe.isTrackEvents(); - if (shouldDebugEvent(fe)) { - debugEvent = EventFactory.newDebugEvent(fe); - } - } else { - addFullEvent = true; - } - - // For each user we haven't seen before, we add an index event - unless this is already - // an identify event for that user. - if (!addFullEvent || !eventsConfig.inlineUsersInEvents) { - LDUser user = e.getUser(); - if (user != null && user.getKey() != null) { - if (e instanceof Event.FeatureRequest || e instanceof Event.Custom) { - String key = user.getKey(); - // Add to the set of users we've noticed - boolean alreadySeen = (userKeys.put(key, key) != null); - if (alreadySeen) { - deduplicatedUsers++; - } else { - addIndexEvent = true; - } - } else if (e instanceof Event.Identify) { - String key = user.getKey(); - userKeys.put(key, key); // just mark that we've seen it - } - } - } - - if (addIndexEvent) { - Event.Index ie = new Event.Index(e.getCreationDate(), e.getUser()); - outbox.add(ie); - } - if (addFullEvent) { - outbox.add(e); - } - if (debugEvent != null) { - outbox.add(debugEvent); - } - } - - private boolean shouldDebugEvent(Event.FeatureRequest fe) { - long debugEventsUntilDate = fe.getDebugEventsUntilDate(); - if (debugEventsUntilDate > 0) { - // The "last known past time" comes from the last HTTP response we got from the server. - // In case the client's time is set wrong, at least we know that any expiration date - // earlier than that point is definitely in the past. If there's any discrepancy, we - // want to err on the side of cutting off event debugging sooner. - long lastPast = lastKnownPastTime.get(); - if (debugEventsUntilDate > lastPast && - debugEventsUntilDate > System.currentTimeMillis()) { - return true; - } - } - return false; - } - - private void triggerFlush(EventBuffer outbox, BlockingQueue payloadQueue) { - if (disabled.get() || outbox.isEmpty()) { - return; - } - FlushPayload payload = outbox.getPayload(); - if (diagnosticAccumulator != null) { - diagnosticAccumulator.recordEventsInBatch(payload.events.length); - } - busyFlushWorkersCount.incrementAndGet(); - if (payloadQueue.offer(payload)) { - // These events now belong to the next available flush worker, so drop them from our state - outbox.clear(); - } else { - logger.debug("Skipped flushing because all workers are busy"); - // All the workers are busy so we can't flush now; keep the events in our state - outbox.summarizer.restoreTo(payload.summary); - synchronized(busyFlushWorkersCount) { - busyFlushWorkersCount.decrementAndGet(); - busyFlushWorkersCount.notify(); - } - } - } - - private void handleResponse(EventSender.Result result) { - if (result.getTimeFromServer() != null) { - lastKnownPastTime.set(result.getTimeFromServer().getTime()); - } - if (result.isMustShutDown()) { - disabled.set(true); - } - } - } - - private static final class EventBuffer { - final List events = new ArrayList<>(); - final EventSummarizer summarizer = new EventSummarizer(); - private final int capacity; - private final LDLogger logger; - private boolean capacityExceeded = false; - private long droppedEventCount = 0; - - EventBuffer(int capacity, LDLogger logger) { - this.capacity = capacity; - this.logger = logger; - } - - void add(Event e) { - if (events.size() >= capacity) { - if (!capacityExceeded) { // don't need AtomicBoolean, this is only checked on one thread - capacityExceeded = true; - logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); - } - droppedEventCount++; - } else { - capacityExceeded = false; - events.add(e); - } - } - - void addToSummary(Event e) { - summarizer.summarizeEvent(e); - } - - boolean isEmpty() { - return events.isEmpty() && summarizer.isEmpty(); - } - - long getAndClearDroppedCount() { - long res = droppedEventCount; - droppedEventCount = 0; - return res; - } - - FlushPayload getPayload() { - Event[] eventsOut = events.toArray(new Event[events.size()]); - EventSummarizer.EventSummary summary = summarizer.getSummaryAndReset(); - return new FlushPayload(eventsOut, summary); - } - - void clear() { - events.clear(); - summarizer.clear(); - } - } - - private static final class FlushPayload { - final Event[] events; - final EventSummary summary; - - FlushPayload(Event[] events, EventSummary summary) { - this.events = events; - this.summary = summary; - } - } - - private static interface EventResponseListener { - void handleResponse(EventSender.Result result); - } - - private static final class SendEventsTask implements Runnable { - private final EventsConfiguration eventsConfig; - private final EventResponseListener responseListener; - private final BlockingQueue payloadQueue; - private final AtomicInteger activeFlushWorkersCount; - private final AtomicBoolean stopping; - private final EventOutputFormatter formatter; - private final Thread thread; - private final LDLogger logger; - - SendEventsTask( - EventsConfiguration eventsConfig, - EventResponseListener responseListener, - BlockingQueue payloadQueue, - AtomicInteger activeFlushWorkersCount, - ThreadFactory threadFactory, - LDLogger logger - ) { - this.eventsConfig = eventsConfig; - this.formatter = new EventOutputFormatter(eventsConfig); - this.responseListener = responseListener; - this.payloadQueue = payloadQueue; - this.activeFlushWorkersCount = activeFlushWorkersCount; - this.stopping = new AtomicBoolean(false); - this.logger = logger; - thread = threadFactory.newThread(this); - thread.setDaemon(true); - thread.start(); - } - - public void run() { - while (!stopping.get()) { - FlushPayload payload = null; - try { - payload = payloadQueue.take(); - } catch (InterruptedException e) { - continue; - } - try { - StringWriter stringWriter = new StringWriter(); - int outputEventCount = formatter.writeOutputEvents(payload.events, payload.summary, stringWriter); - EventSender.Result result = eventsConfig.eventSender.sendEventData( - EventDataKind.ANALYTICS, - stringWriter.toString(), - outputEventCount, - eventsConfig.eventsUri - ); - responseListener.handleResponse(result); - } catch (Exception e) { - logger.error("Unexpected error in event processor: {}", LogValues.exceptionSummary(e)); - logger.debug(LogValues.exceptionTrace(e)); - } - synchronized (activeFlushWorkersCount) { - activeFlushWorkersCount.decrementAndGet(); - activeFlushWorkersCount.notifyAll(); - } - } - } - - void stop() { - stopping.set(true); - thread.interrupt(); - } - } - - private static final class SendDiagnosticTaskFactory { - private final EventsConfiguration eventsConfig; - private final EventResponseListener eventResponseListener; - - SendDiagnosticTaskFactory( - EventsConfiguration eventsConfig, - EventResponseListener eventResponseListener - ) { - this.eventsConfig = eventsConfig; - this.eventResponseListener = eventResponseListener; - } - - Runnable createSendDiagnosticTask(final DiagnosticEvent diagnosticEvent) { - return new Runnable() { - @Override - public void run() { - String json = JsonHelpers.serialize(diagnosticEvent); - EventSender.Result result = eventsConfig.eventSender.sendEventData(EventDataKind.DIAGNOSTICS, - json, 1, eventsConfig.eventsUri); - if (eventResponseListener != null) { - eventResponseListener.handleResponse(result); - } - } - }; - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java b/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java new file mode 100644 index 000000000..b37497a6b --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/DefaultEventProcessorWrapper.java @@ -0,0 +1,70 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.internal.events.DefaultEventProcessor; +import com.launchdarkly.sdk.internal.events.Event; +import com.launchdarkly.sdk.internal.events.EventsConfiguration; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; + +import java.io.IOException; + +final class DefaultEventProcessorWrapper implements EventProcessor { + private final DefaultEventProcessor eventProcessor; + final EventsConfiguration eventsConfig; // visible for testing + + DefaultEventProcessorWrapper(ClientContext clientContext, EventsConfiguration eventsConfig) { + this.eventsConfig = eventsConfig; + LDLogger baseLogger = clientContext.getBaseLogger(); + LDLogger logger = baseLogger.subLogger(Loggers.EVENTS_LOGGER_NAME); + eventProcessor = new DefaultEventProcessor( + eventsConfig, + ClientContextImpl.get(clientContext).sharedExecutor, + clientContext.getThreadPriority(), + logger + ); + } + + @Override + public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation, + LDValue value, EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey, + boolean requireFullEvent, Long debugEventsUntilDate) { + eventProcessor.sendEvent(new Event.FeatureRequest( + System.currentTimeMillis(), + flagKey, + context, + flagVersion, + variation, + value, + defaultValue, + reason, + prerequisiteOfFlagKey, + requireFullEvent, + debugEventsUntilDate, + false + )); + } + + @Override + public void recordIdentifyEvent(LDContext context) { + eventProcessor.sendEvent(new Event.Identify(System.currentTimeMillis(), context)); + } + + @Override + public void recordCustomEvent(LDContext context, String eventKey, LDValue data, Double metricValue) { + eventProcessor.sendEvent(new Event.Custom(System.currentTimeMillis(), eventKey, context, data, metricValue)); + } + + @Override + public void flush() { + eventProcessor.flushAsync(); + } + + @Override + public void close() throws IOException { + eventProcessor.close(); + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/DefaultEventSender.java b/src/main/java/com/launchdarkly/sdk/server/DefaultEventSender.java deleted file mode 100644 index 7c2e4569e..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/DefaultEventSender.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.EventSender; -import com.launchdarkly.sdk.server.interfaces.EventSenderFactory; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; - -import java.io.IOException; -import java.net.URI; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.time.Duration; -import java.util.Date; -import java.util.Locale; -import java.util.UUID; - -import static com.launchdarkly.sdk.server.Util.checkIfErrorIsRecoverableAndLog; -import static com.launchdarkly.sdk.server.Util.concatenateUriPath; -import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder; -import static com.launchdarkly.sdk.server.Util.describeDuration; -import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor; -import static com.launchdarkly.sdk.server.Util.httpErrorDescription; -import static com.launchdarkly.sdk.server.Util.shutdownHttpClient; - -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; - -final class DefaultEventSender implements EventSender { - static final Duration DEFAULT_RETRY_DELAY = Duration.ofSeconds(1); - private static final String EVENT_SCHEMA_HEADER = "X-LaunchDarkly-Event-Schema"; - private static final String EVENT_SCHEMA_VERSION = "3"; - private static final String EVENT_PAYLOAD_ID_HEADER = "X-LaunchDarkly-Payload-ID"; - private static final MediaType JSON_CONTENT_TYPE = MediaType.parse("application/json; charset=utf-8"); - private static final SimpleDateFormat HTTP_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", - Locale.US); // server dates as defined by RFC-822/RFC-1123 use English day/month names - private static final Object HTTP_DATE_FORMAT_LOCK = new Object(); // synchronize on this because DateFormat isn't thread-safe - - private final OkHttpClient httpClient; - private final Headers baseHeaders; - final Duration retryDelay; // visible for testing - private final LDLogger logger; - - DefaultEventSender( - HttpConfiguration httpConfiguration, - Duration retryDelay, - LDLogger logger - ) { - OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); - configureHttpClientBuilder(httpConfiguration, httpBuilder); - this.httpClient = httpBuilder.build(); - this.logger = logger; - - this.baseHeaders = getHeadersBuilderFor(httpConfiguration) - .add("Content-Type", "application/json") - .build(); - - this.retryDelay = retryDelay == null ? DEFAULT_RETRY_DELAY : retryDelay; - } - - @Override - public void close() throws IOException { - shutdownHttpClient(httpClient); - } - - @Override - public Result sendEventData(EventDataKind kind, String data, int eventCount, URI eventsBaseUri) { - if (data == null || data.isEmpty()) { - // DefaultEventProcessor won't normally pass us an empty payload, but if it does, don't bother sending - return new Result(true, false, null); - } - - Headers.Builder headersBuilder = baseHeaders.newBuilder(); - String path; - String description; - - switch (kind) { - case ANALYTICS: - path = StandardEndpoints.ANALYTICS_EVENTS_POST_REQUEST_PATH; - String eventPayloadId = UUID.randomUUID().toString(); - headersBuilder.add(EVENT_PAYLOAD_ID_HEADER, eventPayloadId); - headersBuilder.add(EVENT_SCHEMA_HEADER, EVENT_SCHEMA_VERSION); - description = String.format("%d event(s)", eventCount); - break; - case DIAGNOSTICS: - path = StandardEndpoints.DIAGNOSTIC_EVENTS_POST_REQUEST_PATH; - description = "diagnostic event"; - break; - default: - throw new IllegalArgumentException("kind"); // COVERAGE: unreachable code, those are the only enum values - } - - URI uri = concatenateUriPath(eventsBaseUri, path); - Headers headers = headersBuilder.build(); - RequestBody body = RequestBody.create(data, JSON_CONTENT_TYPE); - boolean mustShutDown = false; - - logger.debug("Posting {} to {} with payload: {}", description, uri, data); - - for (int attempt = 0; attempt < 2; attempt++) { - if (attempt > 0) { - logger.warn("Will retry posting {} after {}", description, describeDuration(retryDelay)); - try { - Thread.sleep(retryDelay.toMillis()); - } catch (InterruptedException e) { // COVERAGE: there's no way to cause this in tests - } - } - - Request request = new Request.Builder() - .url(uri.toASCIIString()) - .post(body) - .headers(headers) - .build(); - - long startTime = System.currentTimeMillis(); - String nextActionMessage = attempt == 0 ? "will retry" : "some events were dropped"; - String errorContext = "posting " + description; - - try (Response response = httpClient.newCall(request).execute()) { - long endTime = System.currentTimeMillis(); - logger.debug("{} delivery took {} ms, response status {}", description, endTime - startTime, response.code()); - - if (response.isSuccessful()) { - return new Result(true, false, parseResponseDate(response)); - } - - String errorDesc = httpErrorDescription(response.code()); - boolean recoverable = checkIfErrorIsRecoverableAndLog( - logger, - errorDesc, - errorContext, - response.code(), - nextActionMessage - ); - if (!recoverable) { - mustShutDown = true; - break; - } - } catch (IOException e) { - checkIfErrorIsRecoverableAndLog(logger, e.toString(), errorContext, 0, nextActionMessage); - } - } - - return new Result(false, mustShutDown, null); - } - - private final Date parseResponseDate(Response response) { - String dateStr = response.header("Date"); - if (dateStr != null) { - try { - // DateFormat is not thread-safe, so must synchronize - synchronized (HTTP_DATE_FORMAT_LOCK) { - return HTTP_DATE_FORMAT.parse(dateStr); - } - } catch (ParseException e) { - logger.warn("Received invalid Date header from events service"); - } - } - return null; - } - - static final class Factory implements EventSenderFactory { - @Override - public EventSender createEventSender(BasicConfiguration basicConfiguration, HttpConfiguration httpConfiguration) { - return new DefaultEventSender(httpConfiguration, DefaultEventSender.DEFAULT_RETRY_DELAY, - LDLogger.none()); - } - - @Override - public EventSender createEventSender( - BasicConfiguration basicConfiguration, - HttpConfiguration httpConfiguration, - LDLogger logger) { - return new DefaultEventSender(httpConfiguration, DefaultEventSender.DEFAULT_RETRY_DELAY, logger); - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java b/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java index 7fb7f1223..9668316e1 100644 --- a/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java +++ b/src/main/java/com/launchdarkly/sdk/server/DefaultFeatureRequestor.java @@ -3,10 +3,12 @@ import com.google.common.annotations.VisibleForTesting; import com.google.gson.stream.JsonReader; import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.internal.http.HttpErrors.HttpErrorException; +import com.launchdarkly.sdk.internal.http.HttpHelpers; +import com.launchdarkly.sdk.internal.http.HttpProperties; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import java.io.IOException; import java.net.URI; @@ -14,10 +16,6 @@ import java.nio.file.Path; import static com.launchdarkly.sdk.server.DataModelSerialization.parseFullDataSet; -import static com.launchdarkly.sdk.server.Util.concatenateUriPath; -import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder; -import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor; -import static com.launchdarkly.sdk.server.Util.shutdownHttpClient; import okhttp3.Cache; import okhttp3.Headers; @@ -38,14 +36,13 @@ final class DefaultFeatureRequestor implements FeatureRequestor { private final Path cacheDir; private final LDLogger logger; - DefaultFeatureRequestor(HttpConfiguration httpConfig, URI baseUri, LDLogger logger) { + DefaultFeatureRequestor(HttpProperties httpProperties, URI baseUri, LDLogger logger) { this.baseUri = baseUri; - this.pollingUri = concatenateUriPath(baseUri, StandardEndpoints.POLLING_REQUEST_PATH); + this.pollingUri = HttpHelpers.concatenateUriPath(baseUri, StandardEndpoints.POLLING_REQUEST_PATH); this.logger = logger; - OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); - configureHttpClientBuilder(httpConfig, httpBuilder); - this.headers = getHeadersBuilderFor(httpConfig).build(); + OkHttpClient.Builder httpBuilder = httpProperties.toHttpClientBuilder(); + this.headers = httpProperties.toHeadersBuilder().build(); try { cacheDir = Files.createTempDirectory("LaunchDarklySDK"); @@ -59,7 +56,7 @@ final class DefaultFeatureRequestor implements FeatureRequestor { } public void close() { - shutdownHttpClient(httpClient); + HttpProperties.shutdownHttpClient(httpClient); Util.deleteDirectory(cacheDir); } @@ -82,10 +79,11 @@ public FullDataSet getAllData(boolean returnDataEvenIfCached) return null; } - logger.debug("Get flag(s) response: " + response.toString()); - logger.debug("Network response: " + response.networkResponse()); - logger.debug("Cache hit count: " + httpClient.cache().hitCount() + " Cache network Count: " + httpClient.cache().networkCount()); - logger.debug("Cache response: " + response.cacheResponse()); + logger.debug("Get flag(s) response: {}", response); + logger.debug("Network response: {}", response.networkResponse()); + logger.debug("Cache hit count: {} Cache network count: {}", + httpClient.cache().hitCount(), httpClient.cache().networkCount()); + logger.debug("Cache response: {}", response.cacheResponse()); if (!response.isSuccessful()) { throw new HttpErrorException(response.code()); diff --git a/src/main/java/com/launchdarkly/sdk/server/DiagnosticAccumulator.java b/src/main/java/com/launchdarkly/sdk/server/DiagnosticAccumulator.java deleted file mode 100644 index cea391e90..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/DiagnosticAccumulator.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.launchdarkly.sdk.server; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -class DiagnosticAccumulator { - - final DiagnosticId diagnosticId; - volatile long dataSinceDate; - private final AtomicInteger eventsInLastBatch = new AtomicInteger(0); - private final Object streamInitsLock = new Object(); - private ArrayList streamInits = new ArrayList<>(); - - DiagnosticAccumulator(DiagnosticId diagnosticId) { - this.diagnosticId = diagnosticId; - this.dataSinceDate = System.currentTimeMillis(); - } - - void recordStreamInit(long timestamp, long durationMillis, boolean failed) { - synchronized (streamInitsLock) { - streamInits.add(new DiagnosticEvent.StreamInit(timestamp, durationMillis, failed)); - } - } - - void recordEventsInBatch(int eventsInBatch) { - eventsInLastBatch.set(eventsInBatch); - } - - DiagnosticEvent.Statistics createEventAndReset(long droppedEvents, long deduplicatedUsers) { - long currentTime = System.currentTimeMillis(); - List eventInits; - synchronized (streamInitsLock) { - eventInits = streamInits; - streamInits = new ArrayList<>(); - } - long eventsInBatch = eventsInLastBatch.getAndSet(0); - DiagnosticEvent.Statistics res = new DiagnosticEvent.Statistics(currentTime, diagnosticId, dataSinceDate, droppedEvents, - deduplicatedUsers, eventsInBatch, eventInits); - dataSinceDate = currentTime; - return res; - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/DiagnosticEvent.java b/src/main/java/com/launchdarkly/sdk/server/DiagnosticEvent.java deleted file mode 100644 index c5803f6a9..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/DiagnosticEvent.java +++ /dev/null @@ -1,202 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.LDValueType; -import com.launchdarkly.sdk.ObjectBuilder; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.DiagnosticDescription; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; - -import java.util.List; -import java.util.Map; - -class DiagnosticEvent { - static enum ConfigProperty { - ALL_ATTRIBUTES_PRIVATE("allAttributesPrivate", LDValueType.BOOLEAN), - CUSTOM_BASE_URI("customBaseURI", LDValueType.BOOLEAN), - CUSTOM_EVENTS_URI("customEventsURI", LDValueType.BOOLEAN), - CUSTOM_STREAM_URI("customStreamURI", LDValueType.BOOLEAN), - DIAGNOSTIC_RECORDING_INTERVAL_MILLIS("diagnosticRecordingIntervalMillis", LDValueType.NUMBER), - EVENTS_CAPACITY("eventsCapacity", LDValueType.NUMBER), - EVENTS_FLUSH_INTERVAL_MILLIS("eventsFlushIntervalMillis", LDValueType.NUMBER), - INLINE_USERS_IN_EVENTS("inlineUsersInEvents", LDValueType.BOOLEAN), - POLLING_INTERVAL_MILLIS("pollingIntervalMillis", LDValueType.NUMBER), - RECONNECT_TIME_MILLIS("reconnectTimeMillis", LDValueType.NUMBER), - SAMPLING_INTERVAL("samplingInterval", LDValueType.NUMBER), - STREAMING_DISABLED("streamingDisabled", LDValueType.BOOLEAN), - USER_KEYS_CAPACITY("userKeysCapacity", LDValueType.NUMBER), - USER_KEYS_FLUSH_INTERVAL_MILLIS("userKeysFlushIntervalMillis", LDValueType.NUMBER), - USING_RELAY_DAEMON("usingRelayDaemon", LDValueType.BOOLEAN); - - String name; - LDValueType type; - - private ConfigProperty(String name, LDValueType type) { - this.name = name; - this.type = type; - } - } - - final String kind; - final long creationDate; - final DiagnosticId id; - - DiagnosticEvent(String kind, long creationDate, DiagnosticId id) { - this.kind = kind; - this.creationDate = creationDate; - this.id = id; - } - - static class StreamInit { - long timestamp; - long durationMillis; - boolean failed; - - StreamInit(long timestamp, long durationMillis, boolean failed) { - this.timestamp = timestamp; - this.durationMillis = durationMillis; - this.failed = failed; - } - } - - static class Statistics extends DiagnosticEvent { - - final long dataSinceDate; - final long droppedEvents; - final long deduplicatedUsers; - final long eventsInLastBatch; - final List streamInits; - - Statistics(long creationDate, DiagnosticId id, long dataSinceDate, long droppedEvents, long deduplicatedUsers, - long eventsInLastBatch, List streamInits) { - super("diagnostic", creationDate, id); - this.dataSinceDate = dataSinceDate; - this.droppedEvents = droppedEvents; - this.deduplicatedUsers = deduplicatedUsers; - this.eventsInLastBatch = eventsInLastBatch; - this.streamInits = streamInits; - } - } - - static class Init extends DiagnosticEvent { - final DiagnosticSdk sdk; - final LDValue configuration; - final DiagnosticPlatform platform = new DiagnosticPlatform(); - - Init( - long creationDate, - DiagnosticId diagnosticId, - LDConfig config, - BasicConfiguration basicConfig, - HttpConfiguration httpConfig - ) { - super("diagnostic-init", creationDate, diagnosticId); - this.sdk = new DiagnosticSdk(httpConfig); - this.configuration = getConfigurationData(config, basicConfig, httpConfig); - } - - static LDValue getConfigurationData(LDConfig config, BasicConfiguration basicConfig, HttpConfiguration httpConfig) { - ObjectBuilder builder = LDValue.buildObject(); - - // Add the top-level properties that are not specific to a particular component type. - builder.put("connectTimeoutMillis", httpConfig.getConnectTimeout().toMillis()); - builder.put("socketTimeoutMillis", httpConfig.getSocketTimeout().toMillis()); - builder.put("usingProxy", httpConfig.getProxy() != null); - builder.put("usingProxyAuthenticator", httpConfig.getProxyAuthentication() != null); - builder.put("startWaitMillis", config.startWait.toMillis()); - - // Allow each pluggable component to describe its own relevant properties. - mergeComponentProperties(builder, config.dataStoreFactory, basicConfig, "dataStoreType"); - mergeComponentProperties(builder, config.dataSourceFactory, basicConfig, null); - mergeComponentProperties(builder, config.eventProcessorFactory, basicConfig, null); - return builder.build(); - } - - // Attempts to add relevant configuration properties, if any, from a customizable component: - // - If the component does not implement DiagnosticDescription, set the defaultPropertyName property to "custom". - // - If it does implement DiagnosticDescription, call its describeConfiguration() method to get a value. - // - If the value is a string, then set the defaultPropertyName property to that value. - // - If the value is an object, then copy all of its properties as long as they are ones we recognize - // and have the expected type. - private static void mergeComponentProperties( - ObjectBuilder builder, - Object component, - BasicConfiguration basicConfig, - String defaultPropertyName - ) { - if (!(component instanceof DiagnosticDescription)) { - if (defaultPropertyName != null) { - builder.put(defaultPropertyName, "custom"); - } - return; - } - LDValue componentDesc = LDValue.normalize(((DiagnosticDescription)component).describeConfiguration(basicConfig)); - if (defaultPropertyName != null) { - builder.put(defaultPropertyName, componentDesc.isString() ? componentDesc.stringValue() : "custom"); - } else if (componentDesc.getType() == LDValueType.OBJECT) { - for (String key: componentDesc.keys()) { - for (ConfigProperty prop: ConfigProperty.values()) { - if (prop.name.equals(key)) { - LDValue value = componentDesc.get(key); - if (value.getType() == prop.type) { - builder.put(key, value); - } - } - } - } - } - } - - static class DiagnosticSdk { - final String name = "java-server-sdk"; - final String version = Version.SDK_VERSION; - final String wrapperName; - final String wrapperVersion; - - DiagnosticSdk(HttpConfiguration httpConfig) { - for (Map.Entry headers: httpConfig.getDefaultHeaders()) { - if (headers.getKey().equalsIgnoreCase("X-LaunchDarkly-Wrapper") ) { - String id = headers.getValue(); - if (id.indexOf("/") >= 0) { - this.wrapperName = id.substring(0, id.indexOf("/")); - this.wrapperVersion = id.substring(id.indexOf("/") + 1); - } else { - this.wrapperName = id; - this.wrapperVersion = null; - } - return; - } - } - this.wrapperName = null; - this.wrapperVersion = null; - } - } - - @SuppressWarnings("unused") // fields are for JSON serialization only - static class DiagnosticPlatform { - private final String name = "Java"; - private final String javaVendor = System.getProperty("java.vendor"); - private final String javaVersion = System.getProperty("java.version"); - private final String osArch = System.getProperty("os.arch"); - final String osName = normalizeOsName(System.getProperty("os.name")); // visible for tests - private final String osVersion = System.getProperty("os.version"); - - DiagnosticPlatform() { - } - - private static String normalizeOsName(String osName) { - // For our diagnostics data, we prefer the standard names "Linux", "MacOS", and "Windows". - // "Linux" is already what the JRE returns in Linux. In Windows, we get "Windows 10" etc. - if (osName != null) { - if (osName.equals("Mac OS X")) { - return "MacOS"; - } - if (osName.startsWith("Windows")) { - return "Windows"; - } - } - return osName; - } - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/DiagnosticId.java b/src/main/java/com/launchdarkly/sdk/server/DiagnosticId.java deleted file mode 100644 index 8601a9780..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/DiagnosticId.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.launchdarkly.sdk.server; - -import java.util.UUID; - -class DiagnosticId { - - final String diagnosticId = UUID.randomUUID().toString(); - final String sdkKeySuffix; - - DiagnosticId(String sdkKey) { - if (sdkKey == null) { - sdkKeySuffix = null; - } else { - this.sdkKeySuffix = sdkKey.substring(Math.max(0, sdkKey.length() - 6)); - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/Evaluator.java b/src/main/java/com/launchdarkly/sdk/server/Evaluator.java index 3c410f5a2..76280a1db 100644 --- a/src/main/java/com/launchdarkly/sdk/server/Evaluator.java +++ b/src/main/java/com/launchdarkly/sdk/server/Evaluator.java @@ -1,9 +1,12 @@ package com.launchdarkly.sdk.server; import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.ErrorKind; import com.launchdarkly.sdk.EvaluationReason.Kind; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; import com.launchdarkly.sdk.server.DataModel.Clause; @@ -17,13 +20,19 @@ import com.launchdarkly.sdk.server.DataModel.Target; import com.launchdarkly.sdk.server.DataModel.VariationOrRollout; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; -import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Map; -import static com.launchdarkly.sdk.server.EvaluatorBucketing.bucketUser; +import static com.launchdarkly.sdk.server.EvaluatorBucketing.computeBucketValue; +import static com.launchdarkly.sdk.server.EvaluatorHelpers.contextKeyIsInTargetList; +import static com.launchdarkly.sdk.server.EvaluatorHelpers.contextKeyIsInTargetLists; +import static com.launchdarkly.sdk.server.EvaluatorHelpers.matchClauseByKind; +import static com.launchdarkly.sdk.server.EvaluatorHelpers.matchClauseWithoutSegments; +import static com.launchdarkly.sdk.server.EvaluatorHelpers.maybeNegate; /** * Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment; @@ -86,17 +95,33 @@ static interface PrerequisiteEvaluationSink { void recordPrerequisiteEvaluation( FeatureFlag flag, FeatureFlag prereqOfFlag, - LDUser user, + LDContext context, EvalResult result ); } + /** + * Represents errors that should terminate evaluation, for situations where it's simpler to use throw/catch + * than to return an error result back up a call chain. + */ + @SuppressWarnings("serial") + static class EvaluationException extends RuntimeException { + final ErrorKind errorKind; + + EvaluationException(ErrorKind errorKind, String message) { + this.errorKind = errorKind; + } + } + /** * This object holds mutable state that Evaluator may need during an evaluation. */ private static class EvaluatorState { - private BigSegmentStoreTypes.Membership bigSegmentsMembership = null; + private Map bigSegmentsMembership = null; private EvaluationReason.BigSegmentsStatus bigSegmentsStatus = null; + private FeatureFlag originalFlag = null; + private List prerequisiteStack = null; + private List segmentStack = null; } Evaluator(Getters getters, LDLogger logger) { @@ -108,52 +133,56 @@ private static class EvaluatorState { * The client's entry point for evaluating a flag. No other Evaluator methods should be exposed. * * @param flag an existing feature flag; any other referenced flags or segments will be queried via {@link Getters} - * @param user the user to evaluate against + * @param context the evaluation context * @param eventFactory produces feature request events * @return an {@link EvalResult} - guaranteed non-null */ - EvalResult evaluate(FeatureFlag flag, LDUser user, PrerequisiteEvaluationSink prereqEvals) { + EvalResult evaluate(FeatureFlag flag, LDContext context, PrerequisiteEvaluationSink prereqEvals) { if (flag.getKey() == INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION) { throw EXPECTED_EXCEPTION_FROM_INVALID_FLAG; } - if (user == null || user.getKey() == null) { - // this should have been prevented by LDClient.evaluateInternal - logger.warn("Null user or null user key when evaluating flag \"{}\"; returning null", flag.getKey()); - return EvalResult.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED); + if (context == null || !context.isValid()) { + // This would be a serious logic error on our part, rather than an application error, since LDClient + // should never be passing a null or invalid context to Evaluator; the SDK should have rejected that + // at a higher level. So we will report it as EXCEPTION to differentiate it from application errors. + logger.error("Null or invalid context was unexpectedly passed to evaluator"); + return EvalResult.error(EvaluationReason.ErrorKind.EXCEPTION); } EvaluatorState state = new EvaluatorState(); + state.originalFlag = flag; - EvalResult result = evaluateInternal(flag, user, prereqEvals, state); - - if (state.bigSegmentsStatus != null) { - return result.withReason( - result.getReason().withBigSegmentsStatus(state.bigSegmentsStatus) - ); + try { + EvalResult result = evaluateInternal(flag, context, prereqEvals, state); + + if (state.bigSegmentsStatus != null) { + return result.withReason( + result.getReason().withBigSegmentsStatus(state.bigSegmentsStatus) + ); + } + return result; + } catch (EvaluationException e) { + logger.error("Could not evaluate flag \"{}\": {}", flag.getKey(), e.getMessage()); + return EvalResult.error(e.errorKind); } - return result; } - private EvalResult evaluateInternal(FeatureFlag flag, LDUser user, + private EvalResult evaluateInternal(FeatureFlag flag, LDContext context, PrerequisiteEvaluationSink prereqEvals, EvaluatorState state) { if (!flag.isOn()) { return EvaluatorHelpers.offResult(flag); } - EvalResult prereqFailureResult = checkPrerequisites(flag, user, prereqEvals, state); + EvalResult prereqFailureResult = checkPrerequisites(flag, context, prereqEvals, state); if (prereqFailureResult != null) { return prereqFailureResult; } // Check to see if targets match - List targets = flag.getTargets(); // guaranteed non-null - int nTargets = targets.size(); - for (int i = 0; i < nTargets; i++) { - Target target = targets.get(i); - if (target.getValues().contains(user.getKey())) { // getValues() is guaranteed non-null - return EvaluatorHelpers.targetMatchResult(flag, target); - } + EvalResult targetMatchResult = checkTargets(flag, context); + if (targetMatchResult != null) { + return targetMatchResult; } // Now walk through the rules and see if any match @@ -161,51 +190,134 @@ private EvalResult evaluateInternal(FeatureFlag flag, LDUser user, int nRules = rules.size(); for (int i = 0; i < nRules; i++) { Rule rule = rules.get(i); - if (ruleMatchesUser(flag, rule, user, state)) { - return computeRuleMatch(flag, user, rule, i); + if (ruleMatchesContext(flag, rule, context, state)) { + return computeRuleMatch(flag, context, rule, i); } } // Walk through the fallthrough and see if it matches - return getValueForVariationOrRollout(flag, flag.getFallthrough(), user, + return getValueForVariationOrRollout(flag, flag.getFallthrough(), context, flag.preprocessed == null ? null : flag.preprocessed.fallthroughResults, EvaluationReason.fallthrough()); } // Checks prerequisites if any; returns null if successful, or an EvalResult if we have to // short-circuit due to a prerequisite failure. - private EvalResult checkPrerequisites(FeatureFlag flag, LDUser user, + private EvalResult checkPrerequisites(FeatureFlag flag, LDContext context, PrerequisiteEvaluationSink prereqEvals, EvaluatorState state) { List prerequisites = flag.getPrerequisites(); // guaranteed non-null int nPrerequisites = prerequisites.size(); - for (int i = 0; i < nPrerequisites; i++) { - Prerequisite prereq = prerequisites.get(i); - boolean prereqOk = true; - FeatureFlag prereqFeatureFlag = getters.getFlag(prereq.getKey()); - if (prereqFeatureFlag == null) { - logger.error("Could not retrieve prerequisite flag \"{}\" when evaluating \"{}\"", prereq.getKey(), flag.getKey()); - prereqOk = false; - } else { - EvalResult prereqEvalResult = evaluateInternal(prereqFeatureFlag, user, prereqEvals, state); - // Note that if the prerequisite flag is off, we don't consider it a match no matter what its - // off variation was. But we still need to evaluate it in order to generate an event. - if (!prereqFeatureFlag.isOn() || prereqEvalResult.getVariationIndex() != prereq.getVariation()) { + if (nPrerequisites == 0) { + return null; + } + + try { + // We use the state object to guard against circular references in prerequisites. To avoid + // the overhead of creating the state.prerequisiteStack list in the most common case where + // there's only a single level prerequisites, we treat state.originalFlag as the first + // element in the stack. + if (flag != state.originalFlag) { + if (state.prerequisiteStack == null) { + state.prerequisiteStack = new ArrayList<>(); + } + state.prerequisiteStack.add(flag.getKey()); + } + + for (int i = 0; i < nPrerequisites; i++) { + Prerequisite prereq = prerequisites.get(i); + String prereqKey = prereq.getKey(); + + if (prereqKey.equals(state.originalFlag.getKey()) || + (flag != state.originalFlag && prereqKey.equals(flag.getKey())) || + (state.prerequisiteStack != null && state.prerequisiteStack.contains(prereqKey))) { + throw new EvaluationException(ErrorKind.MALFORMED_FLAG, + "prerequisite relationship to \"" + prereqKey + "\" caused a circular reference;" + + " this is probably a temporary condition due to an incomplete update"); + } + + boolean prereqOk = true; + FeatureFlag prereqFeatureFlag = getters.getFlag(prereq.getKey()); + if (prereqFeatureFlag == null) { + logger.error("Could not retrieve prerequisite flag \"{}\" when evaluating \"{}\"", prereq.getKey(), flag.getKey()); prereqOk = false; + } else { + EvalResult prereqEvalResult = evaluateInternal(prereqFeatureFlag, context, prereqEvals, state); + // Note that if the prerequisite flag is off, we don't consider it a match no matter what its + // off variation was. But we still need to evaluate it in order to generate an event. + if (!prereqFeatureFlag.isOn() || prereqEvalResult.getVariationIndex() != prereq.getVariation()) { + prereqOk = false; + } + if (prereqEvals != null) { + prereqEvals.recordPrerequisiteEvaluation(prereqFeatureFlag, flag, context, prereqEvalResult); + } } - if (prereqEvals != null) { - prereqEvals.recordPrerequisiteEvaluation(prereqFeatureFlag, flag, user, prereqEvalResult); + if (!prereqOk) { + return EvaluatorHelpers.prerequisiteFailedResult(flag, prereq); } } - if (!prereqOk) { - return EvaluatorHelpers.prerequisiteFailedResult(flag, prereq); + return null; // all prerequisites were satisfied + } + finally { + if (state.prerequisiteStack != null && !state.prerequisiteStack.isEmpty()) { + state.prerequisiteStack.remove(state.prerequisiteStack.size() - 1); } } - return null; } + private static EvalResult checkTargets( + FeatureFlag flag, + LDContext context + ) { + List contextTargets = flag.getContextTargets(); // guaranteed non-null + List userTargets = flag.getTargets(); // guaranteed non-null + int nContextTargets = contextTargets.size(); + int nUserTargets = userTargets.size(); + + if (nContextTargets == 0) { + // old-style data has only targets for users + if (nUserTargets != 0) { + LDContext userContext = context.getIndividualContext(ContextKind.DEFAULT); + if (userContext != null) { + for (int i = 0; i < nUserTargets; i++) { + Target t = userTargets.get(i); + if (t.getValues().contains(userContext.getKey())) { // getValues() is guaranteed non-null + return EvaluatorHelpers.targetMatchResult(flag, t); + } + } + } + } + return null; + } + + // new-style data has ContextTargets, which may include placeholders for user targets that are in Targets + for (int i = 0; i < nContextTargets; i++) { + Target t = contextTargets.get(i); + if (t.getContextKind() == null || t.getContextKind().isDefault()) { + LDContext userContext = context.getIndividualContext(ContextKind.DEFAULT); + if (userContext == null) { + continue; + } + for (int j = 0; j < nUserTargets; j++) { + Target ut = userTargets.get(j); + if (ut.getVariation() == t.getVariation()) { + if (ut.getValues().contains(userContext.getKey())) { + return EvaluatorHelpers.targetMatchResult(flag, t); + } + break; + } + } + } else { + if (contextKeyIsInTargetList(context, t.getContextKind(), t.getValues())) { + return EvaluatorHelpers.targetMatchResult(flag, t); + } + } + } + return null; + } + private EvalResult getValueForVariationOrRollout( FeatureFlag flag, VariationOrRollout vr, - LDUser user, + LDContext context, DataModelPreprocessing.EvalResultFactoryMultiVariations precomputedResults, EvaluationReason reason ) { @@ -217,7 +329,16 @@ private EvalResult getValueForVariationOrRollout( } else { Rollout rollout = vr.getRollout(); if (rollout != null && !rollout.getVariations().isEmpty()) { - float bucket = bucketUser(rollout.getSeed(), user, flag.getKey(), rollout.getBucketBy(), flag.getSalt()); + float bucket = computeBucketValue( + rollout.isExperiment(), + rollout.getSeed(), + context, + rollout.getContextKind(), + flag.getKey(), + rollout.getBucketBy(), + flag.getSalt() + ); + boolean contextWasFound = bucket >= 0; // see comment on computeBucketValue float sum = 0F; List variations = rollout.getVariations(); // guaranteed non-null int nVariations = variations.size(); @@ -226,7 +347,7 @@ private EvalResult getValueForVariationOrRollout( sum += (float) wv.getWeight() / 100000F; if (bucket < sum) { variation = wv.getVariation(); - inExperiment = vr.getRollout().isExperiment() && !wv.isUntracked(); + inExperiment = vr.getRollout().isExperiment() && !wv.isUntracked() && contextWasFound; break; } } @@ -265,101 +386,86 @@ private static EvaluationReason experimentize(EvaluationReason reason) { return reason; } - private boolean ruleMatchesUser(FeatureFlag flag, Rule rule, LDUser user, EvaluatorState state) { + private boolean ruleMatchesContext(FeatureFlag flag, Rule rule, LDContext context, EvaluatorState state) { List clauses = rule.getClauses(); // guaranteed non-null int nClauses = clauses.size(); for (int i = 0; i < nClauses; i++) { Clause clause = clauses.get(i); - if (!clauseMatchesUser(clause, user, state)) { + if (!clauseMatchesContext(clause, context, state)) { return false; } } return true; } - private boolean clauseMatchesUser(Clause clause, LDUser user, EvaluatorState state) { - // In the case of a segment match operator, we check if the user is in any of the segments, - // and possibly negate + private boolean clauseMatchesContext(Clause clause, LDContext context, EvaluatorState state) { if (clause.getOp() == Operator.segmentMatch) { - List values = clause.getValues(); // guaranteed non-null - int nValues = values.size(); - for (int i = 0; i < nValues; i++) { - LDValue clauseValue = values.get(i); - if (clauseValue.isString()) { - Segment segment = getters.getSegment(clauseValue.stringValue()); - if (segment != null) { - if (segmentMatchesUser(segment, user, state)) { - return maybeNegate(clause, true); - } - } - } - } - return maybeNegate(clause, false); + return maybeNegate(clause, matchAnySegment(clause.getValues(), context, state)); } - - return clauseMatchesUserNoSegments(clause, user); - } - - private boolean clauseMatchesUserNoSegments(Clause clause, LDUser user) { - LDValue userValue = user.getAttribute(clause.getAttribute()); - if (userValue.isNull()) { + AttributeRef attr = clause.getAttribute(); + if (attr == null) { + throw new EvaluationException(ErrorKind.MALFORMED_FLAG, "rule clause did not specify an attribute"); + } + if (!attr.isValid()) { + throw new EvaluationException(ErrorKind.MALFORMED_FLAG, + "invalid attribute reference \"" + attr.getError() + "\""); + } + if (attr.getDepth() == 1 && attr.getComponent(0).equals("kind")) { + return maybeNegate(clause, matchClauseByKind(clause, context)); + } + LDContext actualContext = context.getIndividualContext(clause.getContextKind()); + if (actualContext == null) { + return false; + } + LDValue contextValue = actualContext.getValue(attr); + if (contextValue.isNull()) { return false; } - if (userValue.getType() == LDValueType.ARRAY) { - int nValues = userValue.size(); + if (contextValue.getType() == LDValueType.ARRAY) { + int nValues = contextValue.size(); for (int i = 0; i < nValues; i++) { - LDValue value = userValue.get(i); - if (value.getType() == LDValueType.ARRAY || value.getType() == LDValueType.OBJECT) { - logger.error("Invalid custom attribute value in user object for user key \"{}\": {}", user.getKey(), value); - return false; - } - if (clauseMatchAny(clause, value)) { + LDValue value = contextValue.get(i); + if (matchClauseWithoutSegments(clause, value)) { return maybeNegate(clause, true); } } return maybeNegate(clause, false); - } else if (userValue.getType() != LDValueType.OBJECT) { - return maybeNegate(clause, clauseMatchAny(clause, userValue)); + } else if (contextValue.getType() != LDValueType.OBJECT) { + return maybeNegate(clause, matchClauseWithoutSegments(clause, contextValue)); } - logger.warn("Got unexpected user attribute type \"{}\" for user key \"{}\" and attribute \"{}\"", - userValue.getType(), user.getKey(), clause.getAttribute()); return false; } - static boolean clauseMatchAny(Clause clause, LDValue userValue) { - Operator op = clause.getOp(); - if (op != null) { - ClausePreprocessed preprocessed = clause.preprocessed; - if (op == Operator.in) { - // see if we have precomputed a Set for fast equality matching - Set vs = preprocessed == null ? null : preprocessed.valuesSet; - if (vs != null) { - return vs.contains(userValue); + private boolean matchAnySegment(List values, LDContext context, EvaluatorState state) { + // For the segmentMatch operator, the values list is really a list of segment keys. We + // return a match if any of these segments matches the context. + int nValues = values.size(); + for (int i = 0; i < nValues; i++) { + LDValue clauseValue = values.get(i); + if (!clauseValue.isString()) { + continue; + } + String segmentKey = clauseValue.stringValue(); + if (state.segmentStack != null) { + // Clauses within a segment can reference other segments, so we don't want to get stuck in a cycle. + if (state.segmentStack.contains(segmentKey)) { + throw new EvaluationException(ErrorKind.MALFORMED_FLAG, + "segment rule referencing segment \"" + segmentKey + "\" caused a circular reference;" + + " this is probably a temporary condition due to an incomplete update"); } } - List values = clause.getValues(); - List preprocessedValues = - preprocessed == null ? null : preprocessed.valuesExtra; - int n = values.size(); - for (int i = 0; i < n; i++) { - // the preprocessed list, if present, will always have the same size as the values list - ClausePreprocessed.ValueData p = preprocessedValues == null ? null : preprocessedValues.get(i); - LDValue v = values.get(i); - if (EvaluatorOperators.apply(op, userValue, v, p)) { + Segment segment = getters.getSegment(segmentKey); + if (segment != null) { + if (segmentMatchesContext(segment, context, state)) { return true; } } } return false; } - - private boolean maybeNegate(Clause clause, boolean b) { - return clause.isNegate() ? !b : b; - } - private boolean segmentMatchesUser(Segment segment, LDUser user, EvaluatorState state) { - String userKey = user.getKey(); // we've already verified that the key is non-null at the top of evaluate() + private boolean segmentMatchesContext(Segment segment, LDContext context, EvaluatorState state) { if (segment.isUnbounded()) { if (segment.getGeneration() == null) { // Big Segment queries can only be done if the generation is known. If it's unset, that @@ -369,49 +475,78 @@ private boolean segmentMatchesUser(Segment segment, LDUser user, EvaluatorState state.bigSegmentsStatus = EvaluationReason.BigSegmentsStatus.NOT_CONFIGURED; return false; } - - // Even if multiple Big Segments are referenced within a single flag evaluation, we only need - // to do this query once, since it returns *all* of the user's segment memberships. - if (state.bigSegmentsStatus == null) { - BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = getters.getBigSegments(user.getKey()); + LDContext matchContext = context.getIndividualContext(segment.getUnboundedContextKind()); + if (matchContext == null) { + return false; + } + String key = matchContext.getKey(); + BigSegmentStoreTypes.Membership membershipData = + state.bigSegmentsMembership == null ? null : state.bigSegmentsMembership.get(key); + if (membershipData == null) { + BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = getters.getBigSegments(key); if (queryResult == null) { // The SDK hasn't been configured to be able to use big segments state.bigSegmentsStatus = EvaluationReason.BigSegmentsStatus.NOT_CONFIGURED; } else { + membershipData = queryResult.membership; state.bigSegmentsStatus = queryResult.status; - state.bigSegmentsMembership = queryResult.membership; + if (state.bigSegmentsMembership == null) { + state.bigSegmentsMembership = new HashMap<>(); + } + state.bigSegmentsMembership.put(key, membershipData); } } - Boolean membership = state.bigSegmentsMembership == null ? - null : state.bigSegmentsMembership.checkMembership(makeBigSegmentRef(segment)); - if (membership != null) { - return membership; + Boolean membershipResult = membershipData == null ? null : + membershipData.checkMembership(makeBigSegmentRef(segment)); + if (membershipResult != null) { + return membershipResult.booleanValue(); } } else { - if (segment.getIncluded().contains(userKey)) { // getIncluded(), getExcluded(), and getRules() are guaranteed non-null + if (contextKeyIsInTargetList(context, ContextKind.DEFAULT, segment.getIncluded())) { return true; } - if (segment.getExcluded().contains(userKey)) { + if (contextKeyIsInTargetLists(context, segment.getIncludedContexts())) { + return true; + } + if (contextKeyIsInTargetList(context, ContextKind.DEFAULT, segment.getExcluded())) { + return false; + } + if (contextKeyIsInTargetLists(context, segment.getExcludedContexts())) { return false; } } List rules = segment.getRules(); // guaranteed non-null - int nRules = rules.size(); - for (int i = 0; i < nRules; i++) { - SegmentRule rule = rules.get(i); - if (segmentRuleMatchesUser(rule, user, segment.getKey(), segment.getSalt())) { - return true; + if (!rules.isEmpty()) { + // Evaluating rules means we might be doing recursive segment matches, so we'll push the current + // segment key onto the stack for cycle detection. + if (state.segmentStack == null) { + state.segmentStack = new ArrayList<>(); + } + state.segmentStack.add(segment.getKey()); + int nRules = rules.size(); + for (int i = 0; i < nRules; i++) { + SegmentRule rule = rules.get(i); + if (segmentRuleMatchesContext(rule, context, state, segment.getKey(), segment.getSalt())) { + return true; + } } + state.segmentStack.remove(state.segmentStack.size() - 1); } return false; } - - private boolean segmentRuleMatchesUser(SegmentRule segmentRule, LDUser user, String segmentKey, String salt) { + + private boolean segmentRuleMatchesContext( + SegmentRule segmentRule, + LDContext context, + EvaluatorState state, + String segmentKey, + String salt + ) { List clauses = segmentRule.getClauses(); // guaranteed non-null int nClauses = clauses.size(); for (int i = 0; i < nClauses; i++) { Clause c = clauses.get(i); - if (!clauseMatchesUserNoSegments(c, user)) { + if (!clauseMatchesContext(c, context, state)) { return false; } } @@ -421,18 +556,26 @@ private boolean segmentRuleMatchesUser(SegmentRule segmentRule, LDUser user, Str return true; } - // All of the clauses are met. See if the user buckets in - double bucket = EvaluatorBucketing.bucketUser(null, user, segmentKey, segmentRule.getBucketBy(), salt); + // All of the clauses are met. See if the context buckets in + double bucket = computeBucketValue( + false, + null, + context, + segmentRule.getRolloutContextKind(), + segmentKey, + segmentRule.getBucketBy(), + salt + ); double weight = (double)segmentRule.getWeight() / 100000.0; return bucket < weight; } - private EvalResult computeRuleMatch(FeatureFlag flag, LDUser user, Rule rule, int ruleIndex) { + private EvalResult computeRuleMatch(FeatureFlag flag, LDContext context, Rule rule, int ruleIndex) { if (rule.preprocessed != null) { - return getValueForVariationOrRollout(flag, rule, user, rule.preprocessed.allPossibleResults, null); + return getValueForVariationOrRollout(flag, rule, context, rule.preprocessed.allPossibleResults, null); } EvaluationReason reason = EvaluationReason.ruleMatch(ruleIndex, rule.getId()); - return getValueForVariationOrRollout(flag, rule, user, null, reason); + return getValueForVariationOrRollout(flag, rule, context, null, reason); } static String makeBigSegmentRef(Segment segment) { diff --git a/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java b/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java index b770020cb..2a96663d7 100644 --- a/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java +++ b/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java @@ -1,8 +1,9 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import org.apache.commons.codec.digest.DigestUtils; @@ -14,24 +15,52 @@ private EvaluatorBucketing() {} private static final float LONG_SCALE = (float) 0xFFFFFFFFFFFFFFFL; - static float bucketUser(Integer seed, LDUser user, String key, UserAttribute attr, String salt) { - LDValue userValue = user.getAttribute(attr == null ? UserAttribute.KEY : attr); - String idHash = getBucketableStringValue(userValue); - if (idHash != null) { - String prefix; - if (seed != null) { - prefix = seed.toString(); - } else { - prefix = key + "." + salt; + // Computes a bucket value for a rollout or experiment. If an error condition prevents + // us from computing a valid bucket value, we return 0, which will cause the evaluator + // to select the first bucket. A special case is if no context of the desired kind is + // found, in which case we return the special value -1; this similarly will cause the + // first bucket to be chosen (since it is less than the end value of the bucket, just + // as 0 is), but also tells the evaluator that inExperiment must be set to false. + static float computeBucketValue( + boolean isExperiment, + Integer seed, + LDContext context, + ContextKind contextKind, + String flagOrSegmentKey, + AttributeRef attr, + String salt + ) { + LDContext matchContext = context.getIndividualContext(contextKind); + if (matchContext == null) { + return -1; + } + LDValue contextValue; + if (isExperiment || attr == null) { + contextValue = LDValue.of(matchContext.getKey()); + } else { + if (!attr.isValid()) { + return 0; } - if (user.getSecondary() != null) { - idHash = idHash + "." + user.getSecondary(); + contextValue = matchContext.getValue(attr); + if (contextValue.isNull()) { + return 0; } - String hash = DigestUtils.sha1Hex(prefix + "." + idHash).substring(0, 15); - long longVal = Long.parseLong(hash, 16); - return (float) longVal / LONG_SCALE; } - return 0F; + + String idHash = getBucketableStringValue(contextValue); + if (idHash == null) { + return 0; + } + + String prefix; + if (seed != null) { + prefix = seed.toString(); + } else { + prefix = flagOrSegmentKey + "." + salt; + } + String hash = DigestUtils.sha1Hex(prefix + "." + idHash).substring(0, 15); + long longVal = Long.parseLong(hash, 16); + return (float) longVal / LONG_SCALE; } private static String getBucketableStringValue(LDValue userValue) { diff --git a/src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java b/src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java index 68b53de0e..2e4936927 100644 --- a/src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java +++ b/src/main/java/com/launchdarkly/sdk/server/EvaluatorHelpers.java @@ -1,17 +1,31 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.EvaluationReason.ErrorKind; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Operator; import com.launchdarkly.sdk.server.DataModel.Prerequisite; +import com.launchdarkly.sdk.server.DataModel.SegmentTarget; import com.launchdarkly.sdk.server.DataModel.Target; +import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; + +import java.util.Collection; +import java.util.List; +import java.util.Set; import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorHelpers.contextKeyIsInTargetList; /** - * Low-level helpers for producing various kinds of evaluation results. + * Low-level helpers for producing various kinds of evaluation results. We also put any + * helpers here that are used by Evaluator if they are static, i.e. if they can be + * implemented without reference to the Evaluator instance's own state, so as to keep the + * Evaluator logic smaller and easier to follow. *

* For all of the methods that return an {@link EvalResult}, the behavior is as follows: * First we check if the flag data contains a preprocessed value for this kind of result; if @@ -64,4 +78,69 @@ static EvaluationDetail evaluationDetailForVariation(FeatureFlag flag, variation, reason); } + + static boolean maybeNegate(Clause clause, boolean b) { + return clause.isNegate() ? !b : b; + } + + // Performs an operator test between a single context value and all of the clause values, for any + // operator except segmentMatch. + static boolean matchClauseWithoutSegments(Clause clause, LDValue contextValue) { + Operator op = clause.getOp(); + if (op != null) { + ClausePreprocessed preprocessed = clause.preprocessed; + if (op == Operator.in) { + // see if we have precomputed a Set for fast equality matching + Set vs = preprocessed == null ? null : preprocessed.valuesSet; + if (vs != null) { + return vs.contains(contextValue); + } + } + List values = clause.getValues(); + List preprocessedValues = + preprocessed == null ? null : preprocessed.valuesExtra; + int n = values.size(); + for (int i = 0; i < n; i++) { + // the preprocessed list, if present, will always have the same size as the values list + ClausePreprocessed.ValueData p = preprocessedValues == null ? null : preprocessedValues.get(i); + LDValue v = values.get(i); + if (EvaluatorOperators.apply(op, contextValue, v, p)) { + return true; + } + } + } + return false; + } + + static boolean matchClauseByKind(Clause clause, LDContext context) { + // If attribute is "kind", then we treat operator and values as a match expression against a list + // of all individual kinds in the context. That is, for a multi-kind context with kinds of "org" + // and "user", it is a match if either of those strings is a match with Operator and Values. + for (int i = 0; i < context.getIndividualContextCount(); i++) { + if (matchClauseWithoutSegments(clause, LDValue.of( + context.getIndividualContext(i).getKind().toString()))) { + return true; + } + } + return false; + } + + static boolean contextKeyIsInTargetList(LDContext context, ContextKind contextKind, Collection keys) { + if (keys.isEmpty()) { + return false; + } + LDContext matchContext = context.getIndividualContext(contextKind); + return matchContext != null && keys.contains(matchContext.getKey()); + } + + static boolean contextKeyIsInTargetLists(LDContext context, List targets) { + int nTargets = targets.size(); + for (int i = 0; i < nTargets; i++) { + SegmentTarget t = targets.get(i); + if (contextKeyIsInTargetList(context, t.getContextKind(), t.getValues())) { + return true; + } + } + return false; + } } diff --git a/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java b/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java index d3043742b..635e0acb4 100644 --- a/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java +++ b/src/main/java/com/launchdarkly/sdk/server/EvaluatorOperators.java @@ -1,9 +1,13 @@ package com.launchdarkly.sdk.server; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.Operator; import com.launchdarkly.sdk.server.DataModelPreprocessing.ClausePreprocessed; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; import java.util.regex.Pattern; import static com.launchdarkly.sdk.server.EvaluatorTypeConversion.valueToDateTime; @@ -16,138 +20,105 @@ abstract class EvaluatorOperators { private EvaluatorOperators() {} - private static enum ComparisonOp { - EQ, - LT, - LTE, - GT, - GTE; - - boolean test(int delta) { - switch (this) { - case EQ: - return delta == 0; - case LT: - return delta < 0; - case LTE: - return delta <= 0; - case GT: - return delta > 0; - case GTE: - return delta >= 0; - } - // COVERAGE: the compiler insists on a fallthrough line here, even though it's unreachable - return false; - } + private static interface OperatorFn { + boolean match(LDValue userValue, LDValue clauseValue, ClausePreprocessed.ValueData preprocessed); } - + + private static final Map OPERATORS = new HashMap<>(); + static { + OPERATORS.put(Operator.in, EvaluatorOperators::applyIn); + OPERATORS.put(Operator.startsWith, EvaluatorOperators::applyStartsWith); + OPERATORS.put(Operator.endsWith, EvaluatorOperators::applyEndsWith); + OPERATORS.put(Operator.matches, EvaluatorOperators::applyMatches); + OPERATORS.put(Operator.contains, EvaluatorOperators::applyContains); + OPERATORS.put(Operator.lessThan, numericComparison(delta -> delta < 0)); + OPERATORS.put(Operator.lessThanOrEqual, numericComparison(delta -> delta <= 0)); + OPERATORS.put(Operator.greaterThan, numericComparison(delta -> delta > 0)); + OPERATORS.put(Operator.greaterThanOrEqual, numericComparison(delta -> delta >= 0)); + OPERATORS.put(Operator.before, dateComparison(delta -> delta < 0)); + OPERATORS.put(Operator.after, dateComparison(delta -> delta > 0)); + OPERATORS.put(Operator.semVerEqual, semVerComparison(delta -> delta == 0)); + OPERATORS.put(Operator.semVerLessThan, semVerComparison(delta -> delta < 0)); + OPERATORS.put(Operator.semVerGreaterThan, semVerComparison(delta -> delta > 0)); + // Operator.segmentMatch is deliberately not included here, because it is implemented + // separately in Evaluator. + } + static boolean apply( DataModel.Operator op, LDValue userValue, LDValue clauseValue, ClausePreprocessed.ValueData preprocessed ) { - switch (op) { - case in: - return userValue.equals(clauseValue); - - case endsWith: - return userValue.isString() && clauseValue.isString() && userValue.stringValue().endsWith(clauseValue.stringValue()); - - case startsWith: - return userValue.isString() && clauseValue.isString() && userValue.stringValue().startsWith(clauseValue.stringValue()); - - case matches: - // If preprocessed is non-null, it means we've already tried to parse the clause value as a regex, - // in which case if preprocessed.parsedRegex is null it was not a valid regex. - Pattern clausePattern = preprocessed == null ? valueToRegex(clauseValue) : preprocessed.parsedRegex; - return clausePattern != null && userValue.isString() && - clausePattern.matcher(userValue.stringValue()).find(); - - case contains: - return userValue.isString() && clauseValue.isString() && userValue.stringValue().contains(clauseValue.stringValue()); - - case lessThan: - return compareNumeric(ComparisonOp.LT, userValue, clauseValue); - - case lessThanOrEqual: - return compareNumeric(ComparisonOp.LTE, userValue, clauseValue); - - case greaterThan: - return compareNumeric(ComparisonOp.GT, userValue, clauseValue); - - case greaterThanOrEqual: - return compareNumeric(ComparisonOp.GTE, userValue, clauseValue); - - case before: - return compareDate(ComparisonOp.LT, userValue, clauseValue, preprocessed); - - case after: - return compareDate(ComparisonOp.GT, userValue, clauseValue, preprocessed); + OperatorFn fn = OPERATORS.get(op); + return fn != null && fn.match(userValue, clauseValue, preprocessed); + } - case semVerEqual: - return compareSemVer(ComparisonOp.EQ, userValue, clauseValue, preprocessed); + static boolean applyIn(LDValue userValue, LDValue clauseValue, ClausePreprocessed.ValueData preprocessed) { + return userValue.equals(clauseValue); + } - case semVerLessThan: - return compareSemVer(ComparisonOp.LT, userValue, clauseValue, preprocessed); + static boolean applyStartsWith(LDValue userValue, LDValue clauseValue, ClausePreprocessed.ValueData preprocessed) { + return userValue.isString() && clauseValue.isString() && userValue.stringValue().startsWith(clauseValue.stringValue()); + } - case semVerGreaterThan: - return compareSemVer(ComparisonOp.GT, userValue, clauseValue, preprocessed); + static boolean applyEndsWith(LDValue userValue, LDValue clauseValue, ClausePreprocessed.ValueData preprocessed) { + return userValue.isString() && clauseValue.isString() && userValue.stringValue().endsWith(clauseValue.stringValue()); + } - case segmentMatch: - // We shouldn't call apply() for this operator, because it is really implemented in - // Evaluator.clauseMatchesUser(). - return false; - }; - // COVERAGE: the compiler insists on a fallthrough line here, even though it's unreachable - return false; + static boolean applyMatches(LDValue userValue, LDValue clauseValue, ClausePreprocessed.ValueData preprocessed) { + // If preprocessed is non-null, it means we've already tried to parse the clause value as a regex, + // in which case if preprocessed.parsedRegex is null it was not a valid regex. + Pattern clausePattern = preprocessed == null ? valueToRegex(clauseValue) : preprocessed.parsedRegex; + return clausePattern != null && userValue.isString() && + clausePattern.matcher(userValue.stringValue()).find(); } - private static boolean compareNumeric(ComparisonOp op, LDValue userValue, LDValue clauseValue) { - if (!userValue.isNumber() || !clauseValue.isNumber()) { - return false; - } - double n1 = userValue.doubleValue(); - double n2 = clauseValue.doubleValue(); - int compare = n1 == n2 ? 0 : (n1 < n2 ? -1 : 1); - return op.test(compare); + static boolean applyContains(LDValue userValue, LDValue clauseValue, ClausePreprocessed.ValueData preprocessed) { + return userValue.isString() && clauseValue.isString() && userValue.stringValue().contains(clauseValue.stringValue()); } - private static boolean compareDate( - ComparisonOp op, - LDValue userValue, - LDValue clauseValue, - ClausePreprocessed.ValueData preprocessed - ) { - // If preprocessed is non-null, it means we've already tried to parse the clause value as a date/time, - // in which case if preprocessed.parsedDate is null it was not a valid date/time. - Instant clauseDate = preprocessed == null ? valueToDateTime(clauseValue) : preprocessed.parsedDate; - if (clauseDate == null) { - return false; - } - Instant userDate = valueToDateTime(userValue); - if (userDate == null) { - return false; - } - return op.test(userDate.compareTo(clauseDate)); + static OperatorFn numericComparison(Function comparisonTest) { + return (userValue, clauseValue, preprocessed) -> { + if (!userValue.isNumber() || !clauseValue.isNumber()) { + return false; + } + double n1 = userValue.doubleValue(); + double n2 = clauseValue.doubleValue(); + int delta = n1 == n2 ? 0 : (n1 < n2 ? -1 : 1); + return comparisonTest.apply(delta); + }; } - private static boolean compareSemVer( - ComparisonOp op, - LDValue userValue, - LDValue clauseValue, - ClausePreprocessed.ValueData preprocessed - ) { - // If preprocessed is non-null, it means we've already tried to parse the clause value as a version, - // in which case if preprocessed.parsedSemVer is null it was not a valid version. - SemanticVersion clauseVer = preprocessed == null ? valueToSemVer(clauseValue) : preprocessed.parsedSemVer; - if (clauseVer == null) { - return false; - } - SemanticVersion userVer = valueToSemVer(userValue); - if (userVer == null) { - return false; - } - return op.test(userVer.compareTo(clauseVer)); + static OperatorFn dateComparison(Function comparisonTest) { + return (userValue, clauseValue, preprocessed) -> { + // If preprocessed is non-null, it means we've already tried to parse the clause value as a date/time, + // in which case if preprocessed.parsedDate is null it was not a valid date/time. + Instant clauseDate = preprocessed == null ? valueToDateTime(clauseValue) : preprocessed.parsedDate; + if (clauseDate == null) { + return false; + } + Instant userDate = valueToDateTime(userValue); + if (userDate == null) { + return false; + } + return comparisonTest.apply(userDate.compareTo(clauseDate)); + }; + } + + static OperatorFn semVerComparison(Function comparisonTest) { + return (userValue, clauseValue, preprocessed) -> { + // If preprocessed is non-null, it means we've already tried to parse the clause value as a version, + // in which case if preprocessed.parsedSemVer is null it was not a valid version. + SemanticVersion clauseVer = preprocessed == null ? valueToSemVer(clauseValue) : preprocessed.parsedSemVer; + if (clauseVer == null) { + return false; + } + SemanticVersion userVer = valueToSemVer(userValue); + if (userVer == null) { + return false; + } + return comparisonTest.apply(userVer.compareTo(clauseVer)); + }; } } diff --git a/src/main/java/com/launchdarkly/sdk/server/EventFactory.java b/src/main/java/com/launchdarkly/sdk/server/EventFactory.java deleted file mode 100644 index bc06127fd..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/EventFactory.java +++ /dev/null @@ -1,211 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.EvaluationReason.ErrorKind; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.Event.Custom; -import com.launchdarkly.sdk.server.interfaces.Event.FeatureRequest; -import com.launchdarkly.sdk.server.interfaces.Event.Identify; - -import java.util.function.Supplier; - -abstract class EventFactory { - public static final EventFactory DEFAULT = new Default(false, null); - public static final EventFactory DEFAULT_WITH_REASONS = new Default(true, null); - - abstract Event.FeatureRequest newFeatureRequestEvent( - DataModel.FeatureFlag flag, - LDUser user, - LDValue value, - int variationIndex, - EvaluationReason reason, - boolean forceReasonTracking, - LDValue defaultValue, - String prereqOf - ); - - abstract Event.FeatureRequest newUnknownFeatureRequestEvent( - String key, - LDUser user, - LDValue defaultValue, - EvaluationReason.ErrorKind errorKind - ); - - abstract Event.Custom newCustomEvent(String key, LDUser user, LDValue data, Double metricValue); - - abstract Event.Identify newIdentifyEvent(LDUser user); - - abstract Event.AliasEvent newAliasEvent(LDUser user, LDUser previousUser); - - final Event.FeatureRequest newFeatureRequestEvent( - DataModel.FeatureFlag flag, - LDUser user, - EvalResult result, - LDValue defaultValue - ) { - return newFeatureRequestEvent( - flag, - user, - result == null ? null : result.getValue(), - result == null ? -1 : result.getVariationIndex(), - result == null ? null : result.getReason(), - result != null && result.isForceReasonTracking(), - defaultValue, - null - ); - } - - final Event.FeatureRequest newDefaultFeatureRequestEvent( - DataModel.FeatureFlag flag, - LDUser user, - LDValue defaultVal, - EvaluationReason.ErrorKind errorKind - ) { - return newFeatureRequestEvent( - flag, - user, - defaultVal, - -1, - EvaluationReason.error(errorKind), - false, - defaultVal, - null - ); - } - - final Event.FeatureRequest newPrerequisiteFeatureRequestEvent( - DataModel.FeatureFlag prereqFlag, - LDUser user, - EvalResult result, - DataModel.FeatureFlag prereqOf - ) { - return newFeatureRequestEvent( - prereqFlag, - user, - result == null ? null : result.getValue(), - result == null ? -1 : result.getVariationIndex(), - result == null ? null : result.getReason(), - result != null && result.isForceReasonTracking(), - LDValue.ofNull(), - prereqOf.getKey() - ); - } - - static final Event.FeatureRequest newDebugEvent(Event.FeatureRequest from) { - return new Event.FeatureRequest( - from.getCreationDate(), - from.getKey(), - from.getUser(), - from.getVersion(), - from.getVariation(), - from.getValue(), - from.getDefaultVal(), - from.getReason(), - from.getPrereqOf(), - from.isTrackEvents(), - from.getDebugEventsUntilDate(), - true - ); - } - - static class Default extends EventFactory { - private final boolean includeReasons; - private final Supplier timestampFn; - - Default(boolean includeReasons, Supplier timestampFn) { - this.includeReasons = includeReasons; - this.timestampFn = timestampFn != null ? timestampFn : (() -> System.currentTimeMillis()); - } - - @Override - final Event.FeatureRequest newFeatureRequestEvent(DataModel.FeatureFlag flag, LDUser user, - LDValue value, int variationIndex, EvaluationReason reason, boolean forceReasonTracking, - LDValue defaultValue, String prereqOf){ - return new Event.FeatureRequest( - timestampFn.get(), - flag.getKey(), - user, - flag.getVersion(), - variationIndex, - value, - defaultValue, - (forceReasonTracking || includeReasons) ? reason : null, - prereqOf, - forceReasonTracking || flag.isTrackEvents(), - flag.getDebugEventsUntilDate() == null ? 0 : flag.getDebugEventsUntilDate().longValue(), - false - ); - } - - @Override - final Event.FeatureRequest newUnknownFeatureRequestEvent( - String key, - LDUser user, - LDValue defaultValue, - EvaluationReason.ErrorKind errorKind - ) { - return new Event.FeatureRequest( - timestampFn.get(), - key, - user, - -1, - -1, - defaultValue, - defaultValue, - includeReasons ? EvaluationReason.error(errorKind) : null, - null, - false, - 0, - false - ); - } - - @Override - Event.Custom newCustomEvent(String key, LDUser user, LDValue data, Double metricValue) { - return new Event.Custom(timestampFn.get(), key, user, data, metricValue); - } - - @Override - Event.Identify newIdentifyEvent(LDUser user) { - return new Event.Identify(timestampFn.get(), user); - } - - @Override - Event.AliasEvent newAliasEvent(LDUser user, LDUser previousUser) { - return new Event.AliasEvent(timestampFn.get(), user, previousUser); - } - } - - static final class Disabled extends EventFactory { - static final Disabled INSTANCE = new Disabled(); - - @Override - final FeatureRequest newFeatureRequestEvent(FeatureFlag flag, LDUser user, LDValue value, int variationIndex, - EvaluationReason reason, boolean inExperiment, LDValue defaultValue, String prereqOf) { - return null; - } - - @Override - final FeatureRequest newUnknownFeatureRequestEvent(String key, LDUser user, LDValue defaultValue, ErrorKind errorKind) { - return null; - } - - @Override - final Custom newCustomEvent(String key, LDUser user, LDValue data, Double metricValue) { - return null; - } - - @Override - final Identify newIdentifyEvent(LDUser user) { - return null; - } - - @Override - Event.AliasEvent newAliasEvent(LDUser user, LDUser previousUser) { - return null; - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/EventOutputFormatter.java b/src/main/java/com/launchdarkly/sdk/server/EventOutputFormatter.java deleted file mode 100644 index 85f8c3b9d..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/EventOutputFormatter.java +++ /dev/null @@ -1,209 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.gson.Gson; -import com.google.gson.stream.JsonWriter; -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.EventSummarizer.CounterValue; -import com.launchdarkly.sdk.server.EventSummarizer.FlagInfo; -import com.launchdarkly.sdk.server.EventSummarizer.SimpleIntKeyedMap; -import com.launchdarkly.sdk.server.interfaces.Event; - -import java.io.IOException; -import java.io.Writer; -import java.util.Map; - -/** - * Transforms analytics events and summary data into the JSON format that we send to LaunchDarkly. - * Rather than creating intermediate objects to represent this schema, we use the Gson streaming - * output API to construct JSON directly. - * - * Test coverage for this logic is in EventOutputTest and DefaultEventProcessorOutputTest. - */ -final class EventOutputFormatter { - private final EventsConfiguration config; - private final Gson gson; - - EventOutputFormatter(EventsConfiguration config) { - this.config = config; - this.gson = JsonHelpers.gsonInstanceForEventsSerialization(config); - } - - @SuppressWarnings("resource") - final int writeOutputEvents(Event[] events, EventSummarizer.EventSummary summary, Writer writer) throws IOException { - int count = events.length; - try (JsonWriter jsonWriter = new JsonWriter(writer)) { - jsonWriter.beginArray(); - for (Event event: events) { - writeOutputEvent(event, jsonWriter); - } - if (!summary.isEmpty()) { - writeSummaryEvent(summary, jsonWriter); - count++; - } - jsonWriter.endArray(); - } - return count; - } - - private final void writeOutputEvent(Event event, JsonWriter jw) throws IOException { - if (event instanceof Event.FeatureRequest) { - Event.FeatureRequest fe = (Event.FeatureRequest)event; - startEvent(fe, fe.isDebug() ? "debug" : "feature", fe.getKey(), jw); - writeUserOrKey(fe, fe.isDebug(), jw); - if (fe.getVersion() >= 0) { - jw.name("version"); - jw.value(fe.getVersion()); - } - if (fe.getVariation() >= 0) { - jw.name("variation"); - jw.value(fe.getVariation()); - } - writeLDValue("value", fe.getValue(), jw); - writeLDValue("default", fe.getDefaultVal(), jw); - if (fe.getPrereqOf() != null) { - jw.name("prereqOf"); - jw.value(fe.getPrereqOf()); - } - writeEvaluationReason("reason", fe.getReason(), jw); - if (!fe.getContextKind().equals("user")) { - jw.name("contextKind").value(fe.getContextKind()); - } - } else if (event instanceof Event.Identify) { - startEvent(event, "identify", event.getUser() == null ? null : event.getUser().getKey(), jw); - writeUser(event.getUser(), jw); - } else if (event instanceof Event.Custom) { - Event.Custom ce = (Event.Custom)event; - startEvent(event, "custom", ce.getKey(), jw); - writeUserOrKey(ce, false, jw); - writeLDValue("data", ce.getData(), jw); - if (!ce.getContextKind().equals("user")) { - jw.name("contextKind").value(ce.getContextKind()); - } - if (ce.getMetricValue() != null) { - jw.name("metricValue"); - jw.value(ce.getMetricValue()); - } - } else if (event instanceof Event.Index) { - startEvent(event, "index", null, jw); - writeUser(event.getUser(), jw); - } else if (event instanceof Event.AliasEvent) { - Event.AliasEvent ae = (Event.AliasEvent)event; - startEvent(event, "alias", ae.getKey(), jw); - jw.name("contextKind").value(ae.getContextKind()); - jw.name("previousKey").value(ae.getPreviousKey()); - jw.name("previousContextKind").value(ae.getPreviousContextKind()); - } else { - return; - } - - jw.endObject(); - } - - private final void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonWriter jw) throws IOException { - jw.beginObject(); - - jw.name("kind"); - jw.value("summary"); - - jw.name("startDate"); - jw.value(summary.startDate); - jw.name("endDate"); - jw.value(summary.endDate); - - jw.name("features"); - jw.beginObject(); - - for (Map.Entry flag: summary.counters.entrySet()) { - String flagKey = flag.getKey(); - FlagInfo flagInfo = flag.getValue(); - - jw.name(flagKey); - jw.beginObject(); - - writeLDValue("default", flagInfo.defaultVal, jw); - - jw.name("counters"); - jw.beginArray(); - - for (int i = 0; i < flagInfo.versionsAndVariations.size(); i++) { - int version = flagInfo.versionsAndVariations.keyAt(i); - SimpleIntKeyedMap variations = flagInfo.versionsAndVariations.valueAt(i); - for (int j = 0; j < variations.size(); j++) { - int variation = variations.keyAt(j); - CounterValue counter = variations.valueAt(j); - - jw.beginObject(); - - if (variation >= 0) { - jw.name("variation").value(variation); - } - if (version >= 0) { - jw.name("version").value(version); - } else { - jw.name("unknown").value(true); - } - writeLDValue("value", counter.flagValue, jw); - jw.name("count").value(counter.count); - - jw.endObject(); - } - } - - jw.endArray(); // end of "counters" array - jw.endObject(); // end of this flag - } - - jw.endObject(); // end of "features" - jw.endObject(); // end of summary event object - } - - private final void startEvent(Event event, String kind, String key, JsonWriter jw) throws IOException { - jw.beginObject(); - jw.name("kind"); - jw.value(kind); - jw.name("creationDate"); - jw.value(event.getCreationDate()); - if (key != null) { - jw.name("key"); - jw.value(key); - } - } - - private final void writeUserOrKey(Event event, boolean forceInline, JsonWriter jw) throws IOException { - LDUser user = event.getUser(); - if (user != null) { - if (config.inlineUsersInEvents || forceInline) { - writeUser(user, jw); - } else { - jw.name("userKey"); - jw.value(user.getKey()); - } - } - } - - private final void writeUser(LDUser user, JsonWriter jw) throws IOException { - jw.name("user"); - // config.gson is already set up to use our custom serializer, which knows about private attributes - // and already uses the streaming approach - gson.toJson(user, LDUser.class, jw); - } - - private final void writeLDValue(String key, LDValue value, JsonWriter jw) throws IOException { - if (value == null || value.isNull()) { - return; - } - jw.name(key); - gson.toJson(value, LDValue.class, jw); // LDValue defines its own custom serializer - } - - // This logic is so that we don't have to define multiple custom serializers for the various reason subclasses. - private final void writeEvaluationReason(String key, EvaluationReason er, JsonWriter jw) throws IOException { - if (er == null) { - return; - } - jw.name(key); - gson.toJson(er, EvaluationReason.class, jw); // EvaluationReason defines its own custom serializer - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/EventSummarizer.java b/src/main/java/com/launchdarkly/sdk/server/EventSummarizer.java deleted file mode 100644 index 0aaf1276e..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/EventSummarizer.java +++ /dev/null @@ -1,293 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.interfaces.Event; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; - -/** - * Manages the state of summarizable information for the EventProcessor. Note that the - * methods of this class are deliberately not thread-safe, because they should always - * be called from EventProcessor's single message-processing thread. - */ -final class EventSummarizer { - private EventSummary eventsState; - - EventSummarizer() { - this.eventsState = new EventSummary(); - } - - /** - * Adds this event to our counters, if it is a type of event we need to count. - * @param event an event - */ - void summarizeEvent(Event event) { - if (!(event instanceof Event.FeatureRequest)) { - return; - } - Event.FeatureRequest fe = (Event.FeatureRequest)event; - eventsState.incrementCounter(fe.getKey(), fe.getVariation(), fe.getVersion(), fe.getValue(), fe.getDefaultVal()); - eventsState.noteTimestamp(fe.getCreationDate()); - } - - /** - * Gets the current summarized event data, and resets the EventSummarizer's state to contain - * a new empty EventSummary. - * - * @return the summary state - */ - EventSummary getSummaryAndReset() { - EventSummary ret = eventsState; - clear(); - return ret; - } - - /** - * Indicates that we decided not to send the summary values returned by {@link #getSummaryAndReset()}, - * and instead we should return to using the previous state object and keep accumulating data - * in it. - */ - void restoreTo(EventSummary previousState) { - eventsState = previousState; - } - - /** - * Returns true if there is no summary data in the current state. - * - * @return true if the state is empty - */ - boolean isEmpty() { - return eventsState.isEmpty(); - } - - void clear() { - eventsState = new EventSummary(); - } - - static final class EventSummary { - final Map counters; - long startDate; - long endDate; - - EventSummary() { - counters = new HashMap<>(); - } - - EventSummary(EventSummary from) { - counters = new HashMap<>(from.counters); - startDate = from.startDate; - endDate = from.endDate; - } - - boolean isEmpty() { - return counters.isEmpty(); - } - - void incrementCounter(String flagKey, int variation, int version, LDValue flagValue, LDValue defaultVal) { - FlagInfo flagInfo = counters.get(flagKey); - if (flagInfo == null) { - flagInfo = new FlagInfo(defaultVal, new SimpleIntKeyedMap<>()); - counters.put(flagKey, flagInfo); - } - - SimpleIntKeyedMap variations = flagInfo.versionsAndVariations.get(version); - if (variations == null) { - variations = new SimpleIntKeyedMap<>(); - flagInfo.versionsAndVariations.put(version, variations); - } - - CounterValue value = variations.get(variation); - if (value == null) { - variations.put(variation, new CounterValue(1, flagValue)); - } else { - value.increment(); - } - } - - void noteTimestamp(long time) { - if (startDate == 0 || time < startDate) { - startDate = time; - } - if (time > endDate) { - endDate = time; - } - } - - @Override - public boolean equals(Object other) { - if (other instanceof EventSummary) { - EventSummary o = (EventSummary)other; - return o.counters.equals(counters) && startDate == o.startDate && endDate == o.endDate; - } - return false; - } - - @Override - public int hashCode() { - // We can't make meaningful hash codes for EventSummary, because the same counters could be - // represented differently in our Map. It doesn't matter because there's no reason to use an - // EventSummary instance as a hash key. - return 0; - } - } - - static final class FlagInfo { - final LDValue defaultVal; - final SimpleIntKeyedMap> versionsAndVariations; - - FlagInfo(LDValue defaultVal, SimpleIntKeyedMap> versionsAndVariations) { - this.defaultVal = defaultVal; - this.versionsAndVariations = versionsAndVariations; - } - - @Override - public boolean equals(Object other) { - if (other instanceof FlagInfo) { - FlagInfo o = (FlagInfo)other; - return o.defaultVal.equals(this.defaultVal) && o.versionsAndVariations.equals(this.versionsAndVariations); - } - return false; - } - - @Override - public int hashCode() { - return this.defaultVal.hashCode() + 31 * versionsAndVariations.hashCode(); - } - - @Override - public String toString() { // used only in tests - return "(default=" + defaultVal + ", counters=" + versionsAndVariations + ")"; - } - } - - static final class CounterValue { - long count; - final LDValue flagValue; - - CounterValue(long count, LDValue flagValue) { - this.count = count; - this.flagValue = flagValue; - } - - void increment() { - count = count + 1; - } - - @Override - public boolean equals(Object other) - { - if (other instanceof CounterValue) { - CounterValue o = (CounterValue)other; - return count == o.count && Objects.equals(flagValue, o.flagValue); - } - return false; - } - - @Override - public String toString() { // used only in tests - return "(" + count + "," + flagValue + ")"; - } - } - - // A very simple array-backed structure with map-like semantics for primitive int keys. This - // is highly specialized for the EventSummarizer use case (which is why it is an inner class - // of EventSummarizer, to emphasize that it should not be used elsewhere). It makes the - // following assumptions: - // - The number of keys will almost always be small: most flags have only a few variations, - // and most flags will have only one version or a few versions during the lifetime of an - // event payload. Therefore, we use simple iteration and int comparisons for the keys; the - // overhead of this is likely less than the overhead of maintaining a hashtable and creating - // objects for its keys and iterators. - // - Data will never be deleted from the map after being added (the summarizer simply makes - // a new map when it's time to start over). - static final class SimpleIntKeyedMap { - private static final int INITIAL_CAPACITY = 4; - - private int[] keys; - private Object[] values; - private int n; - - SimpleIntKeyedMap() { - keys = new int[INITIAL_CAPACITY]; - values = new Object[INITIAL_CAPACITY]; - } - - int size() { - return n; - } - - int capacity() { - return keys.length; - } - - int keyAt(int index) { - return keys[index]; - } - - @SuppressWarnings("unchecked") - T valueAt(int index) { - return (T)values[index]; - } - - @SuppressWarnings("unchecked") - T get(int key) { - for (int i = 0; i < n; i++) { - if (keys[i] == key) { - return (T)values[i]; - } - } - return null; - } - - SimpleIntKeyedMap put(int key, T value) { - for (int i = 0; i < n; i++) { - if (keys[i] == key) { - values[i] = value; - return this; - } - } - if (n == keys.length) { - int[] newKeys = new int[keys.length * 2]; - System.arraycopy(keys, 0, newKeys, 0, n); - Object[] newValues = new Object[keys.length * 2]; - System.arraycopy(values, 0, newValues, 0, n); - keys = newKeys; - values = newValues; - } - keys[n] = key; - values[n] = value; - n++; - return this; - } - - @SuppressWarnings("unchecked") - @Override - public boolean equals(Object o) { // used only in tests - if (o instanceof SimpleIntKeyedMap) { - SimpleIntKeyedMap other = (SimpleIntKeyedMap)o; - if (this.n == other.n) { - for (int i = 0; i < n; i++) { - T value1 = (T)values[i], value2 = other.get(keys[i]); - if (!Objects.equals(value1, value2)) { - return false; - } - } - return true; - } - } - return false; - } - - @Override - public String toString() { // used only in tests - StringBuilder s = new StringBuilder("{"); - for (int i = 0; i < n; i++) { - s.append(keys[i]).append("=").append(values[i] == null ? "null" : values[i].toString()); - } - s.append("}"); - return s.toString(); - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/EventUserSerialization.java b/src/main/java/com/launchdarkly/sdk/server/EventUserSerialization.java deleted file mode 100644 index ab6b190b0..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/EventUserSerialization.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; - -import java.io.IOException; -import java.util.Set; -import java.util.TreeSet; - -abstract class EventUserSerialization { - private EventUserSerialization() {} - - // Used internally when including users in analytics events, to ensure that private attributes are stripped out. - static class UserAdapterWithPrivateAttributeBehavior extends TypeAdapter { - private static final UserAttribute[] BUILT_IN_OPTIONAL_STRING_ATTRIBUTES = new UserAttribute[] { - UserAttribute.SECONDARY_KEY, - UserAttribute.IP, - UserAttribute.EMAIL, - UserAttribute.NAME, - UserAttribute.AVATAR, - UserAttribute.FIRST_NAME, - UserAttribute.LAST_NAME, - UserAttribute.COUNTRY - }; - - private final EventsConfiguration config; - - public UserAdapterWithPrivateAttributeBehavior(EventsConfiguration config) { - this.config = config; - } - - @Override - public void write(JsonWriter out, LDUser user) throws IOException { - if (user == null) { - out.value((String)null); - return; - } - - // Collect the private attribute names (use TreeSet to make ordering predictable for tests) - Set privateAttributeNames = new TreeSet(); - - out.beginObject(); - // The key can never be private - out.name("key").value(user.getKey()); - - for (UserAttribute attr: BUILT_IN_OPTIONAL_STRING_ATTRIBUTES) { - LDValue value = user.getAttribute(attr); - if (!value.isNull()) { - if (!checkAndAddPrivate(attr, user, privateAttributeNames)) { - out.name(attr.getName()).value(value.stringValue()); - } - } - } - if (!user.getAttribute(UserAttribute.ANONYMOUS).isNull()) { - out.name("anonymous").value(user.isAnonymous()); - } - writeCustomAttrs(out, user, privateAttributeNames); - writePrivateAttrNames(out, privateAttributeNames); - - out.endObject(); - } - - private void writePrivateAttrNames(JsonWriter out, Set names) throws IOException { - if (names.isEmpty()) { - return; - } - out.name("privateAttrs"); - out.beginArray(); - for (String name : names) { - out.value(name); - } - out.endArray(); - } - - private boolean checkAndAddPrivate(UserAttribute attribute, LDUser user, Set privateAttrs) { - boolean result = config.allAttributesPrivate || config.privateAttributes.contains(attribute) || user.isAttributePrivate(attribute); - if (result) { - privateAttrs.add(attribute.getName()); - } - return result; - } - - private void writeCustomAttrs(JsonWriter out, LDUser user, Set privateAttributeNames) throws IOException { - boolean beganObject = false; - for (UserAttribute attribute: user.getCustomAttributes()) { - if (!checkAndAddPrivate(attribute, user, privateAttributeNames)) { - if (!beganObject) { - out.name("custom"); - out.beginObject(); - beganObject = true; - } - out.name(attribute.getName()); - LDValue value = user.getAttribute(attribute); - JsonHelpers.gsonInstance().toJson(value, LDValue.class, out); - } - } - if (beganObject) { - out.endObject(); - } - } - - @Override - public LDUser read(JsonReader in) throws IOException { - // We never need to unmarshal user objects, so there's no need to implement this - return null; - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/EventsConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/EventsConfiguration.java deleted file mode 100644 index 5e64742b7..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/EventsConfiguration.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableSet; -import com.launchdarkly.sdk.UserAttribute; -import com.launchdarkly.sdk.server.interfaces.EventSender; - -import java.net.URI; -import java.time.Duration; -import java.util.Set; - -// Used internally to encapsulate the various config/builder properties for events. -final class EventsConfiguration { - final boolean allAttributesPrivate; - final int capacity; - final EventSender eventSender; - final URI eventsUri; - final Duration flushInterval; - final boolean inlineUsersInEvents; - final ImmutableSet privateAttributes; - final int userKeysCapacity; - final Duration userKeysFlushInterval; - final Duration diagnosticRecordingInterval; - - EventsConfiguration( - boolean allAttributesPrivate, - int capacity, - EventSender eventSender, - URI eventsUri, - Duration flushInterval, - boolean inlineUsersInEvents, - Set privateAttributes, - int userKeysCapacity, - Duration userKeysFlushInterval, - Duration diagnosticRecordingInterval - ) { - super(); - this.allAttributesPrivate = allAttributesPrivate; - this.capacity = capacity; - this.eventSender = eventSender; - this.eventsUri = eventsUri; - this.flushInterval = flushInterval; - this.inlineUsersInEvents = inlineUsersInEvents; - this.privateAttributes = privateAttributes == null ? ImmutableSet.of() : ImmutableSet.copyOf(privateAttributes); - this.userKeysCapacity = userKeysCapacity; - this.userKeysFlushInterval = userKeysFlushInterval; - this.diagnosticRecordingInterval = diagnosticRecordingInterval; - } -} \ No newline at end of file diff --git a/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java b/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java index 071ee5ee0..e48f03769 100644 --- a/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java +++ b/src/main/java/com/launchdarkly/sdk/server/FeatureFlagsState.java @@ -20,7 +20,7 @@ /** * A snapshot of the state of all feature flags with regard to a specific user, generated by - * calling {@link LDClientInterface#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}. + * calling {@link LDClientInterface#allFlagsState(com.launchdarkly.sdk.LDContext, FlagsStateOption...)}. *

* LaunchDarkly defines a standard JSON encoding for this object, suitable for * bootstrapping @@ -91,10 +91,10 @@ private FeatureFlagsState(ImmutableMap flagMetadata, boole *

* Application code will not normally use this builder, since the SDK creates its own instances. * However, it may be useful in testing, to simulate values that might be returned by - * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}. + * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDContext, FlagsStateOption...)}. * * @param options the same {@link FlagsStateOption}s, if any, that would be passed to - * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)} + * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDContext, FlagsStateOption...)} * @return a builder object * @since 5.6.0 */ @@ -166,7 +166,7 @@ public int hashCode() { *

* Application code will not normally use this builder, since the SDK creates its own instances. * However, it may be useful in testing, to simulate values that might be returned by - * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}. + * {@link LDClient#allFlagsState(com.launchdarkly.sdk.LDContext, FlagsStateOption...)}. * * @since 5.6.0 */ diff --git a/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java b/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java index 64a012eec..71c79c3b2 100644 --- a/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java +++ b/src/main/java/com/launchdarkly/sdk/server/FeatureRequestor.java @@ -1,7 +1,8 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.internal.http.HttpErrors.HttpErrorException; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import java.io.Closeable; import java.io.IOException; diff --git a/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java b/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java index a8f5e1467..3f9fb1aef 100644 --- a/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/FlagTrackerImpl.java @@ -1,6 +1,6 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; @@ -13,11 +13,11 @@ final class FlagTrackerImpl implements FlagTracker { private final EventBroadcasterImpl flagChangeBroadcaster; - private final BiFunction evaluateFn; + private final BiFunction evaluateFn; FlagTrackerImpl( EventBroadcasterImpl flagChangeBroadcaster, - BiFunction evaluateFn + BiFunction evaluateFn ) { this.flagChangeBroadcaster = flagChangeBroadcaster; this.evaluateFn = evaluateFn; @@ -34,29 +34,29 @@ public void removeFlagChangeListener(FlagChangeListener listener) { } @Override - public FlagChangeListener addFlagValueChangeListener(String flagKey, LDUser user, FlagValueChangeListener listener) { - FlagValueChangeAdapter adapter = new FlagValueChangeAdapter(flagKey, user, listener); + public FlagChangeListener addFlagValueChangeListener(String flagKey, LDContext context, FlagValueChangeListener listener) { + FlagValueChangeAdapter adapter = new FlagValueChangeAdapter(flagKey, context, listener); addFlagChangeListener(adapter); return adapter; } private final class FlagValueChangeAdapter implements FlagChangeListener { private final String flagKey; - private final LDUser user; + private final LDContext context; private final FlagValueChangeListener listener; private final AtomicReference value; - FlagValueChangeAdapter(String flagKey, LDUser user, FlagValueChangeListener listener) { + FlagValueChangeAdapter(String flagKey, LDContext context, FlagValueChangeListener listener) { this.flagKey = flagKey; - this.user = user; + this.context = context; this.listener = listener; - this.value = new AtomicReference<>(evaluateFn.apply(flagKey, user)); + this.value = new AtomicReference<>(evaluateFn.apply(flagKey, context)); } @Override public void onFlagChange(FlagChangeEvent event) { if (event.getKey().equals(flagKey)) { - LDValue newValue = evaluateFn.apply(flagKey, user); + LDValue newValue = evaluateFn.apply(flagKey, context); LDValue oldValue = value.getAndSet(newValue); if (!newValue.equals(oldValue)) { listener.onFlagValueChange(new FlagValueChangeEvent(flagKey, oldValue, newValue)); diff --git a/src/main/java/com/launchdarkly/sdk/server/FlagsStateOption.java b/src/main/java/com/launchdarkly/sdk/server/FlagsStateOption.java index bbb9f1d51..8204ba9fc 100644 --- a/src/main/java/com/launchdarkly/sdk/server/FlagsStateOption.java +++ b/src/main/java/com/launchdarkly/sdk/server/FlagsStateOption.java @@ -4,7 +4,7 @@ import com.launchdarkly.sdk.server.interfaces.LDClientInterface; /** - * Optional parameters that can be passed to {@link LDClientInterface#allFlagsState(com.launchdarkly.sdk.LDUser, FlagsStateOption...)}. + * Optional parameters that can be passed to {@link LDClientInterface#allFlagsState(com.launchdarkly.sdk.LDContext, FlagsStateOption...)}. * @since 4.3.0 */ public final class FlagsStateOption { diff --git a/src/main/java/com/launchdarkly/sdk/server/HttpConfigurationImpl.java b/src/main/java/com/launchdarkly/sdk/server/HttpConfigurationImpl.java deleted file mode 100644 index 9415fe8b1..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/HttpConfigurationImpl.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableMap; -import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; - -import java.net.Proxy; -import java.time.Duration; -import java.util.Map; - -import javax.net.SocketFactory; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -final class HttpConfigurationImpl implements HttpConfiguration { - final Duration connectTimeout; - final Proxy proxy; - final HttpAuthentication proxyAuth; - final Duration socketTimeout; - final SocketFactory socketFactory; - final SSLSocketFactory sslSocketFactory; - final X509TrustManager trustManager; - final ImmutableMap defaultHeaders; - - HttpConfigurationImpl(Duration connectTimeout, Proxy proxy, HttpAuthentication proxyAuth, - Duration socketTimeout, SocketFactory socketFactory, - SSLSocketFactory sslSocketFactory, X509TrustManager trustManager, - ImmutableMap defaultHeaders) { - this.connectTimeout = connectTimeout; - this.proxy = proxy; - this.proxyAuth = proxyAuth; - this.socketTimeout = socketTimeout; - this.socketFactory = socketFactory; - this.sslSocketFactory = sslSocketFactory; - this.trustManager = trustManager; - this.defaultHeaders = defaultHeaders; - } - - @Override - public Duration getConnectTimeout() { - return connectTimeout; - } - - @Override - public Proxy getProxy() { - return proxy; - } - - @Override - public HttpAuthentication getProxyAuthentication() { - return proxyAuth; - } - - @Override - public Duration getSocketTimeout() { - return socketTimeout; - } - - @Override - public SocketFactory getSocketFactory() { - return socketFactory; - } - - @Override - public SSLSocketFactory getSslSocketFactory() { - return sslSocketFactory; - } - - @Override - public X509TrustManager getTrustManager() { - return trustManager; - } - - @Override - public Iterable> getDefaultHeaders() { - return defaultHeaders.entrySet(); - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/HttpErrorException.java b/src/main/java/com/launchdarkly/sdk/server/HttpErrorException.java deleted file mode 100644 index 30b10f3ff..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/HttpErrorException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.launchdarkly.sdk.server; - -@SuppressWarnings("serial") -final class HttpErrorException extends Exception { - private final int status; - - public HttpErrorException(int status) { - super("HTTP error " + status); - this.status = status; - } - - public int getStatus() { - return status; - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java b/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java index e1ab782d0..47530843f 100644 --- a/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java +++ b/src/main/java/com/launchdarkly/sdk/server/InMemoryDataStore.java @@ -2,12 +2,12 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import java.io.IOException; import java.util.HashMap; diff --git a/src/main/java/com/launchdarkly/sdk/server/JsonHelpers.java b/src/main/java/com/launchdarkly/sdk/server/JsonHelpers.java index 73885d3c7..155544415 100644 --- a/src/main/java/com/launchdarkly/sdk/server/JsonHelpers.java +++ b/src/main/java/com/launchdarkly/sdk/server/JsonHelpers.java @@ -7,8 +7,7 @@ import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import java.io.IOException; @@ -41,15 +40,6 @@ static Gson gsonInstanceWithNullsAllowed() { return gsonWithNullsAllowed; } - /** - * Creates a Gson instance that will correctly serialize users for the given configuration (private attributes, etc.). - */ - static Gson gsonInstanceForEventsSerialization(EventsConfiguration config) { - return new GsonBuilder() - .registerTypeAdapter(LDUser.class, new EventUserSerialization.UserAdapterWithPrivateAttributeBehavior(config)) - .create(); - } - /** * Deserializes an object from JSON. We should use this helper method instead of directly calling * gson.fromJson() to minimize reliance on details of the framework we're using, and to ensure that we diff --git a/src/main/java/com/launchdarkly/sdk/server/LDClient.java b/src/main/java/com/launchdarkly/sdk/server/LDClient.java index db7a2bba0..77674abe3 100644 --- a/src/main/java/com/launchdarkly/sdk/server/LDClient.java +++ b/src/main/java/com/launchdarkly/sdk/server/LDClient.java @@ -5,26 +5,25 @@ import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.LDValueType; +import com.launchdarkly.sdk.internal.http.HttpHelpers; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; -import com.launchdarkly.sdk.server.interfaces.DataSource; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventProcessor; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; import com.launchdarkly.sdk.server.interfaces.FlagTracker; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; import org.apache.commons.codec.binary.Hex; @@ -47,7 +46,7 @@ import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; import static com.launchdarkly.sdk.server.DataModel.FEATURES; import static com.launchdarkly.sdk.server.DataModel.SEGMENTS; -import static com.launchdarkly.sdk.server.Util.isAsciiHeaderValue; +import static com.launchdarkly.sdk.server.subsystems.EventProcessor.NO_VERSION; /** * A client for the LaunchDarkly API. Client instances are thread-safe. Applications should instantiate @@ -64,14 +63,12 @@ public final class LDClient implements LDClientInterface { final DataStore dataStore; private final BigSegmentStoreStatusProvider bigSegmentStoreStatusProvider; private final BigSegmentStoreWrapper bigSegmentStoreWrapper; - private final DataSourceUpdates dataSourceUpdates; + private final DataSourceUpdateSink dataSourceUpdates; private final DataStoreStatusProviderImpl dataStoreStatusProvider; private final DataSourceStatusProviderImpl dataSourceStatusProvider; private final FlagTrackerImpl flagTracker; private final EventBroadcasterImpl flagChangeBroadcaster; private final ScheduledExecutorService sharedExecutor; - private final EventFactory eventFactoryDefault; - private final EventFactory eventFactoryWithReasons; private final LDLogger baseLogger; private final LDLogger evaluationLogger; private final Evaluator.PrerequisiteEvaluationSink prereqEvalsDefault; @@ -179,39 +176,26 @@ private static final DataModel.Segment getSegment(DataStore store, String key) { public LDClient(String sdkKey, LDConfig config) { checkNotNull(config, "config must not be null"); this.sdkKey = checkNotNull(sdkKey, "sdkKey must not be null"); - if (!isAsciiHeaderValue(sdkKey) ) { + if (!HttpHelpers.isAsciiHeaderValue(sdkKey) ) { throw new IllegalArgumentException("SDK key contained an invalid character"); } this.offline = config.offline; this.sharedExecutor = createSharedExecutor(config); - boolean eventsDisabled = Components.isNullImplementation(config.eventProcessorFactory); - if (eventsDisabled) { - this.eventFactoryDefault = EventFactory.Disabled.INSTANCE; - this.eventFactoryWithReasons = EventFactory.Disabled.INSTANCE; - } else { - this.eventFactoryDefault = EventFactory.DEFAULT; - this.eventFactoryWithReasons = EventFactory.DEFAULT_WITH_REASONS; - } - - // Do not create diagnostic accumulator if config has specified is opted out, or if we're not using the - // standard event processor - final boolean useDiagnostics = !config.diagnosticOptOut && config.eventProcessorFactory instanceof EventProcessorBuilder; - final ClientContextImpl context = new ClientContextImpl( + final ClientContextImpl context = ClientContextImpl.fromConfig( sdkKey, config, - sharedExecutor, - useDiagnostics ? new DiagnosticAccumulator(new DiagnosticId(sdkKey)) : null + sharedExecutor ); - this.baseLogger = context.getBasic().getBaseLogger(); + this.baseLogger = context.getBaseLogger(); this.evaluationLogger = this.baseLogger.subLogger(Loggers.EVALUATION_LOGGER_NAME); - this.eventProcessor = config.eventProcessorFactory.createEventProcessor(context); + this.eventProcessor = config.events.build(context); EventBroadcasterImpl bigSegmentStoreStatusNotifier = EventBroadcasterImpl.forBigSegmentStoreStatus(sharedExecutor, baseLogger); - BigSegmentsConfiguration bigSegmentsConfig = config.bigSegmentsConfigBuilder.createBigSegmentsConfiguration(context); + BigSegmentsConfiguration bigSegmentsConfig = config.bigSegments.build(context); if (bigSegmentsConfig.getStore() != null) { bigSegmentStoreWrapper = new BigSegmentStoreWrapper(bigSegmentsConfig, bigSegmentStoreStatusNotifier, sharedExecutor, this.baseLogger.subLogger(Loggers.BIG_SEGMENTS_LOGGER_NAME)); @@ -223,7 +207,7 @@ public LDClient(String sdkKey, LDConfig config) { EventBroadcasterImpl dataStoreStatusNotifier = EventBroadcasterImpl.forDataStoreStatus(sharedExecutor, baseLogger); DataStoreUpdatesImpl dataStoreUpdates = new DataStoreUpdatesImpl(dataStoreStatusNotifier); - this.dataStore = config.dataStoreFactory.createDataStore(context, dataStoreUpdates); + this.dataStore = config.dataStore.build(context.withDataStoreUpdateSink(dataStoreUpdates)); this.evaluator = new Evaluator(new Evaluator.Getters() { public DataModel.FeatureFlag getFlag(String key) { @@ -242,7 +226,7 @@ public BigSegmentStoreWrapper.BigSegmentsQueryResult getBigSegments(String key) this.flagChangeBroadcaster = EventBroadcasterImpl.forFlagChangeEvents(sharedExecutor, baseLogger); this.flagTracker = new FlagTrackerImpl(flagChangeBroadcaster, - (key, user) -> jsonValueVariation(key, user, LDValue.ofNull())); + (key, ctx) -> jsonValueVariation(key, ctx, LDValue.ofNull())); this.dataStoreStatusProvider = new DataStoreStatusProviderImpl(this.dataStore, dataStoreUpdates); @@ -258,7 +242,7 @@ public BigSegmentStoreWrapper.BigSegmentsQueryResult getBigSegments(String key) baseLogger ); this.dataSourceUpdates = dataSourceUpdates; - this.dataSource = config.dataSourceFactory.createDataSource(context, dataSourceUpdates); + this.dataSource = config.dataSource.build(context.withDataSourceUpdateSink(dataSourceUpdates)); this.dataSourceStatusProvider = new DataSourceStatusProviderImpl(dataSourceStatusNotifier, dataSourceUpdates); this.prereqEvalsDefault = makePrerequisiteEventSender(false); @@ -293,45 +277,45 @@ public boolean isInitialized() { } @Override - public void track(String eventName, LDUser user) { - trackData(eventName, user, LDValue.ofNull()); + public void track(String eventName, LDContext context) { + trackData(eventName, context, LDValue.ofNull()); } @Override - public void trackData(String eventName, LDUser user, LDValue data) { - if (user == null || user.getKey() == null || user.getKey().isEmpty()) { - baseLogger.warn("Track called with null user or null/empty user key!"); + public void trackData(String eventName, LDContext context, LDValue data) { + if (context == null) { + baseLogger.warn("Track called with null context!"); + } else if (!context.isValid()) { + baseLogger.warn("Track called with invalid context: " + context.getError()); } else { - eventProcessor.sendEvent(eventFactoryDefault.newCustomEvent(eventName, user, data, null)); + eventProcessor.recordCustomEvent(context, eventName, data, null); } } @Override - public void trackMetric(String eventName, LDUser user, LDValue data, double metricValue) { - if (user == null || user.getKey() == null || user.getKey().isEmpty()) { - baseLogger.warn("Track called with null user or null/empty user key!"); + public void trackMetric(String eventName, LDContext context, LDValue data, double metricValue) { + if (context == null) { + baseLogger.warn("Track called with null context!"); + } else if (!context.isValid()) { + baseLogger.warn("Track called with invalid context: " + context.getError()); } else { - eventProcessor.sendEvent(eventFactoryDefault.newCustomEvent(eventName, user, data, metricValue)); + eventProcessor.recordCustomEvent(context, eventName, data, metricValue); } } @Override - public void identify(LDUser user) { - if (user == null || user.getKey() == null || user.getKey().isEmpty()) { - baseLogger.warn("Identify called with null user or null/empty user key!"); + public void identify(LDContext context) { + if (context == null) { + baseLogger.warn("Identify called with null context!"); + } else if (!context.isValid()) { + baseLogger.warn("Identify called with invalid context: " + context.getError()); } else { - eventProcessor.sendEvent(eventFactoryDefault.newIdentifyEvent(user)); + eventProcessor.recordIdentifyEvent(context); } } - private void sendFlagRequestEvent(Event.FeatureRequest event) { - if (event != null) { - eventProcessor.sendEvent(event); - } - } - @Override - public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) { + public FeatureFlagsState allFlagsState(LDContext context, FlagsStateOption... options) { FeatureFlagsState.Builder builder = FeatureFlagsState.builder(options); if (isOffline()) { @@ -347,11 +331,15 @@ public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) } } - if (user == null || user.getKey() == null) { - evaluationLogger.warn("allFlagsState() was called with null user or null user key! returning no data"); + if (context == null) { + evaluationLogger.warn("allFlagsState() was called with null context! returning no data"); return builder.valid(false).build(); } - + if (!context.isValid()) { + evaluationLogger.warn("allFlagsState() was called with invalid context: " + context.getError()); + return builder.valid(false).build(); + } + boolean clientSideOnly = FlagsStateOption.hasOption(options, FlagsStateOption.CLIENT_SIDE_ONLY); KeyedItems flags; try { @@ -371,7 +359,7 @@ public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) continue; } try { - EvalResult result = evaluator.evaluate(flag, user, null); + EvalResult result = evaluator.evaluate(flag, context, null); // Note: the null parameter to evaluate() is for the PrerequisiteEvaluationSink; allFlagsState should // not generate evaluation events, so we don't want the evaluator to generate any prerequisite evaluation // events either. @@ -385,63 +373,63 @@ public FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) } return builder.build(); } - + @Override - public boolean boolVariation(String featureKey, LDUser user, boolean defaultValue) { - return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.BOOLEAN).booleanValue(); + public boolean boolVariation(String featureKey, LDContext context, boolean defaultValue) { + return evaluate(featureKey, context, LDValue.of(defaultValue), LDValueType.BOOLEAN).booleanValue(); } @Override - public int intVariation(String featureKey, LDUser user, int defaultValue) { - return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.NUMBER).intValue(); + public int intVariation(String featureKey, LDContext context, int defaultValue) { + return evaluate(featureKey, context, LDValue.of(defaultValue), LDValueType.NUMBER).intValue(); } @Override - public double doubleVariation(String featureKey, LDUser user, double defaultValue) { - return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.NUMBER).doubleValue(); + public double doubleVariation(String featureKey, LDContext context, double defaultValue) { + return evaluate(featureKey, context, LDValue.of(defaultValue), LDValueType.NUMBER).doubleValue(); } @Override - public String stringVariation(String featureKey, LDUser user, String defaultValue) { - return evaluate(featureKey, user, LDValue.of(defaultValue), LDValueType.STRING).stringValue(); + public String stringVariation(String featureKey, LDContext context, String defaultValue) { + return evaluate(featureKey, context, LDValue.of(defaultValue), LDValueType.STRING).stringValue(); } @Override - public LDValue jsonValueVariation(String featureKey, LDUser user, LDValue defaultValue) { - return evaluate(featureKey, user, LDValue.normalize(defaultValue), null); + public LDValue jsonValueVariation(String featureKey, LDContext context, LDValue defaultValue) { + return evaluate(featureKey, context, LDValue.normalize(defaultValue), null); } @Override - public EvaluationDetail boolVariationDetail(String featureKey, LDUser user, boolean defaultValue) { - EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + public EvaluationDetail boolVariationDetail(String featureKey, LDContext context, boolean defaultValue) { + EvalResult result = evaluateInternal(featureKey, context, LDValue.of(defaultValue), LDValueType.BOOLEAN, true); return result.getAsBoolean(); } @Override - public EvaluationDetail intVariationDetail(String featureKey, LDUser user, int defaultValue) { - EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + public EvaluationDetail intVariationDetail(String featureKey, LDContext context, int defaultValue) { + EvalResult result = evaluateInternal(featureKey, context, LDValue.of(defaultValue), LDValueType.NUMBER, true); return result.getAsInteger(); } @Override - public EvaluationDetail doubleVariationDetail(String featureKey, LDUser user, double defaultValue) { - EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + public EvaluationDetail doubleVariationDetail(String featureKey, LDContext context, double defaultValue) { + EvalResult result = evaluateInternal(featureKey, context, LDValue.of(defaultValue), LDValueType.NUMBER, true); return result.getAsDouble(); } @Override - public EvaluationDetail stringVariationDetail(String featureKey, LDUser user, String defaultValue) { - EvalResult result = evaluateInternal(featureKey, user, LDValue.of(defaultValue), + public EvaluationDetail stringVariationDetail(String featureKey, LDContext context, String defaultValue) { + EvalResult result = evaluateInternal(featureKey, context, LDValue.of(defaultValue), LDValueType.STRING, true); return result.getAsString(); } @Override - public EvaluationDetail jsonValueVariationDetail(String featureKey, LDUser user, LDValue defaultValue) { - EvalResult result = evaluateInternal(featureKey, user, LDValue.normalize(defaultValue), + public EvaluationDetail jsonValueVariationDetail(String featureKey, LDContext context, LDValue defaultValue) { + EvalResult result = evaluateInternal(featureKey, context, LDValue.normalize(defaultValue), null, true); return result.getAnyType(); } @@ -470,47 +458,46 @@ public boolean isFlagKnown(String featureKey) { return false; } - private LDValue evaluate(String featureKey, LDUser user, LDValue defaultValue, LDValueType requireType) { - return evaluateInternal(featureKey, user, defaultValue, requireType, false).getValue(); + private LDValue evaluate(String featureKey, LDContext context, LDValue defaultValue, LDValueType requireType) { + return evaluateInternal(featureKey, context, defaultValue, requireType, false).getValue(); } private EvalResult errorResult(EvaluationReason.ErrorKind errorKind, final LDValue defaultValue) { return EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.error(errorKind)); } - private EvalResult evaluateInternal(String featureKey, LDUser user, LDValue defaultValue, + private EvalResult evaluateInternal(String featureKey, LDContext context, LDValue defaultValue, LDValueType requireType, boolean withDetail) { - EventFactory eventFactory = withDetail ? eventFactoryWithReasons : eventFactoryDefault; if (!isInitialized()) { if (dataStore.isInitialized()) { evaluationLogger.warn("Evaluation called before client initialized for feature flag \"{}\"; using last known values from data store", featureKey); } else { evaluationLogger.warn("Evaluation called before client initialized for feature flag \"{}\"; data store unavailable, returning default value", featureKey); - sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue, - EvaluationReason.ErrorKind.CLIENT_NOT_READY)); + recordEvaluationUnknownFlagErrorEvent(featureKey, context, defaultValue, + EvaluationReason.ErrorKind.CLIENT_NOT_READY, withDetail); return errorResult(EvaluationReason.ErrorKind.CLIENT_NOT_READY, defaultValue); } } + if (context == null) { + evaluationLogger.warn("Null context when evaluating flag \"{}\"; returning default value", featureKey); + return errorResult(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, defaultValue); + } + if (!context.isValid()) { + evaluationLogger.warn("Invalid context when evaluating flag \"{}\"; returning default value: " + context.getError(), featureKey); + return errorResult(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, defaultValue); + } + DataModel.FeatureFlag featureFlag = null; try { featureFlag = getFlag(dataStore, featureKey); if (featureFlag == null) { evaluationLogger.info("Unknown feature flag \"{}\"; returning default value", featureKey); - sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue, - EvaluationReason.ErrorKind.FLAG_NOT_FOUND)); + recordEvaluationUnknownFlagErrorEvent(featureKey, context, defaultValue, + EvaluationReason.ErrorKind.FLAG_NOT_FOUND, withDetail); return errorResult(EvaluationReason.ErrorKind.FLAG_NOT_FOUND, defaultValue); } - if (user == null || user.getKey() == null) { - evaluationLogger.warn("Null user or null user key when evaluating flag \"{}\"; returning default value", featureKey); - sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue, - EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)); - return errorResult(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED, defaultValue); - } - if (user.getKey().isEmpty()) { - evaluationLogger.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly"); - } - EvalResult evalResult = evaluator.evaluate(featureFlag, user, + EvalResult evalResult = evaluator.evaluate(featureFlag, context, withDetail ? prereqEvalsWithReasons : prereqEvalsDefault); if (evalResult.isNoVariation()) { evalResult = EvalResult.of(defaultValue, evalResult.getVariationIndex(), evalResult.getReason()); @@ -520,23 +507,22 @@ private EvalResult evaluateInternal(String featureKey, LDUser user, LDValue defa !value.isNull() && value.getType() != requireType) { evaluationLogger.error("Feature flag evaluation expected result as {}, but got {}", defaultValue.getType(), value.getType()); - sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue, - EvaluationReason.ErrorKind.WRONG_TYPE)); + recordEvaluationErrorEvent(featureFlag, context, defaultValue, EvaluationReason.ErrorKind.WRONG_TYPE, withDetail); return errorResult(EvaluationReason.ErrorKind.WRONG_TYPE, defaultValue); } } - sendFlagRequestEvent(eventFactory.newFeatureRequestEvent(featureFlag, user, evalResult, defaultValue)); + recordEvaluationEvent(featureFlag, context, evalResult, defaultValue, withDetail, null); return evalResult; } catch (Exception e) { evaluationLogger.error("Encountered exception while evaluating feature flag \"{}\": {}", featureKey, LogValues.exceptionSummary(e)); evaluationLogger.debug("{}", LogValues.exceptionTrace(e)); if (featureFlag == null) { - sendFlagRequestEvent(eventFactory.newUnknownFeatureRequestEvent(featureKey, user, defaultValue, - EvaluationReason.ErrorKind.EXCEPTION)); + recordEvaluationUnknownFlagErrorEvent(featureKey, context, defaultValue, + EvaluationReason.ErrorKind.EXCEPTION, withDetail); } else { - sendFlagRequestEvent(eventFactory.newDefaultFeatureRequestEvent(featureFlag, user, defaultValue, - EvaluationReason.ErrorKind.EXCEPTION)); + recordEvaluationErrorEvent(featureFlag, context, defaultValue, + EvaluationReason.ErrorKind.EXCEPTION, withDetail); } return EvalResult.of(defaultValue, NO_VARIATION, EvaluationReason.exception(e)); } @@ -562,6 +548,12 @@ public DataSourceStatusProvider getDataSourceStatusProvider() { return dataSourceStatusProvider; } + /** + * Shuts down the client and releases any resources it is using. + *

+ * Unless it is offline, the client will attempt to deliver any pending analytics events before + * closing. + */ @Override public void close() throws IOException { baseLogger.info("Closing LaunchDarkly Client"); @@ -586,14 +578,14 @@ public boolean isOffline() { } @Override - public String secureModeHash(LDUser user) { - if (user == null || user.getKey() == null) { + public String secureModeHash(LDContext context) { + if (context == null || !context.isValid()) { return null; } try { Mac mac = Mac.getInstance(HMAC_ALGORITHM); mac.init(new SecretKeySpec(sdkKey.getBytes(), HMAC_ALGORITHM)); - return Hex.encodeHexString(mac.doFinal(user.getKey().getBytes("UTF8"))); + return Hex.encodeHexString(mac.doFinal(context.getFullyQualifiedKey().getBytes("UTF8"))); } catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException e) { // COVERAGE: there is no way to cause these errors in a unit test. baseLogger.error("Could not generate secure mode hash: {}", LogValues.exceptionSummary(e)); @@ -602,11 +594,6 @@ public String secureModeHash(LDUser user) { return null; } - @Override - public void alias(LDUser user, LDUser previousUser) { - this.eventProcessor.sendEvent(eventFactoryDefault.newAliasEvent(user, previousUser)); - } - /** * Returns the current version string of the client library. * @return a version string conforming to Semantic Versioning (http://semver.org) @@ -616,6 +603,79 @@ public String version() { return Version.SDK_VERSION; } + private void recordEvaluationUnknownFlagErrorEvent( + String flagKey, + LDContext context, + LDValue defaultValue, + EvaluationReason.ErrorKind errorKind, + boolean withReasons + ) { + eventProcessor.recordEvaluationEvent( + context, + flagKey, + NO_VERSION, + NO_VARIATION, + defaultValue, + withReasons ? EvaluationReason.error(errorKind) : null, + defaultValue, + null, + false, + null + ); + } + + private void recordEvaluationErrorEvent( + FeatureFlag flag, + LDContext context, + LDValue defaultValue, + EvaluationReason.ErrorKind errorKind, + boolean withReasons + ) { + eventProcessor.recordEvaluationEvent( + context, + flag.getKey(), + flag.getVersion(), + NO_VARIATION, + defaultValue, + withReasons ? EvaluationReason.error(errorKind) : null, + defaultValue, + null, + flag.isTrackEvents(), + flag.getDebugEventsUntilDate() + ); + } + + private void recordEvaluationEvent( + FeatureFlag flag, + LDContext context, + EvalResult result, + LDValue defaultValue, + boolean withReasons, + String prereqOf + ) { + eventProcessor.recordEvaluationEvent( + context, + flag.getKey(), + flag.getVersion(), + result.getVariationIndex(), + result.getValue(), + (withReasons || result.isForceReasonTracking()) ? result.getReason() : null, + defaultValue, + prereqOf, + flag.isTrackEvents() || result.isForceReasonTracking(), + flag.getDebugEventsUntilDate() + ); + } + + private Evaluator.PrerequisiteEvaluationSink makePrerequisiteEventSender(boolean withReasons) { + return new Evaluator.PrerequisiteEvaluationSink() { + @Override + public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDContext context, EvalResult result) { + recordEvaluationEvent(flag, context, result, LDValue.ofNull(), withReasons, prereqOfFlag.getKey()); + } + }; + } + // This executor is used for a variety of SDK tasks such as flag change events, checking the data store // status after an outage, and the poll task in polling mode. These are all tasks that we do not expect // to be executing frequently so that it is acceptable to use a single thread to execute them one at a @@ -629,15 +689,4 @@ private ScheduledExecutorService createSharedExecutor(LDConfig config) { .build(); return Executors.newSingleThreadScheduledExecutor(threadFactory); } - - private Evaluator.PrerequisiteEvaluationSink makePrerequisiteEventSender(boolean withReasons) { - final EventFactory factory = withReasons ? eventFactoryWithReasons : eventFactoryDefault; - return new Evaluator.PrerequisiteEvaluationSink() { - @Override - public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDUser user, EvalResult result) { - eventProcessor.sendEvent( - factory.newPrerequisiteFeatureRequestEvent(flag, user, result, prereqOfFlag)); - } - }; - } } diff --git a/src/main/java/com/launchdarkly/sdk/server/LDConfig.java b/src/main/java/com/launchdarkly/sdk/server/LDConfig.java index 17a003f97..a10e894d1 100644 --- a/src/main/java/com/launchdarkly/sdk/server/LDConfig.java +++ b/src/main/java/com/launchdarkly/sdk/server/LDConfig.java @@ -1,20 +1,19 @@ package com.launchdarkly.sdk.server; import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; import com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder; -import com.launchdarkly.sdk.server.integrations.BigSegmentsConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder; import com.launchdarkly.sdk.server.interfaces.ApplicationInfo; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; -import com.launchdarkly.sdk.server.interfaces.EventProcessor; -import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory; -import com.launchdarkly.sdk.server.interfaces.HttpConfigurationFactory; -import com.launchdarkly.sdk.server.interfaces.LoggingConfigurationFactory; +import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; -import java.net.URI; import java.time.Duration; /** @@ -29,13 +28,13 @@ public final class LDConfig { protected static final LDConfig DEFAULT = new Builder().build(); final ApplicationInfo applicationInfo; - final BigSegmentsConfigurationBuilder bigSegmentsConfigBuilder; - final DataSourceFactory dataSourceFactory; - final DataStoreFactory dataStoreFactory; + final ComponentConfigurer bigSegments; + final ComponentConfigurer dataSource; + final ComponentConfigurer dataStore; final boolean diagnosticOptOut; - final EventProcessorFactory eventProcessorFactory; - final HttpConfigurationFactory httpConfigFactory; - final LoggingConfigurationFactory loggingConfigFactory; + final ComponentConfigurer events; + final ComponentConfigurer http; + final ComponentConfigurer logging; final ServiceEndpoints serviceEndpoints; final boolean offline; final Duration startWait; @@ -43,26 +42,20 @@ public final class LDConfig { protected LDConfig(Builder builder) { if (builder.offline) { - this.dataSourceFactory = Components.externalUpdatesOnly(); - this.eventProcessorFactory = Components.noEvents(); + this.dataSource = Components.externalUpdatesOnly(); + this.events = Components.noEvents(); } else { - this.dataSourceFactory = builder.dataSourceFactory == null ? Components.streamingDataSource() : - builder.dataSourceFactory; - this.eventProcessorFactory = builder.eventProcessorFactory == null ? Components.sendEvents() : - builder.eventProcessorFactory; + this.dataSource = builder.dataSource == null ? Components.streamingDataSource() : builder.dataSource; + this.events = builder.events == null ? Components.sendEvents() : builder.events; } this.applicationInfo = (builder.applicationInfoBuilder == null ? Components.applicationInfo() : builder.applicationInfoBuilder) .createApplicationInfo(); - this.bigSegmentsConfigBuilder = builder.bigSegmentsConfigBuilder == null ? - Components.bigSegments(null) : builder.bigSegmentsConfigBuilder; - this.dataStoreFactory = builder.dataStoreFactory == null ? Components.inMemoryDataStore() : - builder.dataStoreFactory; + this.bigSegments = builder.bigSegments == null ? Components.bigSegments(null) : builder.bigSegments; + this.dataStore = builder.dataStore == null ? Components.inMemoryDataStore() : builder.dataStore; this.diagnosticOptOut = builder.diagnosticOptOut; - this.httpConfigFactory = builder.httpConfigFactory == null ? Components.httpConfiguration() : - builder.httpConfigFactory; - this.loggingConfigFactory = builder.loggingConfigFactory == null ? Components.logging() : - builder.loggingConfigFactory; + this.http = builder.http == null ? Components.httpConfiguration() : builder.http; + this.logging = builder.logging == null ? Components.logging() : builder.logging; this.offline = builder.offline; this.serviceEndpoints = (builder.serviceEndpointsBuilder == null ? Components.serviceEndpoints() : builder.serviceEndpointsBuilder) @@ -84,13 +77,13 @@ protected LDConfig(Builder builder) { */ public static class Builder { private ApplicationInfoBuilder applicationInfoBuilder = null; - private BigSegmentsConfigurationBuilder bigSegmentsConfigBuilder = null; - private DataSourceFactory dataSourceFactory = null; - private DataStoreFactory dataStoreFactory = null; + private ComponentConfigurer bigSegments = null; + private ComponentConfigurer dataSource = null; + private ComponentConfigurer dataStore = null; private boolean diagnosticOptOut = false; - private EventProcessorFactory eventProcessorFactory = null; - private HttpConfigurationFactory httpConfigFactory = null; - private LoggingConfigurationFactory loggingConfigFactory = null; + private ComponentConfigurer events = null; + private ComponentConfigurer http = null; + private ComponentConfigurer logging = null; private ServiceEndpointsBuilder serviceEndpointsBuilder = null; private boolean offline = false; private Duration startWait = DEFAULT_START_WAIT; @@ -132,8 +125,7 @@ public Builder applicationInfo(ApplicationInfoBuilder applicationInfoBuilder) { * By default, there is no implementation and Big Segments cannot be evaluated. In this case, * any flag evaluation that references a Big Segment will behave as if no users are included in * any Big Segments, and the {@link EvaluationReason} associated with any such flag evaluation - * will have a {@link EvaluationReason.BigSegmentsStatus} of - * {@link EvaluationReason.BigSegmentsStatus#NOT_CONFIGURED}. + * will have a {@link BigSegmentsStatus} of {@link BigSegmentsStatus#NOT_CONFIGURED}. * *


      *     // This example uses the Redis integration
@@ -143,13 +135,13 @@ public Builder applicationInfo(ApplicationInfoBuilder applicationInfoBuilder) {
      *         .build();
      * 
* - * @param bigSegmentsConfigBuilder a configuration builder object returned by - * {@link Components#bigSegments(BigSegmentStoreFactory)}. + * @param bigSegmentsConfigurer the Big Segments configuration builder * @return the builder * @since 5.7.0 + * @see Components#bigSegments(ComponentConfigurer) */ - public Builder bigSegments(BigSegmentsConfigurationBuilder bigSegmentsConfigBuilder) { - this.bigSegmentsConfigBuilder = bigSegmentsConfigBuilder; + public Builder bigSegments(ComponentConfigurer bigSegmentsConfigurer) { + this.bigSegments = bigSegmentsConfigurer; return this; } @@ -163,12 +155,12 @@ public Builder bigSegments(BigSegmentsConfigurationBuilder bigSegmentsConfigBuil * {@link com.launchdarkly.sdk.server.integrations.FileData#dataSource()}. See those methods * for details on how to configure them. * - * @param factory the factory object - * @return the builder + * @param dataSourceConfigurer the data source configuration builder + * @return the main configuration builder * @since 4.12.0 */ - public Builder dataSource(DataSourceFactory factory) { - this.dataSourceFactory = factory; + public Builder dataSource(ComponentConfigurer dataSourceConfigurer) { + this.dataSource = dataSourceConfigurer; return this; } @@ -176,14 +168,14 @@ public Builder dataSource(DataSourceFactory factory) { * Sets the implementation of the data store to be used for holding feature flags and * related data received from LaunchDarkly, using a factory object. The default is * {@link Components#inMemoryDataStore()}; for database integrations, use - * {@link Components#persistentDataStore(com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory)}. + * {@link Components#persistentDataStore(ComponentConfigurer)}. * - * @param factory the factory object - * @return the builder + * @param dataStoreConfigurer the data store configuration builder + * @return the main configuration builder * @since 4.12.0 */ - public Builder dataStore(DataStoreFactory factory) { - this.dataStoreFactory = factory; + public Builder dataStore(ComponentConfigurer dataStoreConfigurer) { + this.dataStore = dataStoreConfigurer; return this; } @@ -210,30 +202,34 @@ public Builder diagnosticOptOut(boolean diagnosticOptOut) { /** * Sets the implementation of {@link EventProcessor} to be used for processing analytics events. *

- * The default is {@link Components#sendEvents()}, but you may choose to use a custom implementation - * (for instance, a test fixture), or disable events with {@link Components#noEvents()}. + * The default is {@link Components#sendEvents()} with no custom options. You may instead call + * {@link Components#sendEvents()} and then set custom options for event processing; or, disable + * events with {@link Components#noEvents()}; or, choose to use a custom implementation (for + * instance, a test fixture). * - * @param factory a builder/factory object for event configuration - * @return the builder + * @param eventsConfigurer the events configuration builder + * @return the main configuration builder * @since 4.12.0 + * @see Components#sendEvents() + * @see Components#noEvents() */ - public Builder events(EventProcessorFactory factory) { - this.eventProcessorFactory = factory; + public Builder events(ComponentConfigurer eventsConfigurer) { + this.events = eventsConfigurer; return this; } /** - * Sets the SDK's networking configuration, using a factory object. This object is normally a - * configuration builder obtained from {@link Components#httpConfiguration()}, which has methods - * for setting individual HTTP-related properties. + * Sets the SDK's networking configuration, using a configuration builder. This builder is + * obtained from {@link Components#httpConfiguration()}, and has methods for setting individual + * HTTP-related properties. * - * @param factory the factory object - * @return the builder + * @param httpConfigurer the HTTP configuration builder + * @return the main configuration builder * @since 4.13.0 * @see Components#httpConfiguration() */ - public Builder http(HttpConfigurationFactory factory) { - this.httpConfigFactory = factory; + public Builder http(ComponentConfigurer httpConfigurer) { + this.http = httpConfigurer; return this; } @@ -242,13 +238,13 @@ public Builder http(HttpConfigurationFactory factory) { * configuration builder obtained from {@link Components#logging()}, which has methods * for setting individual logging-related properties. * - * @param factory the factory object - * @return the builder + * @param loggingConfigurer the logging configuration builder + * @return the main configuration builder * @since 5.0.0 * @see Components#logging() */ - public Builder logging(LoggingConfigurationFactory factory) { - this.loggingConfigFactory = factory; + public Builder logging(ComponentConfigurer loggingConfigurer) { + this.logging = loggingConfigurer; return this; } @@ -261,7 +257,7 @@ public Builder logging(LoggingConfigurationFactory factory) { *

* This is equivalent to calling {@code dataSource(Components.externalUpdatesOnly())} and * {@code events(Components.noEvents())}. It overrides any other values you may have set for - * {@link #dataSource(DataSourceFactory)} or {@link #events(EventProcessorFactory)}. + * {@link #dataSource(ComponentConfigurer)} or {@link #events(ComponentConfigurer)}. * * @param offline when set to true no calls to LaunchDarkly will be made * @return the builder diff --git a/src/main/java/com/launchdarkly/sdk/server/LoggingConfigurationImpl.java b/src/main/java/com/launchdarkly/sdk/server/LoggingConfigurationImpl.java deleted file mode 100644 index 81b7f1904..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/LoggingConfigurationImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.logging.LDLogAdapter; -import com.launchdarkly.sdk.server.interfaces.LoggingConfiguration; - -import java.time.Duration; - -final class LoggingConfigurationImpl implements LoggingConfiguration { - private final String baseLoggerName; - private final LDLogAdapter logAdapter; - private final Duration logDataSourceOutageAsErrorAfter; - - LoggingConfigurationImpl( - String baseLoggerName, - LDLogAdapter logAdapter, - Duration logDataSourceOutageAsErrorAfter - ) { - this.baseLoggerName = baseLoggerName; - this.logAdapter = logAdapter; - this.logDataSourceOutageAsErrorAfter = logDataSourceOutageAsErrorAfter; - } - - @Override - public String getBaseLoggerName() { - return baseLoggerName; - } - - @Override - public LDLogAdapter getLogAdapter() { - return logAdapter; - } - - @Override - public Duration getLogDataSourceOutageAsErrorAfter() { - return logDataSourceOutageAsErrorAfter; - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java b/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java index 836070293..b58adf354 100644 --- a/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java +++ b/src/main/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapper.java @@ -11,15 +11,15 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreUpdates; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStore; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreUpdateSink; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import java.io.IOException; import java.time.Duration; @@ -60,7 +60,7 @@ final class PersistentDataStoreWrapper implements DataStore { Duration cacheTtl, PersistentDataStoreBuilder.StaleValuesPolicy staleValuesPolicy, boolean recordCacheStats, - DataStoreUpdates dataStoreUpdates, + DataStoreUpdateSink dataStoreUpdates, ScheduledExecutorService sharedExecutor, LDLogger logger ) { diff --git a/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java b/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java index 435712878..99d63b552 100644 --- a/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java +++ b/src/main/java/com/launchdarkly/sdk/server/PollingProcessor.java @@ -2,14 +2,15 @@ import com.google.common.annotations.VisibleForTesting; import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.server.interfaces.DataSource; +import com.launchdarkly.sdk.internal.http.HttpErrors.HttpErrorException; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import java.io.IOException; import java.time.Duration; @@ -20,15 +21,15 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import static com.launchdarkly.sdk.server.Util.checkIfErrorIsRecoverableAndLog; -import static com.launchdarkly.sdk.server.Util.httpErrorDescription; +import static com.launchdarkly.sdk.internal.http.HttpErrors.checkIfErrorIsRecoverableAndLog; +import static com.launchdarkly.sdk.internal.http.HttpErrors.httpErrorDescription; final class PollingProcessor implements DataSource { private static final String ERROR_CONTEXT_MESSAGE = "on polling request"; private static final String WILL_RETRY_MESSAGE = "will retry at next scheduled poll interval"; @VisibleForTesting final FeatureRequestor requestor; - private final DataSourceUpdates dataSourceUpdates; + private final DataSourceUpdateSink dataSourceUpdates; private final ScheduledExecutorService scheduler; @VisibleForTesting final Duration pollInterval; private final AtomicBoolean initialized = new AtomicBoolean(false); @@ -38,7 +39,7 @@ final class PollingProcessor implements DataSource { PollingProcessor( FeatureRequestor requestor, - DataSourceUpdates dataSourceUpdates, + DataSourceUpdateSink dataSourceUpdates, ScheduledExecutorService sharedExecutor, Duration pollInterval, LDLogger logger diff --git a/src/main/java/com/launchdarkly/sdk/server/ServerSideDiagnosticEvents.java b/src/main/java/com/launchdarkly/sdk/server/ServerSideDiagnosticEvents.java new file mode 100644 index 000000000..d01b555d3 --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/ServerSideDiagnosticEvents.java @@ -0,0 +1,97 @@ +package com.launchdarkly.sdk.server; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.internal.events.DiagnosticConfigProperty; +import com.launchdarkly.sdk.internal.events.DiagnosticStore; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.DiagnosticDescription; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; + +abstract class ServerSideDiagnosticEvents { + public static DiagnosticStore.SdkDiagnosticParams getSdkDiagnosticParams( + ClientContext clientContext, + LDConfig config + ) { + return new DiagnosticStore.SdkDiagnosticParams( + clientContext.getSdkKey(), + "java-server-sdk", + Version.SDK_VERSION, + "java", + makePlatformData(), + ImmutableMap.copyOf(clientContext.getHttp().getDefaultHeaders()), + makeConfigProperties(clientContext, config) + ); + } + + private static ImmutableList makeConfigProperties(ClientContext clientContext, LDConfig config) { + ImmutableList.Builder listBuilder = ImmutableList.builder(); + HttpConfiguration httpConfig = clientContext.getHttp(); + + // Add the top-level properties that are not specific to a particular component type. + ObjectBuilder builder = LDValue.buildObject(); + builder.put(DiagnosticConfigProperty.CONNECT_TIMEOUT_MILLIS.name, httpConfig.getConnectTimeout().toMillis()); + builder.put(DiagnosticConfigProperty.SOCKET_TIMEOUT_MILLIS.name, httpConfig.getSocketTimeout().toMillis()); + builder.put(DiagnosticConfigProperty.USING_PROXY.name, httpConfig.getProxy() != null); + builder.put(DiagnosticConfigProperty.USING_PROXY_AUTHENTICATOR.name, httpConfig.getProxyAuthentication() != null); + builder.put(DiagnosticConfigProperty.START_WAIT_MILLIS.name, config.startWait.toMillis()); + listBuilder.add(builder.build()); + + // Allow each pluggable component to describe its own relevant properties. + listBuilder.add(describeComponent(config.dataStore, clientContext, DiagnosticConfigProperty.DATA_STORE_TYPE.name)); + listBuilder.add(describeComponent(config.dataSource, clientContext, null)); + listBuilder.add(describeComponent(config.events, clientContext, null)); + return listBuilder.build(); + } + + // Attempts to add relevant configuration properties, if any, from a customizable component: + // - If the component does not implement DiagnosticDescription, set the defaultPropertyName property to "custom". + // - If it does implement DiagnosticDescription, call its describeConfiguration() method to get a value. + // - If the value is a string, then set the defaultPropertyName property to that value. + // - If the value is an object, then copy all of its properties as long as they are ones we recognize + // and have the expected type. + private static LDValue describeComponent( + Object component, + ClientContext clientContext, + String defaultPropertyName + ) { + if (!(component instanceof DiagnosticDescription)) { + if (defaultPropertyName != null) { + return LDValue.buildObject().put(defaultPropertyName, "custom").build(); + } + return LDValue.ofNull(); + } + LDValue componentDesc = LDValue.normalize(((DiagnosticDescription)component).describeConfiguration(clientContext)); + if (defaultPropertyName == null) { + return componentDesc; + } + return LDValue.buildObject().put(defaultPropertyName, + componentDesc.isString() ? componentDesc.stringValue() : "custom").build(); + } + + private static LDValue makePlatformData() { + // We're getting these properties in the server-side-specific logic because they don't return + // useful values in Android. + return LDValue.buildObject() + .put("osName", normalizeOsName(System.getProperty("os.name"))) + .put("javaVendor", System.getProperty("java.vendor")) + .put("javaVersion", System.getProperty("java.version")) + .build(); + } + + private static String normalizeOsName(String osName) { + // For our diagnostics data, we prefer the standard names "Linux", "MacOS", and "Windows". + // "Linux" is already what the JRE returns in Linux. In Windows, we get "Windows 10" etc. + if (osName != null) { + if (osName.equals("Mac OS X")) { + return "MacOS"; + } + if (osName.startsWith("Windows")) { + return "Windows"; + } + } + return osName; + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/ServerSideEventContextDeduplicator.java b/src/main/java/com/launchdarkly/sdk/server/ServerSideEventContextDeduplicator.java new file mode 100644 index 000000000..37fb78fb7 --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/ServerSideEventContextDeduplicator.java @@ -0,0 +1,39 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.internal.events.EventContextDeduplicator; + +import java.time.Duration; + +final class ServerSideEventContextDeduplicator implements EventContextDeduplicator { + private final SimpleLRUCache contextKeys; + private final Duration flushInterval; + + public ServerSideEventContextDeduplicator( + int capacity, + Duration flushInterval + ) { + this.contextKeys = new SimpleLRUCache<>(capacity); + this.flushInterval = flushInterval; + } + + @Override + public Long getFlushInterval() { + return flushInterval.toMillis(); + } + + @Override + public boolean processContext(LDContext context) { + String key = context.getFullyQualifiedKey(); + if (key == null || key.isEmpty()) { + return false; + } + String previousValue = contextKeys.put(key, key); + return previousValue == null; + } + + @Override + public void flush() { + contextKeys.clear(); + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java b/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java index 9e64ce884..867d0e86e 100644 --- a/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java +++ b/src/main/java/com/launchdarkly/sdk/server/StandardEndpoints.java @@ -1,20 +1,18 @@ package com.launchdarkly.sdk.server; -import java.net.URI; - import com.launchdarkly.logging.LDLogger; +import java.net.URI; + abstract class StandardEndpoints { private StandardEndpoints() {} - static URI DEFAULT_STREAMING_BASE_URI = URI.create("https://stream.launchdarkly.com"); - static URI DEFAULT_POLLING_BASE_URI = URI.create("https://app.launchdarkly.com"); - static URI DEFAULT_EVENTS_BASE_URI = URI.create("https://events.launchdarkly.com"); + static final URI DEFAULT_STREAMING_BASE_URI = URI.create("https://stream.launchdarkly.com"); + static final URI DEFAULT_POLLING_BASE_URI = URI.create("https://app.launchdarkly.com"); + static final URI DEFAULT_EVENTS_BASE_URI = URI.create("https://events.launchdarkly.com"); - static String STREAMING_REQUEST_PATH = "/all"; - static String POLLING_REQUEST_PATH = "/sdk/latest-all"; - static String ANALYTICS_EVENTS_POST_REQUEST_PATH = "/bulk"; - static String DIAGNOSTIC_EVENTS_POST_REQUEST_PATH = "/diagnostic"; + static final String STREAMING_REQUEST_PATH = "/all"; + static final String POLLING_REQUEST_PATH = "/sdk/latest-all"; /** * Internal method to decide which URI a given component should connect to. @@ -23,16 +21,12 @@ private StandardEndpoints() {} * set some custom endpoints but not this one. * * @param serviceEndpointsValue the value set in ServiceEndpoints (this is either the default URI, a custom URI, or null) - * @param overrideValue the value overridden via the deprecated .baseURI() method (this is either a custom URI or null) * @param defaultValue the constant default URI value defined in StandardEndpoints * @param description a human-readable string for the type of endpoint being selected, for logging purposes * @param logger the logger to which we should print the warning, if needed * @return the base URI we should connect to */ - static URI selectBaseUri(URI serviceEndpointsValue, URI overrideValue, URI defaultValue, String description, LDLogger logger) { - if (overrideValue != null) { - return overrideValue; - } + static URI selectBaseUri(URI serviceEndpointsValue, URI defaultValue, String description, LDLogger logger) { if (serviceEndpointsValue != null) { return serviceEndpointsValue; } @@ -49,12 +43,10 @@ static URI selectBaseUri(URI serviceEndpointsValue, URI overrideValue, URI defau * for the purposes of this diagnostic. * * @param serviceEndpointsValue the value set in ServiceEndpoints - * @param overrideValue the value overridden via the deprecated .baseURI() method * @param defaultValue the constant default URI value defined in StandardEndpoints * @return true iff the base URI was customized */ - static boolean isCustomBaseUri(URI serviceEndpointsValue, URI overrideValue, URI defaultValue) { - return (overrideValue != null && !overrideValue.equals(defaultValue)) || - (serviceEndpointsValue != null && !serviceEndpointsValue.equals(defaultValue)); + static boolean isCustomBaseUri(URI serviceEndpointsValue, URI defaultValue) { + return serviceEndpointsValue != null && !serviceEndpointsValue.equals(defaultValue); } } \ No newline at end of file diff --git a/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java b/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java index a5b731784..b687ec5ec 100644 --- a/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java +++ b/src/main/java/com/launchdarkly/sdk/server/StreamProcessor.java @@ -11,18 +11,20 @@ import com.launchdarkly.eventsource.UnsuccessfulResponseException; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LogValues; +import com.launchdarkly.sdk.internal.events.DiagnosticStore; +import com.launchdarkly.sdk.internal.http.HttpHelpers; +import com.launchdarkly.sdk.internal.http.HttpProperties; import com.launchdarkly.sdk.server.StreamProcessorEvents.DeleteData; import com.launchdarkly.sdk.server.StreamProcessorEvents.PatchData; import com.launchdarkly.sdk.server.StreamProcessorEvents.PutData; -import com.launchdarkly.sdk.server.interfaces.DataSource; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import java.io.IOException; import java.io.Reader; @@ -31,14 +33,12 @@ import java.time.Instant; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; -import static com.launchdarkly.sdk.server.Util.checkIfErrorIsRecoverableAndLog; -import static com.launchdarkly.sdk.server.Util.concatenateUriPath; -import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder; -import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor; -import static com.launchdarkly.sdk.server.Util.httpErrorDescription; +import static com.launchdarkly.sdk.internal.http.HttpErrors.checkIfErrorIsRecoverableAndLog; +import static com.launchdarkly.sdk.internal.http.HttpErrors.httpErrorDescription; import okhttp3.Headers; import okhttp3.OkHttpClient; @@ -75,12 +75,12 @@ final class StreamProcessor implements DataSource { private static final String ERROR_CONTEXT_MESSAGE = "in stream connection"; private static final String WILL_RETRY_MESSAGE = "will retry"; - private final DataSourceUpdates dataSourceUpdates; - private final HttpConfiguration httpConfig; + private final DataSourceUpdateSink dataSourceUpdates; + private final HttpProperties httpProperties; private final Headers headers; @VisibleForTesting final URI streamUri; @VisibleForTesting final Duration initialReconnectDelay; - private final DiagnosticAccumulator diagnosticAccumulator; + private final DiagnosticStore diagnosticAccumulator; private final int threadPriority; private final DataStoreStatusProvider.StatusListener statusListener; private volatile EventSource es; @@ -92,23 +92,23 @@ final class StreamProcessor implements DataSource { ConnectionErrorHandler connectionErrorHandler = createDefaultConnectionErrorHandler(); // exposed for testing StreamProcessor( - HttpConfiguration httpConfig, - DataSourceUpdates dataSourceUpdates, + HttpProperties httpProperties, + DataSourceUpdateSink dataSourceUpdates, int threadPriority, - DiagnosticAccumulator diagnosticAccumulator, + DiagnosticStore diagnosticAccumulator, URI streamUri, Duration initialReconnectDelay, LDLogger logger ) { this.dataSourceUpdates = dataSourceUpdates; - this.httpConfig = httpConfig; + this.httpProperties = httpProperties; this.diagnosticAccumulator = diagnosticAccumulator; this.threadPriority = threadPriority; this.streamUri = streamUri; this.initialReconnectDelay = initialReconnectDelay; this.logger = logger; - - this.headers = getHeadersBuilderFor(httpConfig) + + this.headers = httpProperties.toHeadersBuilder() .add("Accept", "text/event-stream") .build(); @@ -175,7 +175,7 @@ public Future start() { }; EventHandler handler = new StreamEventHandler(initFuture); - URI endpointUri = concatenateUriPath(streamUri, StandardEndpoints.STREAMING_REQUEST_PATH); + URI endpointUri = HttpHelpers.concatenateUriPath(streamUri, StandardEndpoints.STREAMING_REQUEST_PATH); // Notes about the configuration of the EventSource below: // @@ -191,19 +191,19 @@ public Future start() { EventSource.Builder builder = new EventSource.Builder(handler, endpointUri) .threadPriority(threadPriority) - .logger(new EventSourceLoggerAdapter()) + .logger(logger) .readBufferSize(5000) .streamEventData(true) .expectFields("event") .clientBuilderActions(new EventSource.Builder.ClientConfigurer() { - public void configure(OkHttpClient.Builder builder) { - configureHttpClientBuilder(httpConfig, builder); + public void configure(OkHttpClient.Builder clientBuilder) { + httpProperties.applyToHttpClientBuilder(clientBuilder); } }) .connectionErrorHandler(wrappedConnectionErrorHandler) .headers(headers) - .reconnectTime(initialReconnectDelay) - .readTimeout(DEAD_CONNECTION_INTERVAL); + .reconnectTime(initialReconnectDelay.toMillis(), TimeUnit.MILLISECONDS) + .readTimeout(DEAD_CONNECTION_INTERVAL.toMillis(), TimeUnit.MILLISECONDS); es = builder.build(); esStarted = System.currentTimeMillis(); @@ -372,31 +372,4 @@ public StreamInputException(Throwable cause) { // This exception class indicates that the data store failed to persist an update. @SuppressWarnings("serial") private static final class StreamStoreException extends Exception {} - - private final class EventSourceLoggerAdapter implements com.launchdarkly.eventsource.Logger { - @Override - public void debug(String format, Object param) { - logger.debug(format, param); - } - - @Override - public void debug(String format, Object param1, Object param2) { - logger.debug(format, param1, param2); - } - - @Override - public void info(String message) { - logger.info(message); - } - - @Override - public void warn(String message) { - logger.warn(message); - } - - @Override - public void error(String message) { - logger.error(message); - } - } } diff --git a/src/main/java/com/launchdarkly/sdk/server/StreamProcessorEvents.java b/src/main/java/com/launchdarkly/sdk/server/StreamProcessorEvents.java index b8d740265..06fb6b9a8 100644 --- a/src/main/java/com/launchdarkly/sdk/server/StreamProcessorEvents.java +++ b/src/main/java/com/launchdarkly/sdk/server/StreamProcessorEvents.java @@ -6,10 +6,10 @@ import com.google.gson.stream.JsonToken; import com.launchdarkly.sdk.server.DataModel.VersionedData; import com.launchdarkly.sdk.server.DataModelDependencies.KindAndKey; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import java.io.IOException; diff --git a/src/main/java/com/launchdarkly/sdk/server/Util.java b/src/main/java/com/launchdarkly/sdk/server/Util.java index 5c3126154..67c4b7dfd 100644 --- a/src/main/java/com/launchdarkly/sdk/server/Util.java +++ b/src/main/java/com/launchdarkly/sdk/server/Util.java @@ -3,10 +3,8 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.server.interfaces.ApplicationInfo; import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; import java.io.IOException; -import java.net.URI; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -15,16 +13,11 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.regex.Pattern; -import java.util.concurrent.TimeUnit; import static com.google.common.collect.Iterables.transform; import okhttp3.Authenticator; -import okhttp3.ConnectionPool; -import okhttp3.Headers; -import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.Route; @@ -32,145 +25,22 @@ abstract class Util { private Util() {} - static Headers.Builder getHeadersBuilderFor(HttpConfiguration config) { - Headers.Builder builder = new Headers.Builder(); - for (Map.Entry kv: config.getDefaultHeaders()) { - builder.add(kv.getKey(), kv.getValue()); - } - return builder; - } - - // This is specifically testing whether the string would be considered a valid HTTP header value - // *by the OkHttp client*. The actual HTTP spec does not prohibit characters >= 127; OkHttp's - // check is overly strict, as was pointed out in https://github.com/square/okhttp/issues/2016. - // But all OkHttp 3.x and 4.x versions so far have continued to enforce that check. Control - // characters other than a tab are always illegal. - // - // The value we're mainly concerned with is the SDK key (Authorization header). If an SDK key - // accidentally has (for instance) a newline added to it, we don't want to end up having OkHttp - // throw an exception mentioning the value, which might get logged (https://github.com/square/okhttp/issues/6738). - static boolean isAsciiHeaderValue(String value) { - for (int i = 0; i < value.length(); i++) { - char ch = value.charAt(i); - if ((ch < 0x20 || ch > 0x7e) && ch != '\t') { - return false; - } - } - return true; - } - - static void configureHttpClientBuilder(HttpConfiguration config, OkHttpClient.Builder builder) { - builder.connectionPool(new ConnectionPool(5, 5, TimeUnit.SECONDS)) - .connectTimeout(config.getConnectTimeout()) - .readTimeout(config.getSocketTimeout()) - .writeTimeout(config.getSocketTimeout()) - .retryOnConnectionFailure(false); // we will implement our own retry logic - - if (config.getSocketFactory() != null) { - builder.socketFactory(config.getSocketFactory()); - } - - if (config.getSslSocketFactory() != null) { - builder.sslSocketFactory(config.getSslSocketFactory(), config.getTrustManager()); - } - - if (config.getProxy() != null) { - builder.proxy(config.getProxy()); - if (config.getProxyAuthentication() != null) { - builder.proxyAuthenticator(okhttpAuthenticatorFromHttpAuthStrategy( - config.getProxyAuthentication(), - "Proxy-Authentication", - "Proxy-Authorization" - )); - } - } - } - - static final Authenticator okhttpAuthenticatorFromHttpAuthStrategy(final HttpAuthentication strategy, - final String challengeHeaderName, final String responseHeaderName) { + static final Authenticator okhttpAuthenticatorFromHttpAuthStrategy(final HttpAuthentication strategy) { return new Authenticator() { public Request authenticate(Route route, Response response) throws IOException { - if (response.request().header(responseHeaderName) != null) { + if (response.request().header("Proxy-Authorization") != null) { return null; // Give up, we've already failed to authenticate } Iterable challenges = transform(response.challenges(), c -> new HttpAuthentication.Challenge(c.scheme(), c.realm())); String credential = strategy.provideAuthorization(challenges); return response.request().newBuilder() - .header(responseHeaderName, credential) + .header("Proxy-Authorization", credential) .build(); } }; } - static void shutdownHttpClient(OkHttpClient client) { - if (client.dispatcher() != null) { - client.dispatcher().cancelAll(); - if (client.dispatcher().executorService() != null) { - client.dispatcher().executorService().shutdown(); - } - } - if (client.connectionPool() != null) { - client.connectionPool().evictAll(); - } - if (client.cache() != null) { - try { - client.cache().close(); - } catch (Exception e) {} - } - } - - /** - * Tests whether an HTTP error status represents a condition that might resolve on its own if we retry. - * @param statusCode the HTTP status - * @return true if retrying makes sense; false if it should be considered a permanent failure - */ - static boolean isHttpErrorRecoverable(int statusCode) { - if (statusCode >= 400 && statusCode < 500) { - switch (statusCode) { - case 400: // bad request - case 408: // request timeout - case 429: // too many requests - return true; - default: - return false; // all other 4xx errors are unrecoverable - } - } - return true; - } - - /** - * Logs an HTTP error or network error at the appropriate level and determines whether it is recoverable - * (as defined by {@link #isHttpErrorRecoverable(int)}). - * - * @param logger the logger to log to - * @param errorDesc description of the error - * @param errorContext a phrase like "when doing such-and-such" - * @param statusCode HTTP status code, or 0 for a network error - * @param recoverableMessage a phrase like "will retry" to use if the error is recoverable - * @return true if the error is recoverable - */ - static boolean checkIfErrorIsRecoverableAndLog( - LDLogger logger, - String errorDesc, - String errorContext, - int statusCode, - String recoverableMessage - ) { - if (statusCode > 0 && !isHttpErrorRecoverable(statusCode)) { - logger.error("Error {} (giving up permanently): {}", errorContext, errorDesc); - return false; - } else { - logger.warn("Error {} ({}): {}", errorContext, recoverableMessage, errorDesc); - return true; - } - } - - static String httpErrorDescription(int statusCode) { - return "HTTP error " + statusCode + - (statusCode == 401 || statusCode == 403 ? " (invalid SDK key)" : ""); - } - static String describeDuration(Duration d) { if (d.toMillis() % 1000 == 0) { if (d.toMillis() % 60000 == 0) { @@ -205,12 +75,6 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) { } catch (IOException e) {} } - static URI concatenateUriPath(URI baseUri, String path) { - String uriStr = baseUri.toString(); - String addPath = path.startsWith("/") ? path.substring(1) : path; - return URI.create(uriStr + (uriStr.endsWith("/") ? "" : "/") + addPath); - } - // Tag values must not be empty, and only contain letters, numbers, `.`, `_`, or `-`. private static Pattern TAG_VALUE_REGEX = Pattern.compile("^[\\w.-]+$"); diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilder.java index 5b19d9258..aac381601 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilder.java @@ -1,12 +1,13 @@ package com.launchdarkly.sdk.server.integrations; import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.LDConfig; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; +import com.launchdarkly.sdk.server.LDConfig.Builder; import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; -import com.launchdarkly.sdk.server.interfaces.ClientContext; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; import java.time.Duration; @@ -18,8 +19,8 @@ * . *

* If you want non-default values for any of these properties create a builder with - * {@link Components#bigSegments(BigSegmentStoreFactory)}, change its properties with the methods - * of this class, and pass it to {@link LDConfig.Builder#bigSegments(BigSegmentsConfigurationBuilder)} + * {@link Components#bigSegments(ComponentConfigurer)}, change its properties with the methods + * of this class, and pass it to {@link Builder#bigSegments(ComponentConfigurer)} *


  *     LDConfig config = new LDConfig.Builder()
  *         .bigSegments(Components.bigSegments(Redis.dataStore().prefix("app1"))
@@ -29,7 +30,7 @@
  *
  * @since 5.7.0
  */
-public final class BigSegmentsConfigurationBuilder {
+public final class BigSegmentsConfigurationBuilder implements ComponentConfigurer {
   /**
    * The default value for {@link #userCacheSize(int)}.
    */
@@ -50,7 +51,7 @@ public final class BigSegmentsConfigurationBuilder {
    */
   public static final Duration DEFAULT_STALE_AFTER = Duration.ofMinutes(2);
 
-  private final BigSegmentStoreFactory storeFactory;
+  private final ComponentConfigurer storeConfigurer;
   private int userCacheSize = DEFAULT_USER_CACHE_SIZE;
   private Duration userCacheTime = DEFAULT_USER_CACHE_TIME;
   private Duration statusPollInterval = DEFAULT_STATUS_POLL_INTERVAL;
@@ -59,10 +60,10 @@ public final class BigSegmentsConfigurationBuilder {
   /**
    * Creates a new builder for Big Segments configuration.
    *
-   * @param storeFactory the factory implementation for the specific data store type
+   * @param storeConfigurer the factory implementation for the specific data store type
    */
-  public BigSegmentsConfigurationBuilder(BigSegmentStoreFactory storeFactory) {
-    this.storeFactory = storeFactory;
+  public BigSegmentsConfigurationBuilder(ComponentConfigurer storeConfigurer) {
+    this.storeConfigurer = storeConfigurer;
   }
 
   /**
@@ -137,8 +138,8 @@ public BigSegmentsConfigurationBuilder statusPollInterval(Duration statusPollInt
    * While in a stale state, the SDK will still continue using the last known data, but
    * {@link com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider.Status} will return
    * true in its {@code stale} property, and any {@link EvaluationReason} generated from a feature
-   * flag that references a Big Segment will have a {@link EvaluationReason.BigSegmentsStatus} of
-   * {@link EvaluationReason.BigSegmentsStatus#STALE}.
+   * flag that references a Big Segment will have a {@link BigSegmentsStatus} of
+   * {@link BigSegmentsStatus#STALE}.
    *
    * @param staleAfter the time limit for marking the data as stale (a null, zero, or negative
    *                   value will be changed to {@link #DEFAULT_STALE_AFTER})
@@ -151,15 +152,9 @@ public BigSegmentsConfigurationBuilder staleAfter(Duration staleAfter) {
     return this;
   }
 
-  /**
-   * Called internally by the SDK to create a configuration instance. Applications do not need to
-   * call this method.
-   *
-   * @param context allows access to the client configuration
-   * @return a {@link BigSegmentsConfiguration} instance
-   */
-  public BigSegmentsConfiguration createBigSegmentsConfiguration(ClientContext context) {
-    BigSegmentStore store = storeFactory == null ? null : storeFactory.createBigSegmentStore(context);
+  @Override
+  public BigSegmentsConfiguration build(ClientContext context) {
+    BigSegmentStore store = storeConfigurer == null ? null : storeConfigurer.build(context);
     return new BigSegmentsConfiguration(
         store,
         userCacheSize,
diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilder.java
index a257e168c..4c79c4e59 100644
--- a/src/main/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilder.java
+++ b/src/main/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilder.java
@@ -1,14 +1,13 @@
 package com.launchdarkly.sdk.server.integrations;
 
-import com.launchdarkly.sdk.UserAttribute;
+import com.launchdarkly.sdk.AttributeRef;
 import com.launchdarkly.sdk.server.Components;
-import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory;
-import com.launchdarkly.sdk.server.interfaces.EventSender;
-import com.launchdarkly.sdk.server.interfaces.EventSenderFactory;
+import com.launchdarkly.sdk.server.LDConfig.Builder;
+import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer;
+import com.launchdarkly.sdk.server.subsystems.EventProcessor;
+import com.launchdarkly.sdk.server.subsystems.EventSender;
 
-import java.net.URI;
 import java.time.Duration;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -17,7 +16,7 @@
  * 

* The SDK normally buffers analytics events and sends them to LaunchDarkly at intervals. If you want * to customize this behavior, create a builder with {@link Components#sendEvents()}, change its - * properties with the methods of this class, and pass it to {@link com.launchdarkly.sdk.server.LDConfig.Builder#events(EventProcessorFactory)}: + * properties with the methods of this class, and pass it to {@link Builder#events(ComponentConfigurer)}: *


  *     LDConfig config = new LDConfig.Builder()
  *         .events(Components.sendEvents().capacity(5000).flushIntervalSeconds(2))
@@ -28,7 +27,7 @@
  * 
  * @since 4.12.0
  */
-public abstract class EventProcessorBuilder implements EventProcessorFactory {
+public abstract class EventProcessorBuilder implements ComponentConfigurer {
   /**
    * The default value for {@link #capacity(int)}.
    */
@@ -60,59 +59,31 @@ public abstract class EventProcessorBuilder implements EventProcessorFactory {
   public static final Duration MIN_DIAGNOSTIC_RECORDING_INTERVAL = Duration.ofSeconds(60);
   
   protected boolean allAttributesPrivate = false;
-  protected URI baseURI;
   protected int capacity = DEFAULT_CAPACITY;
   protected Duration diagnosticRecordingInterval = DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL;
   protected Duration flushInterval = DEFAULT_FLUSH_INTERVAL;
-  protected boolean inlineUsersInEvents = false;
-  protected Set privateAttributes;
+  protected Set privateAttributes;
   protected int userKeysCapacity = DEFAULT_USER_KEYS_CAPACITY;
   protected Duration userKeysFlushInterval = DEFAULT_USER_KEYS_FLUSH_INTERVAL;
-  protected EventSenderFactory eventSenderFactory = null;
+  protected ComponentConfigurer eventSenderConfigurer = null;
 
   /**
    * Sets whether or not all optional user attributes should be hidden from LaunchDarkly.
    * 

* If this is {@code true}, all user attribute values (other than the key) will be private, not just - * the attributes specified in {@link #privateAttributeNames(String...)} or on a per-user basis with - * {@link com.launchdarkly.sdk.LDUser.Builder} methods. By default, it is {@code false}. + * the attributes specified in {@link #privateAttributes(String...)} or on a per-user basis with + * {@link com.launchdarkly.sdk.ContextBuilder} methods. By default, it is {@code false}. * * @param allAttributesPrivate true if all user attributes should be private * @return the builder - * @see #privateAttributeNames(String...) - * @see com.launchdarkly.sdk.LDUser.Builder + * @see #privateAttributes(String...) + * @see com.launchdarkly.sdk.ContextBuilder */ public EventProcessorBuilder allAttributesPrivate(boolean allAttributesPrivate) { this.allAttributesPrivate = allAttributesPrivate; return this; } - /** - * Deprecated method for setting a custom base URI for the events service. - *

- * The preferred way to set this option is now with - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. - * If you set this deprecated option, it overrides any value that was set with - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. - *

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

    - *
  • You are using the Relay Proxy with - * event forwarding enabled. Set {@code streamUri} to the base URI of the Relay Proxy instance. - *
  • You are connecting to a test server or a nonstandard endpoint for the LaunchDarkly service. - *
- * - * @param baseURI the base URI of the events service; null to use the default - * @return the builder - * @deprecated Use {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)} and - * {@link ServiceEndpointsBuilder#events(URI)}. - */ - @Deprecated - public EventProcessorBuilder baseURI(URI baseURI) { - this.baseURI = baseURI; - return this; - } - /** * Set the capacity of the events buffer. *

@@ -135,9 +106,9 @@ public EventProcessorBuilder capacity(int capacity) { *

* The default value is {@link #DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL}; the minimum value is * {@link #MIN_DIAGNOSTIC_RECORDING_INTERVAL}. This property is ignored if - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#diagnosticOptOut(boolean)} is set to {@code true}. + * {@link Builder#diagnosticOptOut(boolean)} is set to {@code true}. * - * @see com.launchdarkly.sdk.server.LDConfig.Builder#diagnosticOptOut(boolean) + * @see Builder#diagnosticOptOut(boolean) * * @param diagnosticRecordingInterval the diagnostics interval; null to use the default * @return the builder @@ -156,14 +127,14 @@ public EventProcessorBuilder diagnosticRecordingInterval(Duration diagnosticReco * Specifies a custom implementation for event delivery. *

* The standard event delivery implementation sends event data via HTTP/HTTPS to the LaunchDarkly events - * service endpoint (or any other endpoint specified with {@link #baseURI(URI)}. Providing a custom - * implementation may be useful in tests, or if the event data needs to be stored and forwarded. + * service endpoint (or any other endpoint specified with {@link Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + * Providing a custom implementation may be useful in tests, or if the event data needs to be stored and forwarded. * - * @param eventSenderFactory a factory for an {@link EventSender} implementation + * @param eventSenderConfigurer a factory for an {@link EventSender} implementation * @return the builder */ - public EventProcessorBuilder eventSender(EventSenderFactory eventSenderFactory) { - this.eventSenderFactory = eventSenderFactory; + public EventProcessorBuilder eventSender(ComponentConfigurer eventSenderConfigurer) { + this.eventSenderConfigurer = eventSenderConfigurer; return this; } @@ -181,62 +152,37 @@ public EventProcessorBuilder flushInterval(Duration flushInterval) { this.flushInterval = flushInterval == null ? DEFAULT_FLUSH_INTERVAL : flushInterval; return this; } - - /** - * Sets whether to include full user details in every analytics event. - *

- * The default is {@code false}: events will only include the user key, except for one "index" event - * that provides the full details for the user). - * - * @param inlineUsersInEvents true if you want full user details in each event - * @return the builder - */ - public EventProcessorBuilder inlineUsersInEvents(boolean inlineUsersInEvents) { - this.inlineUsersInEvents = inlineUsersInEvents; - return this; - } /** - * Marks a set of attribute names as private. + * Marks a set of attribute names or subproperties as private. *

- * Any users sent to LaunchDarkly with this configuration active will have attributes with these + * Any contexts sent to LaunchDarkly with this configuration active will have attributes with these * names removed. This is in addition to any attributes that were marked as private for an - * individual user with {@link com.launchdarkly.sdk.LDUser.Builder} methods. + * individual context with {@link com.launchdarkly.sdk.ContextBuilder} methods. *

- * Using {@link #privateAttributes(UserAttribute...)} is preferable to avoid the possibility of - * misspelling a built-in attribute. - * - * @param attributeNames a set of names that will be removed from user data set to LaunchDarkly + * If and only if a parameter starts with a slash, it is interpreted as a slash-delimited path that + * can denote a nested property within a JSON object. For instance, "/address/street" means that if + * there is an attribute called "address" that is a JSON object, and one of the object's properties + * is "street", the "street" property will be redacted from the analytics data but other properties + * within "address" will still be sent. This syntax also uses the JSON Pointer convention of escaping + * a literal slash character as "~1" and a tilde as "~0". + *

+ * This method replaces any previous private attributes that were set on the same builder, rather + * than adding to them. + * + * @param attributeNames a set of names or paths that will be removed from context data set to LaunchDarkly * @return the builder * @see #allAttributesPrivate(boolean) - * @see com.launchdarkly.sdk.LDUser.Builder + * @see com.launchdarkly.sdk.ContextBuilder#privateAttributes(String...) */ - public EventProcessorBuilder privateAttributeNames(String... attributeNames) { + public EventProcessorBuilder privateAttributes(String... attributeNames) { privateAttributes = new HashSet<>(); for (String a: attributeNames) { - privateAttributes.add(UserAttribute.forName(a)); + privateAttributes.add(AttributeRef.fromPath(a)); } return this; } - /** - * Marks a set of attribute names as private. - *

- * Any users sent to LaunchDarkly with this configuration active will have attributes with these - * names removed. This is in addition to any attributes that were marked as private for an - * individual user with {@link com.launchdarkly.sdk.LDUser.Builder} methods. - * - * @param attributes a set of attributes that will be removed from user data set to LaunchDarkly - * @return the builder - * @see #allAttributesPrivate(boolean) - * @see com.launchdarkly.sdk.LDUser.Builder - * @see #privateAttributeNames - */ - public EventProcessorBuilder privateAttributes(UserAttribute... attributes) { - privateAttributes = new HashSet<>(Arrays.asList(attributes)); - return this; - } - /** * Sets the number of user keys that the event processor can remember at any one time. *

diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java b/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java index 1c7c60fb1..c75b64c02 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/FileData.java @@ -1,5 +1,7 @@ package com.launchdarkly.sdk.server.integrations; +import com.launchdarkly.sdk.server.LDConfig.Builder; + /** * Integration between the LaunchDarkly SDK and file data. *

@@ -39,7 +41,7 @@ public enum DuplicateKeysHandling { *

* This object can be modified with {@link FileDataSourceBuilder} methods for any desired * custom settings, before including it in the SDK configuration with - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#dataSource(com.launchdarkly.sdk.server.interfaces.DataSourceFactory)}. + * {@link Builder#dataSource(com.launchdarkly.sdk.server.subsystems.ComponentConfigurer)}. *

* At a minimum, you will want to call {@link FileDataSourceBuilder#filePaths(String...)} to specify * your data file(s); you can also use {@link FileDataSourceBuilder#autoUpdate(boolean)} to @@ -57,7 +59,7 @@ public enum DuplicateKeysHandling { * This will cause the client not to connect to LaunchDarkly to get feature flags. The * client may still make network connections to send analytics events, unless you have disabled * this with {@link com.launchdarkly.sdk.server.Components#noEvents()}. IMPORTANT: Do not - * set {@link com.launchdarkly.sdk.server.LDConfig.Builder#offline(boolean)} to {@code true}; doing so + * set {@link Builder#offline(boolean)} to {@code true}; doing so * would not just put the SDK "offline" with regard to LaunchDarkly, but will completely turn off * all flag data sources to the SDK including the file data source. *

diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java index 0ed450ca1..14d607c5c 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceBuilder.java @@ -2,10 +2,10 @@ import com.google.common.io.ByteStreams; import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataSource; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; +import com.launchdarkly.sdk.server.LDConfig.Builder; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; import java.io.IOException; import java.io.InputStream; @@ -20,13 +20,13 @@ * To use the file data source, obtain a new instance of this class with {@link FileData#dataSource()}; * call the builder method {@link #filePaths(String...)} to specify file path(s), and/or * {@link #classpathResources(String...)} to specify classpath data resources; then pass the resulting - * object to {@link com.launchdarkly.sdk.server.LDConfig.Builder#dataSource(DataSourceFactory)}. + * object to {@link Builder#dataSource(ComponentConfigurer)}. *

* For more details, see {@link FileData}. * * @since 4.12.0 */ -public final class FileDataSourceBuilder implements DataSourceFactory { +public final class FileDataSourceBuilder implements ComponentConfigurer { final List sources = new ArrayList<>(); // visible for tests private boolean autoUpdate = false; private FileData.DuplicateKeysHandling duplicateKeysHandling = FileData.DuplicateKeysHandling.FAIL; @@ -118,13 +118,10 @@ public FileDataSourceBuilder duplicateKeysHandling(FileData.DuplicateKeysHandlin return this; } - /** - * Used internally by the LaunchDarkly client. - */ @Override - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) { - LDLogger logger = context.getBasic().getBaseLogger().subLogger("DataSource"); - return new FileDataSourceImpl(dataSourceUpdates, sources, autoUpdate, duplicateKeysHandling, logger); + public DataSource build(ClientContext context) { + LDLogger logger = context.getBaseLogger().subLogger("DataSource"); + return new FileDataSourceImpl(context.getDataSourceUpdateSink(), sources, autoUpdate, duplicateKeysHandling, logger); } static abstract class SourceInfo { diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java index 99fdfa98f..08f39a234 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceImpl.java @@ -9,15 +9,15 @@ import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFactory; import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFileParser; import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FlagFileRep; -import com.launchdarkly.sdk.server.interfaces.DataSource; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -52,7 +52,7 @@ * optionally whenever files change. */ final class FileDataSourceImpl implements DataSource { - private final DataSourceUpdates dataSourceUpdates; + private final DataSourceUpdateSink dataSourceUpdates; private final DataLoader dataLoader; private final FileData.DuplicateKeysHandling duplicateKeysHandling; private final AtomicBoolean inited = new AtomicBoolean(false); @@ -60,7 +60,7 @@ final class FileDataSourceImpl implements DataSource { private final LDLogger logger; FileDataSourceImpl( - DataSourceUpdates dataSourceUpdates, + DataSourceUpdateSink dataSourceUpdates, List sources, boolean autoUpdate, FileData.DuplicateKeysHandling duplicateKeysHandling, diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceParsing.java b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceParsing.java index 03bfd3676..cca8aac93 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceParsing.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/FileDataSourceParsing.java @@ -7,7 +7,7 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.server.integrations.FileDataSourceBuilder.SourceInfo; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.error.YAMLException; diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java index 9a3ca86c4..e7ed3ad48 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilder.java @@ -2,7 +2,8 @@ import com.launchdarkly.sdk.server.Components; import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; -import com.launchdarkly.sdk.server.interfaces.HttpConfigurationFactory; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; import java.time.Duration; @@ -15,7 +16,7 @@ *

* If you want to set non-default values for any of these properties, create a builder with * {@link Components#httpConfiguration()}, change its properties with the methods of this class, - * and pass it to {@link com.launchdarkly.sdk.server.LDConfig.Builder#http(HttpConfigurationFactory)}: + * and pass it to {@link com.launchdarkly.sdk.server.LDConfig.Builder#http(ComponentConfigurer)}: *


  *     LDConfig config = new LDConfig.Builder()
  *         .http(
@@ -30,7 +31,7 @@
  * 
  * @since 4.13.0
  */
-public abstract class HttpConfigurationBuilder implements HttpConfigurationFactory {
+public abstract class HttpConfigurationBuilder implements ComponentConfigurer {
   /**
    * The default value for {@link #connectTimeout(Duration)}: two seconds.
    */
diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilder.java
index b321d7f7f..9eca6eb19 100644
--- a/src/main/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilder.java
+++ b/src/main/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilder.java
@@ -4,7 +4,8 @@
 import com.launchdarkly.logging.LDLogLevel;
 import com.launchdarkly.logging.Logs;
 import com.launchdarkly.sdk.server.Components;
-import com.launchdarkly.sdk.server.interfaces.LoggingConfigurationFactory;
+import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer;
+import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
 
 import java.time.Duration;
 
@@ -13,7 +14,7 @@
  * 

* If you want to set non-default values for any of these properties, create a builder with * {@link Components#logging()}, change its properties with the methods of this class, and pass it - * to {@link com.launchdarkly.sdk.server.LDConfig.Builder#logging(LoggingConfigurationFactory)}: + * to {@link com.launchdarkly.sdk.server.LDConfig.Builder#logging(ComponentConfigurer)}: *


  *     LDConfig config = new LDConfig.Builder()
  *         .logging(
@@ -27,7 +28,7 @@
  * 
  * @since 5.0.0
  */
-public abstract class LoggingConfigurationBuilder implements LoggingConfigurationFactory {
+public abstract class LoggingConfigurationBuilder implements ComponentConfigurer {
   /**
    * The default value for {@link #logDataSourceOutageAsErrorAfter(Duration)}: one minute.
    */
@@ -42,13 +43,18 @@ public abstract class LoggingConfigurationBuilder implements LoggingConfiguratio
    * Specifies the implementation of logging to use.
    * 

* The com.launchdarkly.logging - * API defines the {@link LDLogAdapter} interface to specify where log output should be sent. By default, - * it is set to {@link com.launchdarkly.logging.LDSLF4J#adapter()}, meaning that output will be sent to - * SLF4J and controlled by the SLF4J configuration. You may use - * the {@link com.launchdarkly.logging.Logs} factory methods, or a custom implementation, to handle log - * output differently. For instance, you may specify {@link com.launchdarkly.logging.Logs#basic()} for - * simple console output, or {@link com.launchdarkly.logging.Logs#toJavaUtilLogging()} to use the - * java.util.logging framework. + * API defines the {@link LDLogAdapter} interface to specify where log output should be sent. + *

+ * The default logging destination, if no adapter is specified, depends on whether + * SLF4J is present in the classpath. If it is, then the SDK uses + * {@link com.launchdarkly.logging.LDSLF4J#adapter()}, causing output to go to SLF4J; what happens to + * the output then is determined by the SLF4J configuration. If SLF4J is not present in the classpath, + * the SDK uses {@link Logs#toConsole()} instead, causing output to go to the {@code System.err} stream. + *

+ * You may use the {@link com.launchdarkly.logging.Logs} factory methods, or a custom implementation, + * to handle log output differently. For instance, you may specify + * {@link com.launchdarkly.logging.Logs#toJavaUtilLogging()} to use the java.util.logging + * framework. *

* For more about logging adapters, * see the SDK reference guide diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilder.java index 50608411f..406be5927 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilder.java @@ -1,9 +1,11 @@ package com.launchdarkly.sdk.server.integrations; import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; +import com.launchdarkly.sdk.server.LDConfig.Builder; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import java.time.Duration; import java.util.concurrent.TimeUnit; @@ -12,11 +14,11 @@ * A configurable factory for a persistent data store. *

* Several database integrations exist for the LaunchDarkly SDK, each with its own behavior and options - * specific to that database; this is described via some implementation of {@link PersistentDataStoreFactory}. + * specific to that database; this is described via some implementation of {@link PersistentDataStore}. * There is also universal behavior that the SDK provides for all persistent data stores, such as caching; * the {@link PersistentDataStoreBuilder} adds this. *

- * After configuring this object, pass it to {@link com.launchdarkly.sdk.server.LDConfig.Builder#dataStore(DataStoreFactory)} + * After configuring this object, pass it to {@link Builder#dataStore(ComponentConfigurer)} * to use it in the SDK configuration. For example, using the Redis integration: * *


@@ -33,16 +35,16 @@
  * {@code cacheSeconds()} is an option that can be used for any persistent data store. 
  * 

* Note that this class is abstract; the actual implementation is created by calling - * {@link Components#persistentDataStore(PersistentDataStoreFactory)}. + * {@link Components#persistentDataStore(ComponentConfigurer)}. * @since 4.12.0 */ -public abstract class PersistentDataStoreBuilder implements DataStoreFactory { +public abstract class PersistentDataStoreBuilder implements ComponentConfigurer { /** * The default value for the cache TTL. */ public static final Duration DEFAULT_CACHE_TTL = Duration.ofSeconds(15); - protected final PersistentDataStoreFactory persistentDataStoreFactory; // see Components for why these are not private + protected final ComponentConfigurer persistentDataStoreConfigurer; // see Components for why these are not private protected Duration cacheTime = DEFAULT_CACHE_TTL; protected StaleValuesPolicy staleValuesPolicy = StaleValuesPolicy.EVICT; protected boolean recordCacheStats = false; @@ -95,10 +97,10 @@ public enum StaleValuesPolicy { /** * Creates a new builder. * - * @param persistentDataStoreFactory the factory implementation for the specific data store type + * @param persistentDataStoreConfigurer the factory implementation for the specific data store type */ - protected PersistentDataStoreBuilder(PersistentDataStoreFactory persistentDataStoreFactory) { - this.persistentDataStoreFactory = persistentDataStoreFactory; + protected PersistentDataStoreBuilder(ComponentConfigurer persistentDataStoreConfigurer) { + this.persistentDataStoreConfigurer = persistentDataStoreConfigurer; } /** diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilder.java index 6b285d5c2..f0fa81872 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilder.java @@ -1,9 +1,10 @@ package com.launchdarkly.sdk.server.integrations; import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; +import com.launchdarkly.sdk.server.LDConfig.Builder; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; -import java.net.URI; import java.time.Duration; /** @@ -15,7 +16,7 @@ * 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.sdk.server.LDConfig.Builder#dataSource(DataSourceFactory)}: + * change its properties with the methods of this class, and pass it to {@link Builder#dataSource(ComponentConfigurer)}: *


  *     LDConfig config = new LDConfig.Builder()
  *         .dataSource(Components.pollingDataSource().pollInterval(Duration.ofSeconds(45)))
@@ -26,41 +27,14 @@
  * 
  * @since 4.12.0
  */
-public abstract class PollingDataSourceBuilder implements DataSourceFactory {
+public abstract class PollingDataSourceBuilder implements ComponentConfigurer {
   /**
    * The default and minimum value for {@link #pollInterval(Duration)}: 30 seconds.
    */
   public static final Duration DEFAULT_POLL_INTERVAL = Duration.ofSeconds(30);
   
-  protected URI baseURI;
   protected Duration pollInterval = DEFAULT_POLL_INTERVAL;
-
-  /**
-   * Deprecated method for setting a custom base URI for the polling service.
-   * 

- * The preferred way to set this option is now with - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. - * If you set this deprecated option, it overrides any value that was set with - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. - *

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

    - *
  • You are using the Relay Proxy. Set - * {@code streamUri} to the base URI of the Relay Proxy instance. - *
  • You are connecting to a test server or anything else other than the standard LaunchDarkly service. - *
- * - * @param baseURI the base URI of the polling service; null to use the default - * @return the builder - * @deprecated Use {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)} and - * {@link ServiceEndpointsBuilder#polling(URI)}. - */ - @Deprecated - public PollingDataSourceBuilder baseURI(URI baseURI) { - this.baseURI = baseURI; - return this; - } - + /** * Sets the interval at which the SDK will poll for feature flag updates. *

diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilder.java b/src/main/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilder.java index 24ed254a2..25b7654cf 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilder.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilder.java @@ -1,9 +1,10 @@ package com.launchdarkly.sdk.server.integrations; import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; +import com.launchdarkly.sdk.server.LDConfig.Builder; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; -import java.net.URI; import java.time.Duration; /** @@ -11,7 +12,7 @@ *

* 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.sdk.server.LDConfig.Builder#dataSource(DataSourceFactory)}: + * change its properties with the methods of this class, and pass it to {@link Builder#dataSource(ComponentConfigurer)}: *


  *     LDConfig config = new LDConfig.Builder()
  *         .dataSource(Components.streamingDataSource().initialReconnectDelayMillis(500))
@@ -22,41 +23,14 @@
  * 
  * @since 4.12.0
  */
-public abstract class StreamingDataSourceBuilder implements DataSourceFactory {
+public abstract class StreamingDataSourceBuilder implements ComponentConfigurer {
   /**
    * The default value for {@link #initialReconnectDelay(Duration)}: 1000 milliseconds.
    */
   public static final Duration DEFAULT_INITIAL_RECONNECT_DELAY = Duration.ofMillis(1000);
   
-  protected URI baseURI;
   protected Duration initialReconnectDelay = DEFAULT_INITIAL_RECONNECT_DELAY;
 
-  /**
-   * Deprecated method for setting a custom base URI for the streaming service.
-   * 

- * The preferred way to set this option is now with - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. - * If you set this deprecated option, it overrides any value that was set with - * {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. - *

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

    - *
  • You are using the Relay Proxy. Set - * {@code baseUri} to the base URI of the Relay Proxy instance. - *
  • You are connecting to a test server or a nonstandard endpoint for the LaunchDarkly service. - *
- * - * @param baseURI the base URI of the streaming service; null to use the default - * @return the builder - * @deprecated Use {@link com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)} and - * {@link ServiceEndpointsBuilder#streaming(URI)}. - */ - @Deprecated - public StreamingDataSourceBuilder baseURI(URI baseURI) { - this.baseURI = baseURI; - return this; - } - /** * Sets the initial reconnect delay for the streaming connection. *

@@ -74,19 +48,4 @@ public StreamingDataSourceBuilder initialReconnectDelay(Duration initialReconnec this.initialReconnectDelay = initialReconnectDelay == null ? DEFAULT_INITIAL_RECONNECT_DELAY : initialReconnectDelay; return this; } - - /** - * Obsolete method for setting a different custom base URI for special polling requests. - *

- * Previously, LaunchDarkly sometimes required the SDK to temporarily do a polling request even in - * streaming mode (based on the size of the updated data item); this property specified the base URI - * for such requests. However, the system no longer has this behavior so this property is ignored. - * It will be deprecated and then removed in a future release. - * - * @param pollingBaseURI the polling endpoint URI; null to use the default - * @return the builder - */ - public StreamingDataSourceBuilder pollingBaseURI(URI pollingBaseURI) { - return this; - } } diff --git a/src/main/java/com/launchdarkly/sdk/server/integrations/TestData.java b/src/main/java/com/launchdarkly/sdk/server/integrations/TestData.java index 488ad74cf..268ade0d6 100644 --- a/src/main/java/com/launchdarkly/sdk/server/integrations/TestData.java +++ b/src/main/java/com/launchdarkly/sdk/server/integrations/TestData.java @@ -5,19 +5,20 @@ import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Iterables; import com.launchdarkly.sdk.ArrayBuilder; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataSource; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import java.io.IOException; import java.util.ArrayList; @@ -63,7 +64,7 @@ * @since 5.1.0 * @see FileData */ -public final class TestData implements DataSourceFactory { +public final class TestData implements ComponentConfigurer { private final Object lock = new Object(); private final Map currentFlags = new HashMap<>(); private final Map currentBuilders = new HashMap<>(); @@ -167,13 +168,9 @@ public TestData updateStatus(DataSourceStatusProvider.State newState, DataSource return this; } - /** - * Called internally by the SDK to associate this test data source with an {@code LDClient} instance. - * You do not need to call this method. - */ @Override - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) { - DataSourceImpl instance = new DataSourceImpl(dataSourceUpdates); + public DataSource build(ClientContext context) { + DataSourceImpl instance = new DataSourceImpl(context.getDataSourceUpdateSink()); synchronized (lock) { instances.add(instance); } @@ -209,8 +206,8 @@ public static final class FlagBuilder { boolean on; int fallthroughVariation; CopyOnWriteArrayList variations; - Map> targets; - List rules; + final Map>> targets = new TreeMap<>(); // TreeMap enforces ordering for test determinacy + final List rules = new ArrayList<>(); private FlagBuilder(String key) { this.key = key; @@ -224,8 +221,10 @@ private FlagBuilder(FlagBuilder from) { this.on = from.on; this.fallthroughVariation = from.fallthroughVariation; this.variations = new CopyOnWriteArrayList<>(from.variations); - this.targets = from.targets == null ? null : new HashMap<>(from.targets); - this.rules = from.rules == null ? null : new ArrayList<>(from.rules); + for (ContextKind contextKind: from.targets.keySet()) { + this.targets.put(contextKind, new TreeMap<>(from.targets.get(contextKind))); + } + this.rules.addAll(from.rules); } private boolean isBooleanFlag() { @@ -271,7 +270,7 @@ public FlagBuilder on(boolean on) { /** * Specifies the fallthrough variation for a boolean flag. The fallthrough is the value - * that is returned if targeting is on and the user was not matched by a more specific + * that is returned if targeting is on and the context was not matched by a more specific * target or rule. *

* If the flag was previously configured with other variations, this also changes it to a @@ -286,7 +285,7 @@ public FlagBuilder fallthroughVariation(boolean value) { /** * Specifies the index of the fallthrough variation. The fallthrough is the variation - * that is returned if targeting is on and the user was not matched by a more specific + * that is returned if targeting is on and the context was not matched by a more specific * target or rule. * * @param variationIndex the desired fallthrough variation: 0 for the first, 1 for the second, etc. @@ -321,14 +320,16 @@ public FlagBuilder offVariation(int variationIndex) { } /** - * Sets the flag to always return the specified boolean variation for all users. + * Sets the flag to always return the specified boolean variation for all contexts. *

* Targeting is switched on, any existing targets or rules are removed, and the flag's variations are * set to true and false. The fallthrough variation is set to the specified value. The off variation is * left unchanged. * - * @param variation the desired true/false variation to be returned for all users + * @param variation the desired true/false variation to be returned for all contexts * @return the builder + * @see #variationForAll(int) + * @see #valueForAll(LDValue) * @since 5.10.0 */ public FlagBuilder variationForAll(boolean variation) { @@ -336,7 +337,7 @@ public FlagBuilder variationForAll(boolean variation) { } /** - * Sets the flag to always return the specified variation for all users. + * Sets the flag to always return the specified variation for all contexts. *

* The variation is specified by number, out of whatever variation values have already been * defined. Targeting is switched on, and any existing targets or rules are removed. The fallthrough @@ -344,40 +345,11 @@ public FlagBuilder variationForAll(boolean variation) { * * @param variationIndex the desired variation: 0 for the first, 1 for the second, etc. * @return the builder - * @since 5.10.0 + * @see #variationForAll(boolean) + * @see #valueForAll(LDValue) */ public FlagBuilder variationForAll(int variationIndex) { - return on(true).clearRules().clearUserTargets().fallthroughVariation(variationIndex); - } - - /** - * Deprecated name for {@link #variationForAll(boolean)}. - *

- * This method name will be dropped in a future SDK version because "users" will not always be the - * only kind of input for an evaluation. - * - * @param variation the desired true/false variation to be returned for all users - * @return the builder - * @deprecated Use {@link #variationForAll(boolean)}. - */ - @Deprecated - public FlagBuilder variationForAllUsers(boolean variation) { - return variationForAll(variation); - } - - /** - * Deprecated name for {@link #variationForAll(int)}. - *

- * This method name will be dropped in a future SDK version because "users" will not always be the - * only kind of input for an evaluation. - * - * @param variationIndex the desired variation: 0 for the first, 1 for the second, etc. - * @return the builder - * @deprecated Use {@link #variationForAll(int)}. - */ - @Deprecated - public FlagBuilder variationForAllUsers(int variationIndex) { - return variationForAll(variationIndex); + return on(true).clearRules().clearTargets().fallthroughVariation(variationIndex); } /** @@ -390,6 +362,8 @@ public FlagBuilder variationForAllUsers(int variationIndex) { * * @param value the desired value to be returned for all users * @return the builder + * @see #variationForAll(boolean) + * @see #variationForAll(int) */ public FlagBuilder valueForAll(LDValue value) { variations.clear(); @@ -398,40 +372,48 @@ public FlagBuilder valueForAll(LDValue value) { } /** - * Deprecated name for {@link #valueForAll(LDValue)}. + * Sets the flag to return the specified boolean variation for a specific user key (that is, + * for a context with that key whose context kind is "user") when targeting is on. + *

+ * This has no effect when targeting is turned off for the flag. *

- * This method name will be dropped in a future SDK version because "users" will not always be the - * only kind of input for an evaluation. + * If the flag was not already a boolean flag, this also changes it to a boolean flag. * - * @param value the desired value to be returned for all users - * @return the builder' - * @deprecated Use {@link #valueForAll(LDValue)}. + * @param userKey a user key + * @param variation the desired true/false variation to be returned for this user when + * targeting is on + * @return the builder + * @see #variationForUser(String, int) + * @see #variationForKey(ContextKind, String, boolean) */ - @Deprecated - public FlagBuilder valueForAllUsers(LDValue value) { - return valueForAll(value); + public FlagBuilder variationForUser(String userKey, boolean variation) { + return variationForKey(ContextKind.DEFAULT, userKey, variation); } - + /** - * Sets the flag to return the specified boolean variation for a specific user key when - * targeting is on. + * Sets the flag to return the specified boolean variation for a specific context, identified + * by context kind and key, when targeting is on. *

* This has no effect when targeting is turned off for the flag. *

* If the flag was not already a boolean flag, this also changes it to a boolean flag. * - * @param userKey a user key - * @param variation the desired true/false variation to be returned for this user when + * @param contextKind the context kind + * @param key the context key + * @param variation the desired true/false variation to be returned for this context when * targeting is on * @return the builder + * @see #variationForKey(ContextKind, String, int) + * @see #variationForUser(String, boolean) + * @since 6.0.0 */ - public FlagBuilder variationForUser(String userKey, boolean variation) { - return booleanFlag().variationForUser(userKey, variationForBoolean(variation)); + public FlagBuilder variationForKey(ContextKind contextKind, String key, boolean variation) { + return booleanFlag().variationForKey(contextKind, key, variationForBoolean(variation)); } /** - * Sets the flag to return the specified variation for a specific user key when targeting - * is on. + * Sets the flag to return the specified variation for a specific user key (that is, + * for a context with that key whose context kind is "user") when targeting is on. *

* This has no effect when targeting is turned off for the flag. *

@@ -442,29 +424,57 @@ public FlagBuilder variationForUser(String userKey, boolean variation) { * @param variationIndex the desired variation to be returned for this user when targeting is on: * 0 for the first, 1 for the second, etc. * @return the builder + * @see #variationForKey(ContextKind, String, int) + * @see #variationForUser(String, boolean) */ public FlagBuilder variationForUser(String userKey, int variationIndex) { - if (targets == null) { - targets = new TreeMap<>(); // TreeMap keeps variations in order for test determinacy + return variationForKey(ContextKind.DEFAULT, userKey, variationIndex); + } + + /** + * Sets the flag to return the specified boolean variation for a specific context, identified + * by context kind and key, when targeting is on. + *

+ * This has no effect when targeting is turned off for the flag. + *

+ * If the flag was not already a boolean flag, this also changes it to a boolean flag. + * + * @param contextKind the context kind + * @param key the context key + * @param variationIndex the desired variation to be returned for this context when targeting is on: + * 0 for the first, 1 for the second, etc. + * @return the builder + * @see #variationForKey(ContextKind, String, boolean) + * @see #variationForUser(String, int) + * @since 6.0.0 + */ + public FlagBuilder variationForKey(ContextKind contextKind, String key, int variationIndex) { + if (contextKind == null) { + contextKind = ContextKind.DEFAULT; + } + Map> keysByVariation = targets.get(contextKind); + if (keysByVariation == null) { + keysByVariation = new TreeMap<>(); // TreeMap keeps variations in order for test determinacy + targets.put(contextKind, keysByVariation); } for (int i = 0; i < variations.size(); i++) { - ImmutableSet keys = targets.get(i); + ImmutableSet keys = keysByVariation.get(i); if (i == variationIndex) { if (keys == null) { - targets.put(i, ImmutableSortedSet.of(userKey)); - } else if (!keys.contains(userKey)) { - targets.put(i, ImmutableSortedSet.naturalOrder().addAll(keys).add(userKey).build()); + keysByVariation.put(i, ImmutableSortedSet.of(key)); + } else if (!keys.contains(key)) { + keysByVariation.put(i, ImmutableSortedSet.naturalOrder().addAll(keys).add(key).build()); } } else { - if (keys != null && keys.contains(userKey)) { - targets.put(i, ImmutableSortedSet.copyOf(Iterables.filter(keys, k -> !k.equals(userKey)))); + if (keys != null && keys.contains(key)) { + keysByVariation.put(i, ImmutableSortedSet.copyOf(Iterables.filter(keys, k -> !k.equals(key)))); } } } // Note, we use ImmutableSortedSet just to make the output determinate for our own testing return this; } - + /** * Changes the allowable variation values for the flag. *

@@ -482,15 +492,45 @@ public FlagBuilder variations(LDValue... values) { } return this; } + + /** + * Starts defining a flag rule, using the "is one of" operator. This matching expression only + * applies to contexts of a specific kind. + *

+ * For example, this creates a rule that returns {@code true} if the name attribute for the + * "company" context is "Ella" or "Monsoon": + * + *


+     *     testData.flag("flag")
+     *         .ifMatch(ContextKind.of("company"), "name",
+     *             LDValue.of("Ella"), LDValue.of("Monsoon"))
+     *         .thenReturn(true));
+     * 
+ * + * @param contextKind the context kind to match + * @param attribute the attribute to match against + * @param values values to compare to + * @return a {@link FlagRuleBuilder}; call {@link FlagRuleBuilder#thenReturn(boolean)} or + * {@link FlagRuleBuilder#thenReturn(int)} to finish the rule, or add more tests with another + * method like {@link FlagRuleBuilder#andMatch(ContextKind, String, LDValue...)} + * @see #ifMatch(String, LDValue...) + * @see #ifNotMatch(ContextKind, String, LDValue...) + * @since 6.0.0 + */ + public FlagRuleBuilder ifMatch(ContextKind contextKind, String attribute, LDValue... values) { + return new FlagRuleBuilder().andMatch(contextKind, attribute, values); + } /** - * Starts defining a flag rule, using the "is one of" operator. + * Starts defining a flag rule, using the "is one of" operator. This is a shortcut for calling + * {@link #ifMatch(ContextKind, String, LDValue...)} with {@link ContextKind#DEFAULT} as the + * context kind. *

* For example, this creates a rule that returns {@code true} if the name is "Patsy" or "Edina": * *


      *     testData.flag("flag")
-     *         .ifMatch(UserAttribute.NAME, LDValue.of("Patsy"), LDValue.of("Edina"))
+     *         .ifMatch("name", LDValue.of("Patsy"), LDValue.of("Edina"))
      *         .thenReturn(true));
      * 
* @@ -498,20 +538,52 @@ public FlagBuilder variations(LDValue... values) { * @param values values to compare to * @return a {@link FlagRuleBuilder}; call {@link FlagRuleBuilder#thenReturn(boolean)} or * {@link FlagRuleBuilder#thenReturn(int)} to finish the rule, or add more tests with another - * method like {@link FlagRuleBuilder#andMatch(UserAttribute, LDValue...)} + * method like {@link FlagRuleBuilder#andMatch(String, LDValue...)} + * @see #ifMatch(ContextKind, String, LDValue...) + * @see #ifNotMatch(String, LDValue...) */ - public FlagRuleBuilder ifMatch(UserAttribute attribute, LDValue... values) { - return new FlagRuleBuilder().andMatch(attribute, values); + public FlagRuleBuilder ifMatch(String attribute, LDValue... values) { + return ifMatch(ContextKind.DEFAULT, attribute, values); + } + + /** + * Starts defining a flag rule, using the "is not one of" operator. This matching expression only + * applies to contexts of a specific kind. + *

+ * For example, this creates a rule that returns {@code true} if the name attribute for the + * "company" context is neither "Pendant" nor "Sterling Cooper": + * + *


+     *     testData.flag("flag")
+     *         .ifNotMatch(ContextKind.of("company"), "name",
+     *             LDValue.of("Pendant"), LDValue.of("Sterling Cooper"))
+     *         .thenReturn(true));
+     * 
+ * + * @param contextKind the context kind to match + * @param attribute the attribute to match against + * @param values values to compare to + * @return a {@link FlagRuleBuilder}; call {@link FlagRuleBuilder#thenReturn(boolean)} or + * {@link FlagRuleBuilder#thenReturn(int)} to finish the rule, or add more tests with another + * method like {@link FlagRuleBuilder#andMatch(ContextKind, String, LDValue...)} + * @see #ifMatch(ContextKind, String, LDValue...) + * @see #ifNotMatch(String, LDValue...) + * @since 6.0.0 + */ + public FlagRuleBuilder ifNotMatch(ContextKind contextKind, String attribute, LDValue... values) { + return new FlagRuleBuilder().andNotMatch(contextKind, attribute, values); } /** - * Starts defining a flag rule, using the "is not one of" operator. + * Starts defining a flag rule, using the "is not one of" operator. This is a shortcut for calling + * {@link #ifNotMatch(ContextKind, String, LDValue...)} with {@link ContextKind#DEFAULT} as the + * context kind. *

* For example, this creates a rule that returns {@code true} if the name is neither "Saffron" nor "Bubble": * *


      *     testData.flag("flag")
-     *         .ifNotMatch(UserAttribute.NAME, LDValue.of("Saffron"), LDValue.of("Bubble"))
+     *         .ifNotMatch("name", LDValue.of("Saffron"), LDValue.of("Bubble"))
      *         .thenReturn(true));
      * 
@@ -519,31 +591,33 @@ public FlagRuleBuilder ifMatch(UserAttribute attribute, LDValue... values) { * @param values values to compare to * @return a {@link FlagRuleBuilder}; call {@link FlagRuleBuilder#thenReturn(boolean)} or * {@link FlagRuleBuilder#thenReturn(int)} to finish the rule, or add more tests with another - * method like {@link FlagRuleBuilder#andMatch(UserAttribute, LDValue...)} + * method like {@link FlagRuleBuilder#andMatch(String, LDValue...)} + * @see #ifNotMatch(ContextKind, String, LDValue...) + * @see #ifMatch(String, LDValue...) */ - public FlagRuleBuilder ifNotMatch(UserAttribute attribute, LDValue... values) { - return new FlagRuleBuilder().andNotMatch(attribute, values); + public FlagRuleBuilder ifNotMatch(String attribute, LDValue... values) { + return ifNotMatch(ContextKind.DEFAULT, attribute, values); } - + /** * Removes any existing rules from the flag. This undoes the effect of methods like - * {@link #ifMatch(UserAttribute, LDValue...)}. + * {@link #ifMatch(String, LDValue...)}. * * @return the same builder */ public FlagBuilder clearRules() { - rules = null; + rules.clear(); return this; } /** - * Removes any existing user targets from the flag. This undoes the effect of methods like + * Removes any existing user/context targets from the flag. This undoes the effect of methods like * {@link #variationForUser(String, boolean)}. * * @return the same builder */ - public FlagBuilder clearUserTargets() { - targets = null; + public FlagBuilder clearTargets() { + targets.clear(); return this; } @@ -554,25 +628,49 @@ ItemDescriptor createFlag(int version) { .put("on", on) .put("offVariation", offVariation) .put("fallthrough", LDValue.buildObject().put("variation", fallthroughVariation).build()); + + // The following properties shouldn't actually be used in evaluations of this flag, but + // adding them makes the JSON output more predictable for tests + builder.put("prerequisites", LDValue.arrayOf()) + .put("salt", ""); + ArrayBuilder jsonVariations = LDValue.buildArray(); for (LDValue v: variations) { jsonVariations.add(v); } builder.put("variations", jsonVariations.build()); - if (targets != null) { - ArrayBuilder jsonTargets = LDValue.buildArray(); - for (Map.Entry> e: targets.entrySet()) { - jsonTargets.add(LDValue.buildObject() - .put("variation", e.getKey().intValue()) - .put("values", LDValue.Convert.String.arrayFrom(e.getValue())) - .build()); + ArrayBuilder jsonTargets = LDValue.buildArray(); + ArrayBuilder jsonContextTargets = LDValue.buildArray(); + if (!targets.isEmpty()) { + if (targets.get(ContextKind.DEFAULT) != null) { + for (Map.Entry> e: targets.get(ContextKind.DEFAULT).entrySet()) { + if (!e.getValue().isEmpty()) { + jsonTargets.add(LDValue.buildObject() + .put("variation", e.getKey().intValue()) + .put("values", LDValue.Convert.String.arrayFrom(e.getValue())) + .build()); + } + } + } + for (ContextKind contextKind: targets.keySet()) { + for (Map.Entry> e: targets.get(contextKind).entrySet()) { + if (!e.getValue().isEmpty()) { + jsonContextTargets.add(LDValue.buildObject() + .put("contextKind", contextKind.toString()) + .put("variation", e.getKey().intValue()) + .put("values", contextKind.isDefault() ? LDValue.arrayOf() : + LDValue.Convert.String.arrayFrom(e.getValue())) + .build()); + } + } } - builder.put("targets", jsonTargets.build()); } + builder.put("targets", jsonTargets.build()); + builder.put("contextTargets", jsonContextTargets.build()); - if (rules != null) { - ArrayBuilder jsonRules = LDValue.buildArray(); + ArrayBuilder jsonRules = LDValue.buildArray(); + if (!rules.isEmpty()) { int ri = 0; for (FlagRuleBuilder r: rules) { ArrayBuilder jsonClauses = LDValue.buildArray(); @@ -582,7 +680,8 @@ ItemDescriptor createFlag(int version) { jsonValues.add(v); } jsonClauses.add(LDValue.buildObject() - .put("attribute", c.attribute.getName()) + .put("contextKind", c.contextKind == null ? null : c.contextKind.toString()) + .put("attribute", c.attribute.toString()) .put("op", c.operator) .put("values", jsonValues.build()) .put("negate", c.negate) @@ -595,8 +694,8 @@ ItemDescriptor createFlag(int version) { .build()); ri++; } - builder.put("rules", jsonRules.build()); } + builder.put("rules", jsonRules.build()); String json = builder.build().toJsonString(); return DataModel.FEATURES.deserialize(json); @@ -614,37 +713,97 @@ private static int variationForBoolean(boolean value) { * rule's clauses match the user. *

* To start defining a rule, use one of the flag builder's matching methods such as - * {@link FlagBuilder#ifMatch(UserAttribute, LDValue...)}. This defines the first clause for the rule. + * {@link FlagBuilder#ifMatch(String, LDValue...)}. This defines the first clause for the rule. * Optionally, you may add more clauses with the rule builder's methods such as - * {@link #andMatch(UserAttribute, LDValue...)}. Finally, call {@link #thenReturn(boolean)} or + * {@link #andMatch(String, LDValue...)}. Finally, call {@link #thenReturn(boolean)} or * {@link #thenReturn(int)} to finish defining the rule. */ public final class FlagRuleBuilder { final List clauses = new ArrayList<>(); int variation; - + /** - * Adds another clause, using the "is one of" operator. + * Adds another clause, using the "is one of" operator. This matching expression only + * applies to contexts of a specific kind. + *

+ * For example, this creates a rule that returns {@code true} if the name attribute for the + * "company" context is "Ella" and the country is "gb": + * + *


+       *     testData.flag("flag")
+       *         .ifMatch(ContextKind.of("company"), "name", LDValue.of("Ella"))
+       *         .andMatch(ContextKind.of("company"), "country", LDValue.of("gb"))
+       *         .thenReturn(true));
+       * 
+ * + * @param contextKind the context kind to match + * @param attribute the attribute to match against + * @param values values to compare to + * @return the rule builder + * @see #andNotMatch(ContextKind, String, LDValue...) + * @see #andMatch(String, LDValue...) + * @since 6.0.0 + */ + public FlagRuleBuilder andMatch(ContextKind contextKind, String attribute, LDValue... values) { + if (attribute != null) { + clauses.add(new Clause(contextKind, AttributeRef.fromPath(attribute), "in", values, false)); + } + return this; + } + + /** + * Adds another clause, using the "is one of" operator. This is a shortcut for calling + * {@link #andMatch(ContextKind, String, LDValue...)} with {@link ContextKind#DEFAULT} as the context kind. *

* For example, this creates a rule that returns {@code true} if the name is "Patsy" and the * country is "gb": * *


        *     testData.flag("flag")
-       *         .ifMatch(UserAttribute.NAME, LDValue.of("Patsy"))
-       *         .andMatch(UserAttribute.COUNTRY, LDValue.of("gb"))
+       *         .ifMatch("name", LDValue.of("Patsy"))
+       *         .andMatch("country", LDValue.of("gb"))
        *         .thenReturn(true));
        * 
* * @param attribute the user attribute to match against * @param values values to compare to * @return the rule builder + * @see #andNotMatch(String, LDValue...) + * @see #andMatch(ContextKind, String, LDValue...) */ - public FlagRuleBuilder andMatch(UserAttribute attribute, LDValue... values) { - clauses.add(new Clause(attribute, "in", values, false)); - return this; + public FlagRuleBuilder andMatch(String attribute, LDValue... values) { + return andMatch(ContextKind.DEFAULT, attribute, values); } + /** + * Adds another clause, using the "is not one of" operator. This matching expression only + * applies to contexts of a specific kind. + *

+ * For example, this creates a rule that returns {@code true} if the name attribute for the + * "company" context is "Ella" and the country is not "gb": + * + *


+       *     testData.flag("flag")
+       *         .ifMatch(ContextKind.of("company"), "name", LDValue.of("Ella"))
+       *         .andNotMatch(ContextKind.of("company"), "country", LDValue.of("gb"))
+       *         .thenReturn(true));
+       * 
+ * + * @param contextKind the context kind to match + * @param attribute the user attribute to match against + * @param values values to compare to + * @return the rule builder + * @see #andMatch(ContextKind, String, LDValue...) + * @see #andNotMatch(String, LDValue...) + * @since 6.0.0 + */ + public FlagRuleBuilder andNotMatch(ContextKind contextKind, String attribute, LDValue... values) { + if (attribute != null) { + clauses.add(new Clause(contextKind, AttributeRef.fromPath(attribute), "in", values, true)); + } + return this; + } + /** * Adds another clause, using the "is not one of" operator. *

@@ -653,20 +812,21 @@ public FlagRuleBuilder andMatch(UserAttribute attribute, LDValue... values) { * *


        *     testData.flag("flag")
-       *         .ifMatch(UserAttribute.NAME, LDValue.of("Patsy"))
-       *         .andNotMatch(UserAttribute.COUNTRY, LDValue.of("gb"))
+       *         .ifMatch("name", LDValue.of("Patsy"))
+       *         .andNotMatch("country", LDValue.of("gb"))
        *         .thenReturn(true));
        * 
* * @param attribute the user attribute to match against * @param values values to compare to * @return the rule builder + * @see #andMatch(String, LDValue...) + * @see #andNotMatch(ContextKind, String, LDValue...) */ - public FlagRuleBuilder andNotMatch(UserAttribute attribute, LDValue... values) { - clauses.add(new Clause(attribute, "in", values, true)); - return this; + public FlagRuleBuilder andNotMatch(String attribute, LDValue... values) { + return andNotMatch(ContextKind.DEFAULT, attribute, values); } - + /** * Finishes defining the rule, specifying the result value as a boolean. * @@ -687,21 +847,20 @@ public FlagBuilder thenReturn(boolean variation) { */ public FlagBuilder thenReturn(int variationIndex) { this.variation = variationIndex; - if (FlagBuilder.this.rules == null) { - FlagBuilder.this.rules = new ArrayList<>(); - } FlagBuilder.this.rules.add(this); return FlagBuilder.this; } } private static final class Clause { - final UserAttribute attribute; + final ContextKind contextKind; + final AttributeRef attribute; final String operator; final LDValue[] values; final boolean negate; - Clause(UserAttribute attribute, String operator, LDValue[] values, boolean negate) { + Clause(ContextKind contextKind, AttributeRef attribute, String operator, LDValue[] values, boolean negate) { + this.contextKind = contextKind; this.attribute = attribute; this.operator = operator; this.values = values; @@ -711,9 +870,9 @@ private static final class Clause { } private final class DataSourceImpl implements DataSource { - final DataSourceUpdates updates; + final DataSourceUpdateSink updates; - DataSourceImpl(DataSourceUpdates updates) { + DataSourceImpl(DataSourceUpdateSink updates) { this.updates = updates; } diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/BasicConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/BasicConfiguration.java deleted file mode 100644 index 238dd6a3a..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/BasicConfiguration.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.sdk.server.Components; - -/** - * The most basic properties of the SDK client that are available to all SDK component factories. - * - * @since 5.0.0 - */ -public final class BasicConfiguration { - private final String sdkKey; - private final boolean offline; - private final int threadPriority; - private final ApplicationInfo applicationInfo; - private final ServiceEndpoints serviceEndpoints; - private final LDLogger baseLogger; - - /** - * Constructs an instance. - * - * @param sdkKey the SDK key - * @param offline true if the SDK was configured to be completely offline - * @param threadPriority the thread priority that should be used for any worker threads created by SDK components - * @param applicationInfo metadata about the application using this SDK - * @param serviceEndpoints the SDK's service URIs - * @param baseLogger the base logger - * @since 5.10.0 - */ - public BasicConfiguration( - String sdkKey, - boolean offline, - int threadPriority, - ApplicationInfo applicationInfo, - ServiceEndpoints serviceEndpoints, - LDLogger baseLogger - ) { - this.sdkKey = sdkKey; - this.offline = offline; - this.threadPriority = threadPriority; - this.applicationInfo = applicationInfo; - this.serviceEndpoints = serviceEndpoints != null ? serviceEndpoints : Components.serviceEndpoints().createServiceEndpoints(); - this.baseLogger = baseLogger != null ? baseLogger : LDLogger.none(); - } - - /** - * Constructs an instance. - * - * @param sdkKey the SDK key - * @param offline true if the SDK was configured to be completely offline - * @param threadPriority the thread priority that should be used for any worker threads created by SDK components - * @param applicationInfo metadata about the application using this SDK - * @param serviceEndpoints the SDK's service URIs - */ - public BasicConfiguration( - String sdkKey, - boolean offline, - int threadPriority, - ApplicationInfo applicationInfo, - ServiceEndpoints serviceEndpoints - ) { - this(sdkKey, offline, threadPriority, applicationInfo, serviceEndpoints, null); - } - - /** - * Constructs an instance. - * - * @param sdkKey the SDK key - * @param offline true if the SDK was configured to be completely offline - * @param threadPriority the thread priority that should be used for any worker threads created by SDK components - * @param applicationInfo metadata about the application using this SDK - * @deprecated Use {@link BasicConfiguration#BasicConfiguration(String, boolean, int, ApplicationInfo, ServiceEndpoints)} - */ - @Deprecated - public BasicConfiguration(String sdkKey, boolean offline, int threadPriority, ApplicationInfo applicationInfo) { - this(sdkKey, offline, threadPriority, applicationInfo, null, null); - } - - /** - * Constructs an instance. - * - * @param sdkKey the SDK key - * @param offline true if the SDK was configured to be completely offline - * @param threadPriority the thread priority that should be used for any worker threads created by SDK components - * @deprecated Use {@link BasicConfiguration#BasicConfiguration(String, boolean, int, ApplicationInfo, ServiceEndpoints)} - */ - @Deprecated - public BasicConfiguration(String sdkKey, boolean offline, int threadPriority) { - this(sdkKey, offline, threadPriority, null, null); - } - - /** - * Returns the configured SDK key. - * - * @return the SDK key - */ - public String getSdkKey() { - return sdkKey; - } - - /** - * Returns true if the client was configured to be completely offline. - * - * @return true if offline - * @see com.launchdarkly.sdk.server.LDConfig.Builder#offline(boolean) - */ - public boolean isOffline() { - return offline; - } - - /** - * The thread priority that should be used for any worker threads created by SDK components. - * - * @return the thread priority - * @see com.launchdarkly.sdk.server.LDConfig.Builder#threadPriority(int) - */ - public int getThreadPriority() { - return threadPriority; - } - - /** - * The metadata about the application using this SDK. - * - * @return the application info - * @see com.launchdarkly.sdk.server.LDConfig.Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder) - */ - public ApplicationInfo getApplicationInfo() { - return applicationInfo; - } - - /** - * Returns the base service URIs used by SDK components. - * - * @return the service endpoints - * @see com.launchdarkly.sdk.server.LDConfig.Builder#serviceEndpoints(com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder) - */ - public ServiceEndpoints getServiceEndpoints() { - return serviceEndpoints; - } - - /** - * Returns the base logger used by SDK components. Suffixes may be added to the logger name for - * specific areas of functionality. - * - * @return the base logger - * @see com.launchdarkly.sdk.server.LDConfig.Builder#logging(LoggingConfigurationFactory) - * @since 5.10.0 - */ - public LDLogger getBaseLogger() { - return baseLogger; - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStoreFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStoreFactory.java deleted file mode 100644 index 5e6a16cbc..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStoreFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -/** - * Interface for a factory that creates some implementation of {@link BigSegmentStore}. - * - * @see com.launchdarkly.sdk.server.Components#bigSegments(BigSegmentStoreFactory) - * @see com.launchdarkly.sdk.server.LDConfig.Builder#bigSegments(com.launchdarkly.sdk.server.integrations.BigSegmentsConfigurationBuilder) - * @since 5.7.0 - */ -public interface BigSegmentStoreFactory { - /** - * Called internally by the SDK to create an implementation instance. Applications do not need to - * call this method. - * - * @param context allows access to the client configuration - * @return a {@link BigSegmentStore} instance - */ - BigSegmentStore createBigSegmentStore(ClientContext context); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentsConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentsConfiguration.java index c2bd2cfde..965737dbe 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentsConfiguration.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentsConfiguration.java @@ -1,6 +1,7 @@ package com.launchdarkly.sdk.server.interfaces; import com.launchdarkly.sdk.server.integrations.BigSegmentsConfigurationBuilder; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; import java.time.Duration; diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/ClientContext.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/ClientContext.java deleted file mode 100644 index e5db5fc5f..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/ClientContext.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -/** - * Context information provided by the {@link com.launchdarkly.sdk.server.LDClient} when creating components. - *

- * This is passed as a parameter to {@link DataStoreFactory#createDataStore(ClientContext, DataStoreUpdates)}, - * etc. Component factories do not receive the entire {@link com.launchdarkly.sdk.server.LDConfig} because - * it could contain factory objects that have mutable state, and because components should not be able - * to access the configurations of unrelated components. - *

- * The actual implementation class may contain other properties that are only relevant to the built-in - * SDK components and are therefore not part of the public interface; this allows the SDK to add its own - * context information as needed without disturbing the public API. - * - * @since 5.0.0 - */ -public interface ClientContext { - /** - * The SDK's basic global properties. - * - * @return the basic configuration - */ - public BasicConfiguration getBasic(); - - /** - * The configured networking properties that apply to all components. - * - * @return the HTTP configuration - */ - public HttpConfiguration getHttp(); - - /** - * The configured logging properties that apply to all components. - * @return the logging configuration - */ - public LoggingConfiguration getLogging(); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceFactory.java deleted file mode 100644 index f29ef7846..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceFactory.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.sdk.server.Components; - -/** - * Interface for a factory that creates some implementation of {@link DataSource}. - * @see Components - * @since 4.11.0 - */ -public interface DataSourceFactory { - /** - * Creates an implementation instance. - *

- * The new {@code DataSource} should not attempt to make any connections until - * {@link DataSource#start()} is called. - * - * @param context allows access to the client configuration - * @param dataSourceUpdates the component pushes data into the SDK via this interface - * @return an {@link DataSource} - */ - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceStatusProvider.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceStatusProvider.java index 819efeb51..a3eab8a3a 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceStatusProvider.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceStatusProvider.java @@ -7,7 +7,7 @@ import java.util.Objects; /** - * An interface for querying the status of a {@link DataSource}. The data source is the component + * An interface for querying the status of the SDK's data source. The data source is the component * that receives updates to feature flag data; normally this is a streaming connection, but it could * be polling or file data depending on your configuration. *

@@ -23,8 +23,8 @@ public interface DataSourceStatusProvider { * All of the built-in data source implementations are guaranteed to update this status whenever they * successfully initialize, encounter an error, or recover after an error. *

- * For a custom data source implementation, it is the responsibility of the data source to report its - * status via {@link DataSourceUpdates}; if it does not do so, the status will always be reported as + * For a custom data source implementation, it is the responsibility of the data source to push + * status updates to the SDK; if it does not do so, the status will always be reported as * {@link State#INITIALIZING}. * * @return the latest status; will never be null @@ -157,7 +157,7 @@ public static enum ErrorKind { * store failed (so the SDK may not have the latest data). *

* Data source implementations do not need to report this kind of error; it will be automatically - * reported by the SDK whenever one of the update methods of {@link DataSourceUpdates} throws an exception. + * reported by the SDK when exceptions are detected. */ STORE_ERROR } diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreFactory.java deleted file mode 100644 index adfb1e06e..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreFactory.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.sdk.server.Components; - -/** - * Interface for a factory that creates some implementation of {@link DataStore}. - * @see Components - * @since 4.11.0 - */ -public interface DataStoreFactory { - /** - * Creates an implementation instance. - * - * @param context allows access to the client configuration - * @param dataStoreUpdates the data store can use this object to report information back to - * the SDK if desired - * @return a {@link DataStore} - */ - DataStore createDataStore(ClientContext context, DataStoreUpdates dataStoreUpdates); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreStatusProvider.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreStatusProvider.java index 3536692b9..0c71fa3b9 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreStatusProvider.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreStatusProvider.java @@ -16,9 +16,9 @@ public interface DataStoreStatusProvider { /** * Returns the current status of the store. *

- * This is only meaningful for persistent stores, or any other {@link DataStore} implementation that makes use of - * the reporting mechanism provided by {@link DataStoreFactory#createDataStore(ClientContext, DataStoreUpdates)}. - * For the default in-memory store, the status will always be reported as "available". + * This is only meaningful for persistent stores, or any custom data store implementation that makes use of + * the status reporting mechanism provided by the SDK. For the default in-memory store, the status will always + * be reported as "available". * * @return the latest status; will never be null */ diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/Event.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/Event.java deleted file mode 100644 index 76614117f..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/Event.java +++ /dev/null @@ -1,333 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; - -/** - * Base class for all analytics events that are generated by the client. Also defines all of its own subclasses. - * - * Applications do not need to reference these types directly. They are used internally in analytics event - * processing, and are visible only to support writing a custom implementation of {@link EventProcessor} if - * desired. - */ -public class Event { - private final long creationDate; - private final LDUser user; - - /** - * Base event constructor. - * @param creationDate the timestamp in milliseconds - * @param user the user associated with the event - */ - public Event(long creationDate, LDUser user) { - this.creationDate = creationDate; - this.user = user; - } - - /** - * The event timestamp. - * @return the timestamp in milliseconds - */ - public long getCreationDate() { - return creationDate; - } - - /** - * The user associated with the event. - * @return the user object - */ - public LDUser getUser() { - return user; - } - - /** - * Convert a user into a context kind string - * @param user the user to get the context kind from - * @return the context kind string - */ - private static final String computeContextKind(LDUser user) { - return user != null && user.isAnonymous() ? "anonymousUser" : "user"; - } - - /** - * A custom event created with {@link LDClientInterface#track(String, LDUser)} or one of its overloads. - */ - public static final class Custom extends Event { - private final String key; - private final LDValue data; - private final Double metricValue; - private final String contextKind; - - /** - * Constructs a custom event. - * @param timestamp the timestamp in milliseconds - * @param key the event key - * @param user the user associated with the event - * @param data custom data if any (null is the same as {@link LDValue#ofNull()}) - * @param metricValue custom metric value if any - * @since 4.8.0 - */ - public Custom(long timestamp, String key, LDUser user, LDValue data, Double metricValue) { - super(timestamp, user); - this.key = key; - this.data = LDValue.normalize(data); - this.metricValue = metricValue; - this.contextKind = computeContextKind(user); - } - - /** - * The custom event key. - * @return the event key - */ - public String getKey() { - return key; - } - - /** - * The custom data associated with the event, if any. - * @return the event data (null is equivalent to {@link LDValue#ofNull()}) - */ - public LDValue getData() { - return data; - } - - /** - * The numeric metric value associated with the event, if any. - * @return the metric value or null - */ - public Double getMetricValue() { - return metricValue; - } - - /** - * The context kind of the user that generated this event - * @return the context kind - */ - public String getContextKind() { - return contextKind; - } - } - - /** - * An event created with {@link LDClientInterface#identify(LDUser)}. - */ - public static final class Identify extends Event { - /** - * Constructs an identify event. - * @param timestamp the timestamp in milliseconds - * @param user the user associated with the event - */ - public Identify(long timestamp, LDUser user) { - super(timestamp, user); - } - } - - /** - * An event created internally by the SDK to hold user data that may be referenced by multiple events. - */ - public static final class Index extends Event { - /** - * Constructs an index event. - * @param timestamp the timestamp in milliseconds - * @param user the user associated with the event - */ - public Index(long timestamp, LDUser user) { - super(timestamp, user); - } - } - - /** - * An event generated by a feature flag evaluation. - */ - public static final class FeatureRequest extends Event { - private final String key; - private final int variation; - private final LDValue value; - private final LDValue defaultVal; - private final int version; - private final String prereqOf; - private final boolean trackEvents; - private final long debugEventsUntilDate; - private final EvaluationReason reason; - private final boolean debug; - private final String contextKind; - - /** - * Constructs a feature request event. - * @param timestamp the timestamp in milliseconds - * @param key the flag key - * @param user the user associated with the event - * @param version the flag version, or -1 if the flag was not found - * @param variation the result variation, or -1 if there was an error - * @param value the result value - * @param defaultVal the default value passed by the application - * @param reason the evaluation reason, if it is to be included in the event - * @param prereqOf if this flag was evaluated as a prerequisite, this is the key of the flag that referenced it - * @param trackEvents true if full event tracking is turned on for this flag - * @param debugEventsUntilDate if non-null, the time until which event debugging should be enabled - * @param debug true if this is a debugging event - * @since 4.8.0 - */ - public FeatureRequest(long timestamp, String key, LDUser user, int version, int variation, LDValue value, - LDValue defaultVal, EvaluationReason reason, String prereqOf, boolean trackEvents, long debugEventsUntilDate, boolean debug) { - super(timestamp, user); - this.key = key; - this.version = version; - this.variation = variation; - this.value = value; - this.defaultVal = defaultVal; - this.prereqOf = prereqOf; - this.trackEvents = trackEvents; - this.debugEventsUntilDate = debugEventsUntilDate; - this.reason = reason; - this.debug = debug; - this.contextKind = computeContextKind(user); - } - - /** - * The key of the feature flag that was evaluated. - * @return the flag key - */ - public String getKey() { - return key; - } - - /** - * The index of the selected flag variation, or -1 if the application default value was used. - * @return zero-based index of the variation, or -1 - */ - public int getVariation() { - return variation; - } - - /** - * The value of the selected flag variation. - * @return the value - */ - public LDValue getValue() { - return value; - } - - /** - * The application default value used in the evaluation. - * @return the application default - */ - public LDValue getDefaultVal() { - return defaultVal; - } - - /** - * The version of the feature flag that was evaluated, or -1 if the flag was not found. - * @return the flag version or null - */ - public int getVersion() { - return version; - } - - /** - * If this flag was evaluated as a prerequisite for another flag, the key of the other flag. - * @return a flag key or null - */ - public String getPrereqOf() { - return prereqOf; - } - - /** - * True if full event tracking is enabled for this flag. - * @return true if full event tracking is on - */ - public boolean isTrackEvents() { - return trackEvents; - } - - /** - * If debugging is enabled for this flag, the Unix millisecond time at which to stop debugging. - * @return a timestamp or zero - */ - public long getDebugEventsUntilDate() { - return debugEventsUntilDate; - } - - /** - * The {@link EvaluationReason} for this evaluation, or null if the reason was not requested for this evaluation. - * @return a reason object or null - */ - public EvaluationReason getReason() { - return reason; - } - - /** - * True if this event was generated due to debugging being enabled. - * @return true if this is a debug event - */ - public boolean isDebug() { - return debug; - } - - /** - * The context kind of the user that generated this event - * @return the context kind - */ - public String getContextKind() { - return contextKind; - } - } - - /** - * An event generated by aliasing users - * @since 5.4.0 - */ - public static final class AliasEvent extends Event { - private final String key; - private final String contextKind; - private final String previousKey; - private final String previousContextKind; - - /** - * Constructs an alias event. - * @param timestamp when the event was created - * @param user the user being aliased to - * @param previousUser the user being aliased from - */ - public AliasEvent(long timestamp, LDUser user, LDUser previousUser) { - super(timestamp, user); - this.key = user.getKey(); - this.contextKind = computeContextKind(user); - this.previousKey = previousUser.getKey(); - this.previousContextKind = computeContextKind(previousUser); - } - - /** - * Get the key of the user being aliased to - * @return the user key - */ - public String getKey() { - return key; - } - - /** - * Get the kind of the user being aliased to - * @return the context kind - */ - public String getContextKind() { - return contextKind; - } - - /** - * Get the key of the user being aliased from - * @return the previous user key - */ - public String getPreviousKey() { - return previousKey; - } - - /** - * Get the kind of the user being aliased from - * @return the previous context kind - */ - public String getPreviousContextKind() { - return previousContextKind; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventProcessor.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/EventProcessor.java deleted file mode 100644 index 656964257..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventProcessor.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import java.io.Closeable; - -/** - * Interface for an object that can send or store analytics events. - * @since 4.0.0 - */ -public interface EventProcessor extends Closeable { - /** - * Records an event asynchronously. - * @param e an event - */ - void sendEvent(Event e); - - /** - * Specifies that any buffered events should be sent as soon as possible, rather than waiting - * for the next flush interval. This method is asynchronous, so events still may not be sent - * until a later time. However, calling {@link Closeable#close()} will synchronously deliver - * any events that were not yet delivered prior to shutting down. - */ - void flush(); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventProcessorFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/EventProcessorFactory.java deleted file mode 100644 index afc247770..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventProcessorFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.sdk.server.Components; - -/** - * Interface for a factory that creates some implementation of {@link EventProcessor}. - * @see Components - * @since 4.0.0 - */ -public interface EventProcessorFactory { - /** - * Creates an implementation instance. - * - * @param context allows access to the client configuration - * @return an {@link EventProcessor} - */ - EventProcessor createEventProcessor(ClientContext context); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventSender.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/EventSender.java deleted file mode 100644 index 0325b590f..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventSender.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import java.io.Closeable; -import java.net.URI; -import java.util.Date; - -/** - * Interface for a component that can deliver preformatted event data. - * - * @see com.launchdarkly.sdk.server.integrations.EventProcessorBuilder#eventSender(EventSenderFactory) - * @since 4.14.0 - */ -public interface EventSender extends Closeable { - /** - * Attempt to deliver an event data payload. - *

- * This method will be called synchronously from an event delivery worker thread. - * - * @param kind specifies which type of event data is being sent - * @param data the preformatted JSON data, as a string - * @param eventCount the number of individual events in the data - * @param eventsBaseUri the configured events endpoint base URI - * @return a {@link Result} - */ - Result sendEventData(EventDataKind kind, String data, int eventCount, URI eventsBaseUri); - - /** - * Enumerated values corresponding to different kinds of event data. - */ - public enum EventDataKind { - /** - * Regular analytics events. - */ - ANALYTICS, - - /** - * Diagnostic data generated by the SDK. - */ - DIAGNOSTICS - }; - - /** - * Encapsulates the results of a call to {@link EventSender#sendEventData(EventDataKind, String, int, URI)}. - */ - public static final class Result { - private boolean success; - private boolean mustShutDown; - private Date timeFromServer; - - /** - * Constructs an instance. - * - * @param success true if the events were delivered - * @param mustShutDown true if an unrecoverable error (such as an HTTP 401 error, implying that the - * SDK key is invalid) means the SDK should permanently stop trying to send events - * @param timeFromServer the parsed value of an HTTP Date header received from the remote server, - * if any; this is used to compensate for differences between the application's time and server time - */ - public Result(boolean success, boolean mustShutDown, Date timeFromServer) { - this.success = success; - this.mustShutDown = mustShutDown; - this.timeFromServer = timeFromServer; - } - - /** - * Returns true if the events were delivered. - * - * @return true if the events were delivered - */ - public boolean isSuccess() { - return success; - } - - /** - * Returns true if an unrecoverable error (such as an HTTP 401 error, implying that the - * SDK key is invalid) means the SDK should permanently stop trying to send events - * - * @return true if event delivery should shut down - */ - public boolean isMustShutDown() { - return mustShutDown; - } - - /** - * Returns the parsed value of an HTTP Date header received from the remote server, if any. This - * is used to compensate for differences between the application's time and server time. - * - * @return a date value or null - */ - public Date getTimeFromServer() { - return timeFromServer; - } - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventSenderFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/EventSenderFactory.java deleted file mode 100644 index f576a530b..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/EventSenderFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.logging.LDLogger; - -/** - * Interface for a factory that creates some implementation of {@link EventSender}. - * - * @see com.launchdarkly.sdk.server.integrations.EventProcessorBuilder#eventSender(EventSenderFactory) - * @since 4.14.0 - */ -public interface EventSenderFactory { - /** - * Older method for creating the implementation object. This is superseded by the method that - * includes a logger instance. - * - * @param basicConfiguration the basic global SDK configuration properties - * @param httpConfiguration HTTP configuration properties - * @return an {@link EventSender} - * @deprecated use the overload that includes a logger - */ - @Deprecated - EventSender createEventSender(BasicConfiguration basicConfiguration, HttpConfiguration httpConfiguration); - - /** - * Called by the SDK to create the implementation object. - * - * @param basicConfiguration the basic global SDK configuration properties - * @param httpConfiguration HTTP configuration properties - * @param logger the configured logger - * @return an {@link EventSender} - * @since 5.10.0 - */ - default EventSender createEventSender( - BasicConfiguration basicConfiguration, - HttpConfiguration httpConfiguration, - LDLogger logger) { - return createEventSender(basicConfiguration, httpConfiguration); - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagTracker.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagTracker.java index fbe580c32..26cf1eefa 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagTracker.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagTracker.java @@ -1,5 +1,6 @@ package com.launchdarkly.sdk.server.interfaces; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.server.Components; @@ -20,14 +21,14 @@ public interface FlagTracker { * for other flags, the SDK assumes that those flags may now behave differently and sends flag change events * for them as well. *

- * Note that this does not necessarily mean the flag's value has changed for any particular user, only that - * some part of the flag configuration was changed so that it may return a different value than it - * previously returned for some user. If you want to track flag value changes, use - * {@link #addFlagValueChangeListener(String, LDUser, FlagValueChangeListener)} instead. + * Note that this does not necessarily mean the flag's value has changed for any particular evaluation + * context, only that some part of the flag configuration was changed so that it may return a + * different value than it previously returned for some context. If you want to track flag value changes, + * use {@link #addFlagValueChangeListener(String, LDContext, FlagValueChangeListener)} instead. *

* If using the file data source ({@link com.launchdarkly.sdk.server.integrations.FileData}), any change in * a data file will be treated as a change to every flag. Again, use - * {@link #addFlagValueChangeListener(String, LDUser, FlagValueChangeListener)} (or just re-evaluate the flag + * {@link #addFlagValueChangeListener(String, LDContext, FlagValueChangeListener)} (or just re-evaluate the flag * yourself) if you want to know whether this is a change that really affects a flag's value. *

* Change events only work if the SDK is actually connecting to LaunchDarkly (or using the file data source). @@ -41,7 +42,7 @@ public interface FlagTracker { * @param listener the event listener to register * @see #removeFlagChangeListener(FlagChangeListener) * @see FlagChangeListener - * @see #addFlagValueChangeListener(String, LDUser, FlagValueChangeListener) + * @see #addFlagValueChangeListener(String, LDContext, FlagValueChangeListener) */ public void addFlagChangeListener(FlagChangeListener listener); @@ -52,33 +53,49 @@ public interface FlagTracker { * * @param listener the event listener to unregister * @see #addFlagChangeListener(FlagChangeListener) - * @see #addFlagValueChangeListener(String, LDUser, FlagValueChangeListener) + * @see #addFlagValueChangeListener(String, LDContext, FlagValueChangeListener) * @see FlagChangeListener */ public void removeFlagChangeListener(FlagChangeListener listener); /** - * Registers a listener to be notified of a change in a specific feature flag's value for a specific set of - * user properties. + * Registers a listener to be notified of a change in a specific feature flag's value for a specific + * evaluation context. *

* When you call this method, it first immediately evaluates the feature flag. It then uses * {@link #addFlagChangeListener(FlagChangeListener)} to start listening for feature flag configuration - * changes, and whenever the specified feature flag changes, it re-evaluates the flag for the same user. + * changes, and whenever the specified feature flag changes, it re-evaluates the flag for the same context. * It then calls your {@link FlagValueChangeListener} if and only if the resulting value has changed. *

- * All feature flag evaluations require an instance of {@link LDUser}. If the feature flag you are - * tracking does not have any user targeting rules, you must still pass a dummy user such as - * {@code new LDUser("for-global-flags")}. If you do not want the user to appear on your dashboard, use - * the {@code anonymous} property: {@code new LDUserBuilder("for-global-flags").anonymous(true).build()}. + * All feature flag evaluations require an instance of {@link LDContext}. If the feature flag you are + * tracking does not have any user targeting rules, you must still pass a dummy context such as + * {@code LDContext.create("for-global-flags")}. If you do not want the user to appear on your dashboard, + * use the {@code anonymous} property: {@code LDContext.builder("for-global-flags").anonymous(true).build()}. *

* The returned {@link FlagChangeListener} represents the subscription that was created by this method * call; to unsubscribe, pass that object (not your {@code FlagValueChangeListener}) to * {@link #removeFlagChangeListener(FlagChangeListener)}. * * @param flagKey the flag key to be evaluated - * @param user the user properties for evaluation + * @param context the evaluation context * @param listener an object that you provide which will be notified of changes * @return a {@link FlagChangeListener} that can be used to unregister the listener + * @since 6.0.0 */ - public FlagChangeListener addFlagValueChangeListener(String flagKey, LDUser user, FlagValueChangeListener listener); + public FlagChangeListener addFlagValueChangeListener(String flagKey, LDContext context, FlagValueChangeListener listener); + + /** + * Registers a listener to be notified of a change in a specific feature flag's value for a specific user. + *

+ * This is equivalent to {@link #addFlagValueChangeListener(String, LDContext, FlagValueChangeListener)}, but + * using the {@link LDUser} type instead of {@link LDContext}. + * + * @param flagKey the flag key to be evaluated + * @param user the user attributes + * @param listener an object that you provide which will be notified of changes + * @return a {@link FlagChangeListener} that can be used to unregister the listener + */ + public default FlagChangeListener addFlagValueChangeListener(String flagKey, LDUser user, FlagValueChangeListener listener) { + return addFlagValueChangeListener(flagKey, LDContext.fromUser(user), listener); + } } diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeEvent.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeEvent.java index 1f4e4dda7..83e58c905 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeEvent.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeEvent.java @@ -9,7 +9,7 @@ * * @since 5.0.0 * @see FlagValueChangeListener - * @see FlagTracker#addFlagValueChangeListener(String, com.launchdarkly.sdk.LDUser, FlagValueChangeListener) + * @see FlagTracker#addFlagValueChangeListener(String, com.launchdarkly.sdk.LDContext, FlagValueChangeListener) */ public class FlagValueChangeEvent extends FlagChangeEvent { private final LDValue oldValue; @@ -29,7 +29,7 @@ public FlagValueChangeEvent(String key, LDValue oldValue, LDValue newValue) { } /** - * Returns the last known value of the flag for the specified user prior to the update. + * Returns the last known value of the flag for the specified evaluation context prior to the update. *

* Since flag values can be of any JSON data type, this is represented as {@link LDValue}. That class * has methods for converting to a primitive Java type such as {@link LDValue#booleanValue()}. @@ -45,7 +45,7 @@ public LDValue getOldValue() { } /** - * Returns the new value of the flag for the specified user. + * Returns the new value of the flag for the specified evaluation context. *

* Since flag values can be of any JSON data type, this is represented as {@link LDValue}. That class * has methods for converting to a primitive Java type such {@link LDValue#booleanValue()}. diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeListener.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeListener.java index 2981a3633..8813d3254 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeListener.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/FlagValueChangeListener.java @@ -1,37 +1,39 @@ package com.launchdarkly.sdk.server.interfaces; /** - * An event listener that is notified when a feature flag's value has changed for a specific user. + * An event listener that is notified when a feature flag's value has changed for a specific + * evaluation context. *

- * Use this in conjunction with {@link FlagTracker#addFlagValueChangeListener(String, com.launchdarkly.sdk.LDUser, FlagValueChangeListener)} - * if you want the client to re-evaluate a flag for a specific set of user properties whenever - * the flag's configuration has changed, and notify you only if the new value is different from the old + * Use this in conjunction with {@link FlagTracker#addFlagValueChangeListener(String, com.launchdarkly.sdk.LDContext, FlagValueChangeListener)} + * if you want the client to re-evaluate a flag for a specific evaluation context whenever the + * flag's configuration has changed, and notify you only if the new value is different from the old * value. The listener will not be notified if the flag's configuration is changed in some way that does - * not affect its value for that user. + * not affect its value for that context. * *


  *     String flagKey = "my-important-flag";
- *     LDUser userForFlagEvaluation = new LDUser("user-key-for-global-flag-state");
+ *     LDContext contextForFlagEvaluation = LDContext.create("context-key-for-global-flag-state");
  *     FlagValueChangeListener listenForNewValue = event -> {
  *         if (event.getKey().equals(flagKey)) {
  *             doSomethingWithNewValue(event.getNewValue().booleanValue());
  *         }
  *     };
  *     client.getFlagTracker().addFlagValueChangeListener(flagKey,
- *         userForFlagEvaluation, listenForNewValue);
+ *         contextForFlagEvaluation, listenForNewValue);
  * 
* * In the above example, the value provided in {@code event.getNewValue()} is the result of calling - * {@code client.jsonValueVariation(flagKey, userForFlagEvaluation, LDValue.ofNull())} after the flag + * {@code client.jsonValueVariation(flagKey, contextForFlagEvaluation, LDValue.ofNull())} after the flag * has changed. * * @since 5.0.0 * @see FlagChangeListener - * @see FlagTracker#addFlagValueChangeListener(String, com.launchdarkly.sdk.LDUser, FlagValueChangeListener) + * @see FlagTracker#addFlagValueChangeListener(String, com.launchdarkly.sdk.LDContext, FlagValueChangeListener) */ public interface FlagValueChangeListener { /** - * The SDK calls this method when a feature flag's value has changed with regard to the specified user. + * The SDK calls this method when a feature flag's value has changed with regard to the specified + * evaluation context. * * @param event the event parameters */ diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfiguration.java deleted file mode 100644 index 1a2ab19b3..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfiguration.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder; - -import java.net.Proxy; -import java.time.Duration; -import java.util.Map; - -import javax.net.SocketFactory; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.X509TrustManager; - -/** - * Encapsulates top-level HTTP configuration that applies to all SDK components. - *

- * Use {@link HttpConfigurationBuilder} to construct an instance. - *

- * The SDK's built-in components use OkHttp as the HTTP client implementation, but since OkHttp types - * are not surfaced in the public API and custom components might use some other implementation, this - * class only provides the properties that would be used to create an HTTP client; it does not create - * the client itself. SDK implementation code uses its own helper methods to do so. - * - * @since 4.13.0 - */ -public interface HttpConfiguration { - /** - * The connection timeout. This is the time allowed for the underlying HTTP client to connect - * to the LaunchDarkly server. - * - * @return the connection timeout; must not be null - */ - Duration getConnectTimeout(); - - /** - * The proxy configuration, if any. - * - * @return a {@link Proxy} instance or null - */ - Proxy getProxy(); - - /** - * The authentication method to use for a proxy, if any. Ignored if {@link #getProxy()} is null. - * - * @return an {@link HttpAuthentication} implementation or null - */ - HttpAuthentication getProxyAuthentication(); - - /** - * The socket timeout. This is the amount of time without receiving data on a connection that the - * SDK will tolerate before signaling an error. This does not apply to the streaming connection - * used by {@link com.launchdarkly.sdk.server.Components#streamingDataSource()}, which has its own - * non-configurable read timeout based on the expected behavior of the LaunchDarkly streaming service. - * - * @return the socket timeout; must not be null - */ - Duration getSocketTimeout(); - - /** - * The configured socket factory for insecure connections. - * - * @return a SocketFactory or null - */ - default SocketFactory getSocketFactory() { - return null; - } - - /** - * The configured socket factory for secure connections. - * - * @return a SSLSocketFactory or null - */ - SSLSocketFactory getSslSocketFactory(); - - /** - * The configured trust manager for secure connections, if custom certificate verification is needed. - * - * @return an X509TrustManager or null - */ - X509TrustManager getTrustManager(); - - /** - * Returns the basic headers that should be added to all HTTP requests from SDK components to - * LaunchDarkly services, based on the current SDK configuration. - * - * @return a list of HTTP header names and values - */ - Iterable> getDefaultHeaders(); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfigurationFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfigurationFactory.java deleted file mode 100644 index 9a5082af8..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/HttpConfigurationFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -/** - * Interface for a factory that creates an {@link HttpConfiguration}. - * - * @see com.launchdarkly.sdk.server.Components#httpConfiguration() - * @see com.launchdarkly.sdk.server.LDConfig.Builder#http(HttpConfigurationFactory) - * @since 4.13.0 - */ -public interface HttpConfigurationFactory { - /** - * Creates the configuration object. - * - * @param basicConfiguration provides the basic SDK configuration properties - * @return an {@link HttpConfiguration} - */ - public HttpConfiguration createHttpConfiguration(BasicConfiguration basicConfiguration); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java index 5733ef8ce..57baf4fe3 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/LDClientInterface.java @@ -1,6 +1,7 @@ package com.launchdarkly.sdk.server.interfaces; import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.FeatureFlagsState; @@ -25,178 +26,540 @@ public interface LDClientInterface extends Closeable { boolean isInitialized(); /** - * Tracks that a user performed an event. + * Tracks that an application-defined event occurred. *

- * To add custom data to the event, use {@link #trackData(String, LDUser, LDValue)}. + * This method creates a "custom" analytics event containing the specified event name (key) + * and context properties. You may attach arbitrary data or a metric value to the event by calling + * {@link #trackData(String, LDContext, LDValue)} or {@link #trackMetric(String, LDContext, LDValue, double)} + * instead. + *

+ * Note that event delivery is asynchronous, so the event may not actually be sent until + * later; see {@link #flush()}. + * + * @param eventName the name of the event + * @param context the context associated with the event + * @see #trackData(String, LDContext, LDValue) + * @see #trackMetric(String, LDContext, LDValue, double) + * @see #track(String, LDUser) + * @since 6.0.0 + */ + void track(String eventName, LDContext context); + + /** + * Tracks that an application-defined event occurred. + *

+ * This is equivalent to {@link #track(String, LDContext)}, but using the {@link LDUser} type + * instead of {@link LDContext}. * * @param eventName the name of the event - * @param user the user that performed the event + * @param user the user attributes + * @see #trackData(String, LDContext, LDValue) + * @see #trackMetric(String, LDContext, LDValue, double) + * @see #track(String, LDContext) */ - void track(String eventName, LDUser user); + default void track(String eventName, LDUser user) { + track(eventName, LDContext.fromUser(user)); + } /** - * Tracks that a user performed an event, and provides additional custom data. + * Tracks that an application-defined event occurred. + *

+ * This method creates a "custom" analytics event containing the specified event name (key), + * context properties, and optional data. If you do not need custom data, pass {@link LDValue#ofNull()} + * for the last parameter or simply omit the parameter. You may attach a metric value to the event by + * calling {@link #trackMetric(String, LDContext, LDValue, double)} instead. + *

+ * Note that event delivery is asynchronous, so the event may not actually be sent until + * later; see {@link #flush()}. * * @param eventName the name of the event - * @param user the user that performed the event - * @param data an {@link LDValue} containing additional data associated with the event + * @param context the context associated with the event + * @param data additional data associated with the event, if any + * @since 6.0.0 + * @see #track(String, LDContext) + * @see #trackMetric(String, LDContext, LDValue, double) + * @see #trackData(String, LDUser, LDValue) + */ + void trackData(String eventName, LDContext context, LDValue data); + + /** + * Tracks that an application-defined event occurred. + *

+ * This is equivalent to {@link #trackData(String, LDContext, LDValue)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param eventName the name of the event + * @param user the user attributes + * @param data additional data associated with the event, if any * @since 4.8.0 + * @see #track(String, LDContext) + * @see #trackMetric(String, LDContext, LDValue, double) + * @see #trackData(String, LDContext, LDValue) */ - void trackData(String eventName, LDUser user, LDValue data); + default void trackData(String eventName, LDUser user, LDValue data) { + trackData(eventName, LDContext.fromUser(user), data); + } /** - * Tracks that a user performed an event, and provides an additional numeric value for custom metrics. + * Tracks that an application-defined event occurred, and provides an additional numeric value for + * custom metrics. + *

+ * This value is used by the LaunchDarkly experimentation feature in numeric custom metrics, + * and will also be returned as part of the custom event for Data Export. + *

+ * Note that event delivery is asynchronous, so the event may not actually be sent until + * later; see {@link #flush()}. * * @param eventName the name of the event - * @param user the user that performed the event + * @param context the context associated with the event * @param data an {@link LDValue} containing additional data associated with the event; if not applicable, * you may pass either {@code null} or {@link LDValue#ofNull()} * @param metricValue a numeric value used by the LaunchDarkly experimentation feature in numeric custom - * metrics. Can be omitted if this event is used by only non-numeric metrics. This field will also be - * returned as part of the custom event for Data Export. + * metrics * @since 4.9.0 + * @see #track(String, LDContext) + * @see #trackData(String, LDContext, LDValue) */ - void trackMetric(String eventName, LDUser user, LDValue data, double metricValue); + void trackMetric(String eventName, LDContext context, LDValue data, double metricValue); /** - * Registers the user. + * Tracks that an application-defined event occurred, and provides an additional numeric value for + * custom metrics. + *

+ * This is equivalent to {@link #trackMetric(String, LDContext, LDValue, double)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param eventName the name of the event + * @param user the user attributes + * @param data an {@link LDValue} containing additional data associated with the event; if not applicable, + * you may pass either {@code null} or {@link LDValue#ofNull()} + * @param metricValue a numeric value used by the LaunchDarkly experimentation feature in numeric custom + * metrics + * @since 4.9.0 + * @see #track(String, LDContext) + * @see #trackData(String, LDContext, LDValue) + */ + default void trackMetric(String eventName, LDUser user, LDValue data, double metricValue) { + trackMetric(eventName, LDContext.fromUser(user), data, metricValue); + } + + /** + * Reports details about an evaluation context. + *

+ * This method simply creates an analytics event containing the context properties, to + * that LaunchDarkly will know about that context if it does not already. + *

+ * Calling any evaluation method, such as {@link #boolVariation(String, LDContext, boolean)}, + * also sends the context information to LaunchDarkly (if events are enabled), so you only + * need to use {@link #identify(LDContext)} if you want to identify the context without + * evaluating a flag. + *

+ * Note that event delivery is asynchronous, so the event may not actually be sent until + * later; see {@link #flush()}. * - * @param user the user to register + * @param context the context to register + * @see #identify(LDUser) + * @since 6.0.0 + */ + void identify(LDContext context); + + /** + * Reports details about a user. + *

+ * This is equivalent to {@link #identify(LDContext)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param user the user attributes + * @see #identify(LDContext) + */ + default void identify(LDUser user) { + identify(LDContext.fromUser(user)); + } + + /** + * Returns an object that encapsulates the state of all feature flags for a given context, which can be + * passed to front-end code. + *

+ * The object returned by this method contains the flag values as well as other metadata that + * is used by the LaunchDarkly JavaScript client, so it can be used for + * bootstrapping. + *

+ * This method will not send analytics events back to LaunchDarkly. + * + * @param context the evaluation context + * @param options optional {@link FlagsStateOption} values affecting how the state is computed - for + * instance, to filter the set of flags to only include the client-side-enabled ones + * @return a {@link FeatureFlagsState} object (will never be null; see {@link FeatureFlagsState#isValid()} + * @see #allFlagsState(LDUser, FlagsStateOption...) + * @since 6.0.0 */ - void identify(LDUser user); + FeatureFlagsState allFlagsState(LDContext context, FlagsStateOption... options); /** - * Returns an object that encapsulates the state of all feature flags for a given user, including the flag - * values and also metadata that can be used on the front end. This method does not send analytics events - * back to LaunchDarkly. + * Returns an object that encapsulates the state of all feature flags for a given user, which can be + * passed to front-end code. *

- * The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service. + * This is equivalent to {@link #allFlagsState(LDContext, FlagsStateOption...)}, but using the {@link LDUser} type + * instead of {@link LDContext}. * - * @param user the end user requesting the feature flags + * @param user the user attributes * @param options optional {@link FlagsStateOption} values affecting how the state is computed - for * instance, to filter the set of flags to only include the client-side-enabled ones * @return a {@link FeatureFlagsState} object (will never be null; see {@link FeatureFlagsState#isValid()} + * @see #allFlagsState(LDContext, FlagsStateOption...) * @since 4.3.0 */ - FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options); + default FeatureFlagsState allFlagsState(LDUser user, FlagsStateOption... options) { + return allFlagsState(LDContext.fromUser(user), options); + } /** - * Calculates the value of a feature flag for a given user. - * - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + * Calculates the boolean value of a feature flag for a given context. + *

+ * If the flag variation does not have a boolean value, {@code defaultValue} is returned. + *

+ * If an error makes it impossible to evaluate the flag (for instance, the feature flag key + * does not match any existing flag), {@code defaultValue} is returned. + * + * @param key the unique key for the feature flag + * @param context the evaluation context * @param defaultValue the default value of the flag - * @return the variation for the given user, or {@code defaultValue} if there is an error fetching the variation or the flag doesn't exist + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #boolVariation(String, LDUser, boolean) + * @since 6.0.0 */ - boolean boolVariation(String featureKey, LDUser user, boolean defaultValue); + boolean boolVariation(String key, LDContext context, boolean defaultValue); + + /** + * Calculates the boolean value of a feature flag for a given user. + *

+ * This is equivalent to {@link #boolVariation(String, LDContext, boolean)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param key the unique key for the feature flag + * @param user the user attributes + * @param defaultValue the default value of the flag + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #boolVariation(String, LDContext, boolean) + */ + default boolean boolVariation(String key, LDUser user, boolean defaultValue) { + return boolVariation(key, LDContext.fromUser(user), defaultValue); + } + /** + * Calculates the integer value of a feature flag for a given context. + *

+ * If the flag variation has a numeric value that is not an integer, it is rounded toward zero + * (truncated). + *

+ * If the flag variation does not have a numeric value, {@code defaultValue} is returned. + *

+ * If an error makes it impossible to evaluate the flag (for instance, the feature flag key + * does not match any existing flag), {@code defaultValue} is returned. + * + * @param key the unique key for the feature flag + * @param context the evaluation context + * @param defaultValue the default value of the flag + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #intVariation(String, LDUser, int) + * @since 6.0.0 + */ + int intVariation(String key, LDContext context, int defaultValue); + /** * Calculates the integer value of a feature flag for a given user. *

- * If the flag variation has a numeric value that is not an integer, it is rounded toward zero (truncated). - * - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + * This is equivalent to {@link #intVariation(String, LDContext, int)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param key the unique key for the feature flag + * @param user the user attributes * @param defaultValue the default value of the flag - * @return the variation for the given user, or {@code defaultValue} if there is an error fetching the variation or the flag doesn't exist + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #intVariation(String, LDContext, int) */ - int intVariation(String featureKey, LDUser user, int defaultValue); + default int intVariation(String key, LDUser user, int defaultValue) { + return intVariation(key, LDContext.fromUser(user), defaultValue); + } /** - * Calculates the floating point numeric value of a feature flag for a given user. + * Calculates the floating-point numeric value of a feature flag for a given context. + *

+ * If the flag variation does not have a numeric value, {@code defaultValue} is returned. + *

+ * If an error makes it impossible to evaluate the flag (for instance, the feature flag key + * does not match any existing flag), {@code defaultValue} is returned. * - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + * @param key the unique key for the feature flag + * @param context the evaluation context * @param defaultValue the default value of the flag - * @return the variation for the given user, or {@code defaultValue} if there is an error fetching the variation or the flag doesn't exist + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #doubleVariation(String, LDUser, double) + * @since 6.0.0 */ - double doubleVariation(String featureKey, LDUser user, double defaultValue); + double doubleVariation(String key, LDContext context, double defaultValue); /** - * Calculates the String value of a feature flag for a given user. + * Calculates the floating-point numeric value of a feature flag for a given context. + *

+ * This is equivalent to {@link #doubleVariation(String, LDContext, double)}, but using the {@link LDUser} type + * instead of {@link LDContext}. * - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + * @param key the unique key for the feature flag + * @param user the user attributes * @param defaultValue the default value of the flag - * @return the variation for the given user, or {@code defaultValue} if there is an error fetching the variation or the flag doesn't exist + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #doubleVariation(String, LDContext, double) */ - String stringVariation(String featureKey, LDUser user, String defaultValue); + default double doubleVariation(String key, LDUser user, double defaultValue) { + return doubleVariation(key, LDContext.fromUser(user), defaultValue); + } /** - * Calculates the {@link LDValue} value of a feature flag for a given user. + * Calculates the string value of a feature flag for a given context. + *

+ * If the flag variation does not have a string value, {@code defaultValue} is returned. + *

+ * If an error makes it impossible to evaluate the flag (for instance, the feature flag key + * does not match any existing flag), {@code defaultValue} is returned. * - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + * @param key the unique key for the feature flag + * @param context the evaluation context * @param defaultValue the default value of the flag - * @return the variation for the given user, or {@code defaultValue} if there is an error fetching the variation or the flag doesn't exist; - * will never be a null reference, but may be {@link LDValue#ofNull()} - * + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #stringVariation(String, LDUser, String) + * @since 6.0.0 + */ + String stringVariation(String key, LDContext context, String defaultValue); + + /** + * Calculates the string value of a feature flag for a given context. + *

+ * This is equivalent to {@link #stringVariation(String, LDContext, String)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param key the unique key for the feature flag + * @param user the user attributes + * @param defaultValue the default value of the flag + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #stringVariation(String, LDContext, String) + */ + default String stringVariation(String key, LDUser user, String defaultValue) { + return stringVariation(key, LDContext.fromUser(user), defaultValue); + } + + /** + * Calculates the value of a feature flag for a given context as any JSON value type. + *

+ * The type {@link LDValue} is used to represent any of the value types that can + * exist in JSON. Use {@link LDValue} methods to examine its type and value. + * + * @param key the unique key for the feature flag + * @param context the evaluation context + * @param defaultValue the default value of the flag + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #jsonValueVariation(String, LDUser, LDValue) + * @since 6.0.0 + */ + LDValue jsonValueVariation(String key, LDContext context, LDValue defaultValue); + + /** + * Calculates the value of a feature flag for a given context as any JSON value type. + *

+ * This is equivalent to {@link #jsonValueVariation(String, LDContext, LDValue)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param key the unique key for the feature flag + * @param user the user attributes + * @param defaultValue the default value of the flag + * @return the variation for the given context, or {@code defaultValue} if the flag cannot be evaluated + * @see #jsonValueVariation(String, LDContext, LDValue) * @since 4.8.0 */ - LDValue jsonValueVariation(String featureKey, LDUser user, LDValue defaultValue); + default LDValue jsonValueVariation(String key, LDUser user, LDValue defaultValue) { + return jsonValueVariation(key, LDContext.fromUser(user), defaultValue); + } /** - * Calculates the value of a feature flag for a given user, and returns an object that describes the - * way the value was determined. The {@code reason} property in the result will also be included in + * Calculates the boolean value of a feature flag for a given context, and returns an object that + * describes the way the value was determined. + *

+ * The {@link EvaluationDetail#getReason()} property in the result will also be included in * analytics events, if you are capturing detailed event data for this flag. - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + *

+ * The behavior is otherwise identical to {@link #boolVariation(String, LDContext, boolean)}. + * + * @param key the unique key for the feature flag + * @param context the evaluation context + * @param defaultValue the default value of the flag + * @return an {@link EvaluationDetail} object + * @since 6.0.0 + * @see #boolVariationDetail(String, LDUser, boolean) + */ + EvaluationDetail boolVariationDetail(String key, LDContext context, boolean defaultValue); + + /** + * Calculates the boolean value of a feature flag for a given context, and returns an object that + * describes the way the value was determined. + *

+ * This is equivalent to {@link #boolVariationDetail(String, LDContext, boolean)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param key the unique key for the feature flag + * @param user the user attributes * @param defaultValue the default value of the flag * @return an {@link EvaluationDetail} object * @since 2.3.0 + * @see #boolVariationDetail(String, LDContext, boolean) */ - EvaluationDetail boolVariationDetail(String featureKey, LDUser user, boolean defaultValue); + default EvaluationDetail boolVariationDetail(String key, LDUser user, boolean defaultValue) { + return boolVariationDetail(key, LDContext.fromUser(user), defaultValue); + } /** - * Calculates the value of a feature flag for a given user, and returns an object that describes the - * way the value was determined. The {@code reason} property in the result will also be included in + * Calculates the integer numeric value of a feature flag for a given context, and returns an object + * that describes the way the value was determined. + *

+ * The {@link EvaluationDetail#getReason()} property in the result will also be included in * analytics events, if you are capturing detailed event data for this flag. *

- * If the flag variation has a numeric value that is not an integer, it is rounded toward zero (truncated). + * The behavior is otherwise identical to {@link #intVariation(String, LDContext, int)}. + * + * @param key the unique key for the feature flag + * @param context the evaluation context + * @param defaultValue the default value of the flag + * @return an {@link EvaluationDetail} object + * @see #intVariationDetail(String, LDUser, int) + * @since 6.0.0 + */ + EvaluationDetail intVariationDetail(String key, LDContext context, int defaultValue); + + /** + * Calculates the integer numeric value of a feature flag for a given context, and returns an object + * that describes the way the value was determined. + *

+ * This is equivalent to {@link #intVariationDetail(String, LDContext, int)}, but using the {@link LDUser} type + * instead of {@link LDContext}. * - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + * @param key the unique key for the feature flag + * @param user the user attributes * @param defaultValue the default value of the flag * @return an {@link EvaluationDetail} object + * @see #intVariationDetail(String, LDContext, int) * @since 2.3.0 */ - EvaluationDetail intVariationDetail(String featureKey, LDUser user, int defaultValue); + default EvaluationDetail intVariationDetail(String key, LDUser user, int defaultValue) { + return intVariationDetail(key, LDContext.fromUser(user), defaultValue); + } /** - * Calculates the value of a feature flag for a given user, and returns an object that describes the - * way the value was determined. The {@code reason} property in the result will also be included in + * Calculates the floating-point numeric value of a feature flag for a given context, and returns an + * object that describes the way the value was determined. + *

+ * The {@link EvaluationDetail#getReason()} property in the result will also be included in * analytics events, if you are capturing detailed event data for this flag. - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + *

+ * The behavior is otherwise identical to {@link #doubleVariation(String, LDContext, double)}. + * + * @param key the unique key for the feature flag + * @param context the evaluation context * @param defaultValue the default value of the flag * @return an {@link EvaluationDetail} object + * @see #doubleVariationDetail(String, LDUser, double) + * @since 6.0.0 + */ + EvaluationDetail doubleVariationDetail(String key, LDContext context, double defaultValue); + + /** + * Calculates the floating-point numeric value of a feature flag for a given context, and returns an + * object that describes the way the value was determined. + *

+ * This is equivalent to {@link #doubleVariationDetail(String, LDContext, double)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param key the unique key for the feature flag + * @param user the user attributes + * @param defaultValue the default value of the flag + * @return an {@link EvaluationDetail} object + * @see #doubleVariation(String, LDContext, double) * @since 2.3.0 */ - EvaluationDetail doubleVariationDetail(String featureKey, LDUser user, double defaultValue); + default EvaluationDetail doubleVariationDetail(String key, LDUser user, double defaultValue) { + return doubleVariationDetail(key, LDContext.fromUser(user), defaultValue); + } /** - * Calculates the value of a feature flag for a given user, and returns an object that describes the - * way the value was determined. The {@code reason} property in the result will also be included in + * Calculates the string value of a feature flag for a given context, and returns an object + * that describes the way the value was determined. + *

+ * The {@link EvaluationDetail#getReason()} property in the result will also be included in * analytics events, if you are capturing detailed event data for this flag. - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + *

+ * The behavior is otherwise identical to {@link #stringVariation(String, LDContext, String)}. + * + * @param key the unique key for the feature flag + * @param context the evaluation context + * @param defaultValue the default value of the flag + * @see #stringVariation(String, LDUser, String) + * @return an {@link EvaluationDetail} object + * @since 6.0.0 + */ + EvaluationDetail stringVariationDetail(String key, LDContext context, String defaultValue); + + /** + * Calculates the string value of a feature flag for a given context, and returns an object + * that describes the way the value was determined. + *

+ * This is equivalent to {@link #stringVariationDetail(String, LDContext, String)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param key the unique key for the feature flag + * @param user the user attributes * @param defaultValue the default value of the flag * @return an {@link EvaluationDetail} object + * @see #stringVariationDetail(String, LDContext, String) * @since 2.3.0 */ - EvaluationDetail stringVariationDetail(String featureKey, LDUser user, String defaultValue); + default EvaluationDetail stringVariationDetail(String key, LDUser user, String defaultValue) { + return stringVariationDetail(key, LDContext.fromUser(user), defaultValue); + } + + /** + * Calculates the value of a feature flag for a given context as any JSON value type, and returns an + * object that describes the way the value was determined. + *

+ * The {@link EvaluationDetail#getReason()} property in the result will also be included in + * analytics events, if you are capturing detailed event data for this flag. + *

+ * The behavior is otherwise identical to {@link #jsonValueVariation(String, LDContext, LDValue)}. + * + * @param key the unique key for the feature flag + * @param context the evaluation context + * @param defaultValue the default value of the flag + * @return an {@link EvaluationDetail} object + * @see #jsonValueVariation(String, LDUser, LDValue) + * @since 6.0.0 + */ + EvaluationDetail jsonValueVariationDetail(String key, LDContext context, LDValue defaultValue); /** - * Calculates the {@link LDValue} value of a feature flag for a given user. + * Calculates the value of a feature flag for a given context as any JSON value type, and returns an + * object that describes the way the value was determined. + * that describes the way the value was determined. + *

+ * This is equivalent to {@link #jsonValueVariationDetail(String, LDContext, LDValue)}, but using the {@link LDUser} type + * instead of {@link LDContext}. * - * @param featureKey the unique key for the feature flag - * @param user the end user requesting the flag + * @param key the unique key for the feature flag + * @param user the user attributes * @param defaultValue the default value of the flag * @return an {@link EvaluationDetail} object - * + * @see #jsonValueVariation(String, LDContext, LDValue) * @since 4.8.0 */ - EvaluationDetail jsonValueVariationDetail(String featureKey, LDUser user, LDValue defaultValue); + default EvaluationDetail jsonValueVariationDetail(String key, LDUser user, LDValue defaultValue) { + return jsonValueVariationDetail(key, LDContext.fromUser(user), defaultValue); + } /** * Returns true if the specified feature flag currently exists. @@ -275,28 +638,35 @@ public interface LDClientInterface extends Closeable { DataStoreStatusProvider getDataStoreStatusProvider(); /** - * For more info: https://github.com/launchdarkly/js-client#secure-mode - * @param user the user to be hashed along with the SDK key + * Creates a hash string that can be used by the JavaScript SDK to identify a context. + *

+ * See + * Secure mode in the JavaScript SDK Reference. + * + * @param context the evaluation context * @return the hash, or null if the hash could not be calculated + * @see #secureModeHash(LDUser) + * @since 6.0.0 */ - String secureModeHash(LDUser user); - + String secureModeHash(LDContext context); + /** - * Associates two users for analytics purposes. - * - * This can be helpful in the situation where a person is represented by multiple - * LaunchDarkly users. This may happen, for example, when a person initially logs into - * an application-- the person might be represented by an anonymous user prior to logging - * in and a different user after logging in, as denoted by a different user key. - * - * @param user the newly identified user. - * @param previousUser the previously identified user. - * @since 5.4.0 + * Creates a hash string that can be used by the JavaScript SDK to identify a context. + *

+ * This is equivalent to {@link #secureModeHash(LDContext)}, but using the {@link LDUser} type + * instead of {@link LDContext}. + * + * @param user the user attributes + * @return the hash, or null if the hash could not be calculated + * @see #secureModeHash(LDContext) */ - void alias(LDUser user, LDUser previousUser); + default String secureModeHash(LDUser user) { + return secureModeHash(LDContext.fromUser(user)); + } /** * The current version string of the SDK. + * * @return a string in Semantic Versioning 2.0.0 format */ String version(); diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/LoggingConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/LoggingConfiguration.java deleted file mode 100644 index 45403bcab..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/LoggingConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.logging.LDLogAdapter; -import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder; - -import java.time.Duration; - -/** - * Encapsulates the SDK's general logging configuration. - *

- * Use {@link LoggingConfigurationFactory} to construct an instance. - * - * @since 5.0.0 - */ -public interface LoggingConfiguration { - /** - * The time threshold, if any, after which the SDK will log a data source outage at {@code ERROR} - * level instead of {@code WARN} level. - * - * @return the error logging threshold, or null - * @see LoggingConfigurationBuilder#logDataSourceOutageAsErrorAfter(java.time.Duration) - */ - Duration getLogDataSourceOutageAsErrorAfter(); - - /** - * Returns the configured base logger name. - * @return the logger name - * @since 5.10.0 - */ - default String getBaseLoggerName() { - return null; - } - - /** - * Returns the configured logging adapter. - * @return the logging adapter - * @since 5.10.0 - */ - default LDLogAdapter getLogAdapter() { - return null; - } -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/LoggingConfigurationFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/LoggingConfigurationFactory.java deleted file mode 100644 index 25677e019..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/LoggingConfigurationFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -/** - * Interface for a factory that creates a {@link LoggingConfiguration}. - * - * @see com.launchdarkly.sdk.server.Components#logging() - * @see com.launchdarkly.sdk.server.LDConfig.Builder#logging(LoggingConfigurationFactory) - * @since 5.0.0 - */ -public interface LoggingConfigurationFactory { - /** - * Creates the configuration object. - * - * @param basicConfiguration provides the basic SDK configuration properties - * @return a {@link LoggingConfiguration} - */ - public LoggingConfiguration createLoggingConfiguration(BasicConfiguration basicConfiguration); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/PersistentDataStoreFactory.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/PersistentDataStoreFactory.java deleted file mode 100644 index f86b7b788..000000000 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/PersistentDataStoreFactory.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.launchdarkly.sdk.server.interfaces; - -import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder; - -/** - * Interface for a factory that creates some implementation of a persistent data store. - *

- * This interface is implemented by database integrations. Usage is described in - * {@link com.launchdarkly.sdk.server.Components#persistentDataStore}. - * - * @see com.launchdarkly.sdk.server.Components - * @since 4.12.0 - */ -public interface PersistentDataStoreFactory { - /** - * Called internally from {@link PersistentDataStoreBuilder} to create the implementation object - * for the specific type of data store. - * - * @param context allows access to the client configuration - * @return the implementation object - */ - PersistentDataStore createPersistentDataStore(ClientContext context); -} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/package-info.java b/src/main/java/com/launchdarkly/sdk/server/interfaces/package-info.java index 6206d78d7..c511b3086 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/package-info.java +++ b/src/main/java/com/launchdarkly/sdk/server/interfaces/package-info.java @@ -1,10 +1,16 @@ /** - * The package for interfaces that allow customization of LaunchDarkly components, and interfaces - * to other advanced SDK features. + * Types that are part of the public API, but are not needed for basic use of the SDK. *

- * Most applications will not need to refer to these types. You will use them if you are creating a - * plug-in component, such as a database integration, or if you use advanced features such as - * {@link com.launchdarkly.sdk.server.interfaces.LDClientInterface#getDataStoreStatusProvider()} or - * {@link com.launchdarkly.sdk.server.interfaces.LDClientInterface#getFlagTracker()}. + * Types in this namespace include: + *

    + *
  • The interface {@link com.launchdarkly.sdk.server.interfaces.LDClientInterface}, which + * allow the SDK client to be referenced via an interface rather than the concrete type + * {@link com.launchdarkly.sdk.server.LDClient}.
  • + *
  • Interfaces like {@link com.launchdarkly.sdk.server.interfaces.FlagTracker} that provide a + * facade for some part of the SDK API; these are returned by methods like + * {@link com.launchdarkly.sdk.server.LDClient#getFlagTracker()}.
  • + *
  • Concrete types that are used as parameters within these interfaces, like + * {@link com.launchdarkly.sdk.server.interfaces.FlagChangeEvent}.
  • + *
*/ package com.launchdarkly.sdk.server.interfaces; diff --git a/src/main/java/com/launchdarkly/sdk/server/package-info.java b/src/main/java/com/launchdarkly/sdk/server/package-info.java index 501216981..c3d631596 100644 --- a/src/main/java/com/launchdarkly/sdk/server/package-info.java +++ b/src/main/java/com/launchdarkly/sdk/server/package-info.java @@ -4,7 +4,7 @@ * You will most often use {@link com.launchdarkly.sdk.server.LDClient} (the SDK client) and * {@link com.launchdarkly.sdk.server.LDConfig} (configuration options for the client). *

- * Other commonly used types such as {@link com.launchdarkly.sdk.LDUser} are in the {@code com.launchdarkly.sdk} + * Other commonly used types such as {@link com.launchdarkly.sdk.LDContext} are in the {@code com.launchdarkly.sdk} * package, since those are not server-side-specific and are shared with the LaunchDarkly Android SDK. */ package com.launchdarkly.sdk.server; diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStore.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/BigSegmentStore.java similarity index 97% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStore.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/BigSegmentStore.java index e49d31942..082eb203c 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStore.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/BigSegmentStore.java @@ -1,4 +1,4 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; import java.io.Closeable; diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStoreTypes.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/BigSegmentStoreTypes.java similarity index 99% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStoreTypes.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/BigSegmentStoreTypes.java index 46f5519c0..d4d20bfad 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/BigSegmentStoreTypes.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/BigSegmentStoreTypes.java @@ -1,4 +1,4 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java new file mode 100644 index 000000000..ce47db35c --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/ClientContext.java @@ -0,0 +1,201 @@ +package com.launchdarkly.sdk.server.subsystems; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.server.Components; +import com.launchdarkly.sdk.server.LDConfig.Builder; +import com.launchdarkly.sdk.server.interfaces.ApplicationInfo; +import com.launchdarkly.sdk.server.interfaces.ServiceEndpoints; + +/** + * Context information provided by the {@link com.launchdarkly.sdk.server.LDClient} when creating components. + *

+ * This is passed as a parameter to component factories that implement {@link ComponentConfigurer}. + * Component factories do not receive the entire {@link com.launchdarkly.sdk.server.LDConfig} because + * it could contain factory objects that have mutable state, and because components should not be able + * to access the configurations of unrelated components. + *

+ * The actual implementation class may contain other properties that are only relevant to the built-in + * SDK components and are therefore not part of this base class; this allows the SDK to add its own context + * information as needed without disturbing the public API. + * + * @since 5.0.0 + */ +public class ClientContext { + private final String sdkKey; + private final ApplicationInfo applicationInfo; + private final LDLogger baseLogger; + private final HttpConfiguration http; + private final LoggingConfiguration logging; + private final boolean offline; + private final ServiceEndpoints serviceEndpoints; + private final int threadPriority; + + /** + * Constructor that sets all properties. All should be non-null. + * + * @param sdkKey the SDK key + * @param applicationInfo application metadata properties from + * {@link Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)} + * @param http HTTP configuration properties from {@link Builder#http(ComponentConfigurer)} + * @param logging logging configuration properties from {@link Builder#logging(ComponentConfigurer)} + * @param offline true if the SDK should be entirely offline + * @param serviceEndpoints service endpoint URI properties from + * {@link Builder#serviceEndpoints(com.launchdarkly.sdk.server.integrations.ServiceEndpointsBuilder)} + * @param threadPriority worker thread priority from {@link Builder#threadPriority(int)} + */ + public ClientContext( + String sdkKey, + ApplicationInfo applicationInfo, + HttpConfiguration http, + LoggingConfiguration logging, + boolean offline, + ServiceEndpoints serviceEndpoints, + int threadPriority + ) { + this.sdkKey = sdkKey; + this.applicationInfo = applicationInfo; + this.http = http; + this.logging = logging; + this.offline = offline; + this.serviceEndpoints = serviceEndpoints; + this.threadPriority = threadPriority; + + this.baseLogger = logging == null ? LDLogger.none() : + LDLogger.withAdapter(logging.getLogAdapter(), logging.getBaseLoggerName()); + } + + /** + * Copy constructor. + * + * @param copyFrom the instance to copy from + */ + protected ClientContext(ClientContext copyFrom) { + this(copyFrom.sdkKey, copyFrom.applicationInfo, copyFrom.http, copyFrom.logging, + copyFrom.offline, copyFrom.serviceEndpoints, copyFrom.threadPriority); + } + + /** + * Basic constructor for convenience in testing, using defaults for most properties. + * + * @param sdkKey the SDK key + */ + public ClientContext(String sdkKey) { + this( + sdkKey, + new ApplicationInfo(null, null), + defaultHttp(sdkKey), + defaultLogging(), + false, + Components.serviceEndpoints().createServiceEndpoints(), + Thread.MIN_PRIORITY + ); + } + + private static HttpConfiguration defaultHttp(String sdkKey) { + ClientContext minimalContext = new ClientContext(sdkKey, null, null, null, false, null, 0); + return Components.httpConfiguration().build(minimalContext); + } + + private static LoggingConfiguration defaultLogging() { + ClientContext minimalContext = new ClientContext("", null, null, null, false, null, 0); + return Components.logging().build(minimalContext); + } + + /** + * Returns the configured SDK key. + * + * @return the SDK key + */ + public String getSdkKey() { + return sdkKey; + } + + /** + * Returns the application metadata, if any, set by + * {@link Builder#applicationInfo(com.launchdarkly.sdk.server.integrations.ApplicationInfoBuilder)}. + * + * @return the application metadata or null + */ + public ApplicationInfo getApplicationInfo() { + return applicationInfo; + } + + /** + * The base logger for the SDK. + * @return a logger instance + */ + public LDLogger getBaseLogger() { + return baseLogger; + } + + /** + * Returns the component that {@link DataSource} implementations use to deliver data and status + * updates to the SDK. + *

+ * This component is only available when the SDK is calling a {@link DataSource} factory. + * Otherwise the method returns null. + * + * @return the {@link DataSourceUpdateSink}, if applicable + */ + public DataSourceUpdateSink getDataSourceUpdateSink() { + return null; + } + + /** + * Returns the component that {@link DataStore} implementations use to deliver data store status + * updates to the SDK. + *

+ * This component is only available when the SDK is calling a {@link DataStore} factory. + * Otherwise the method returns null. + * + * @return the {@link DataStoreUpdateSink}, if applicable + */ + public DataStoreUpdateSink getDataStoreUpdateSink() { + return null; + } + + /** + * The configured networking properties that apply to all components. + * + * @return the HTTP configuration + */ + public HttpConfiguration getHttp() { + return http; + } + + /** + * The configured logging properties that apply to all components. + * @return the logging configuration + */ + public LoggingConfiguration getLogging() { + return logging; + } + + /** + * Returns true if the SDK was configured to be completely offline. + * + * @return true if configured to be offline + */ + public boolean isOffline() { + return offline; + } + + /** + * Returns the base service URIs used by SDK components. + * + * @return the service endpoint URIs + */ + public ServiceEndpoints getServiceEndpoints() { + return serviceEndpoints; + } + + /** + * Returns the worker thread priority that is set by + * {@link Builder#threadPriority(int)}. + * + * @return the thread priority + */ + public int getThreadPriority() { + return threadPriority; + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/subsystems/ComponentConfigurer.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/ComponentConfigurer.java new file mode 100644 index 000000000..10a9a0b37 --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/ComponentConfigurer.java @@ -0,0 +1,20 @@ +package com.launchdarkly.sdk.server.subsystems; + +/** + * The common interface for SDK component factories and configuration builders. Applications should not + * need to implement this interface. + * + * @param the type of SDK component or configuration object being constructed + * @since 6.0.0 + */ +public interface ComponentConfigurer { + /** + * Called internally by the SDK to create an implementation instance. Applications should not need + * to call this method. + * + * @param clientContext provides configuration properties and other components from the current + * SDK client instance + * @return a instance of the component type + */ + T build(ClientContext clientContext); +} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSource.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSource.java similarity index 96% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/DataSource.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/DataSource.java index 87075dad3..eb844c00b 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSource.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSource.java @@ -1,4 +1,4 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; import java.io.Closeable; import java.io.IOException; diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceUpdates.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSourceUpdateSink.java similarity index 70% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceUpdates.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/DataSourceUpdateSink.java index 730c784cd..622031d41 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataSourceUpdates.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataSourceUpdateSink.java @@ -1,8 +1,13 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind; +import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; /** * Interface that a data source implementation will use to push data into the SDK. @@ -11,14 +16,15 @@ * that the SDK can perform any other necessary operations that must happen when data is updated. * * @since 5.0.0 + * @see ClientContext#getDataSourceUpdateSink() */ -public interface DataSourceUpdates { +public interface DataSourceUpdateSink { /** * Completely overwrites the current contents of the data store with a set of items for each collection. *

* If the underlying data store throws an error during this operation, the SDK will catch it, log it, - * and set the data source state to {@link DataSourceStatusProvider.State#INTERRUPTED} with an error of - * {@link DataSourceStatusProvider.ErrorKind#STORE_ERROR}. It will not rethrow the error to the data + * and set the data source state to {@link State#INTERRUPTED} with an error of + * {@link ErrorKind#STORE_ERROR}. It will not rethrow the error to the data * source, but will simply return {@code false} to indicate that the operation failed. * * @param allData a list of {@link DataStoreTypes.DataKind} instances and their corresponding data sets @@ -35,8 +41,8 @@ public interface DataSourceUpdates { * they do not overwrite a later update in case updates are received out of order. *

* If the underlying data store throws an error during this operation, the SDK will catch it, log it, - * and set the data source state to {@link DataSourceStatusProvider.State#INTERRUPTED} with an error of - * {@link DataSourceStatusProvider.ErrorKind#STORE_ERROR}. It will not rethrow the error to the data + * and set the data source state to {@link State#INTERRUPTED} with an error of + * {@link ErrorKind#STORE_ERROR}. It will not rethrow the error to the data * source, but will simply return {@code false} to indicate that the operation failed. * * @param kind specifies which collection to use @@ -68,14 +74,14 @@ public interface DataSourceUpdates { * {@link DataSourceStatusProvider#getStatus()}, and will trigger status change events to any * registered listeners. *

- * A special case is that if {@code newState} is {@link DataSourceStatusProvider.State#INTERRUPTED}, - * but the previous state was {@link DataSourceStatusProvider.State#INITIALIZING}, the state will remain - * at {@link DataSourceStatusProvider.State#INITIALIZING} because {@link DataSourceStatusProvider.State#INTERRUPTED} + * A special case is that if {@code newState} is {@link State#INTERRUPTED}, + * but the previous state was {@link State#INITIALIZING}, the state will remain + * at {@link State#INITIALIZING} because {@link State#INTERRUPTED} * is only meaningful after a successful startup. * * @param newState the data source state * @param newError information about a new error, if any * @see DataSourceStatusProvider */ - void updateStatus(DataSourceStatusProvider.State newState, DataSourceStatusProvider.ErrorInfo newError); + void updateStatus(State newState, ErrorInfo newError); } diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStore.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStore.java similarity index 89% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/DataStore.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/DataStore.java index fbfb648a3..e2cd36324 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStore.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStore.java @@ -1,10 +1,11 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import java.io.Closeable; @@ -84,9 +85,9 @@ public interface DataStore extends Closeable { * Returns true if this data store implementation supports status monitoring. *

* This is normally only true for persistent data stores created with - * {@link com.launchdarkly.sdk.server.Components#persistentDataStore(PersistentDataStoreFactory)}, - * but it could also be true for any custom {@link DataStore} implementation that makes use of the - * {@code statusUpdater} parameter provided to {@link DataStoreFactory#createDataStore(ClientContext, DataStoreUpdates)}. + * {@link com.launchdarkly.sdk.server.Components#persistentDataStore(ComponentConfigurer)}, + * but it could also be true for any custom {@link DataStore} implementation that makes use of + * {@link ClientContext#getDataStoreUpdateSink()}. * Returning true means that the store guarantees that if it ever enters an invalid state (that is, an * operation has failed or it knows that operations cannot succeed at the moment), it will publish a * status update, and will then publish another status update once it has returned to a valid state. diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreTypes.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java similarity index 99% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreTypes.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java index a412c7c4f..64677b50c 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreTypes.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreTypes.java @@ -1,4 +1,4 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; import com.google.common.collect.ImmutableList; diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreUpdates.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreUpdateSink.java similarity index 64% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreUpdates.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreUpdateSink.java index bf7646d67..e419842e9 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DataStoreUpdates.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/DataStoreUpdateSink.java @@ -1,14 +1,14 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; + +import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; /** * Interface that a data store implementation can use to report information back to the SDK. - *

- * The {@link DataStoreFactory} receives an implementation of this interface and can pass it to the - * data store that it creates, if desired. * * @since 5.0.0 + * @see ClientContext#getDataStoreUpdateSink() */ -public interface DataStoreUpdates { +public interface DataStoreUpdateSink { /** * Reports a change in the data store's operational status. *

diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/DiagnosticDescription.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/DiagnosticDescription.java similarity index 57% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/DiagnosticDescription.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/DiagnosticDescription.java index 4254c8e02..c54a0a4fd 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/DiagnosticDescription.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/DiagnosticDescription.java @@ -1,4 +1,4 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; import com.launchdarkly.sdk.LDValue; @@ -6,13 +6,11 @@ * Optional interface for components to describe their own configuration. *

* The SDK uses a simplified JSON representation of its configuration when recording diagnostics data. - * Any class that implements {@link com.launchdarkly.sdk.server.interfaces.DataStoreFactory}, - * {@link com.launchdarkly.sdk.server.interfaces.DataSourceFactory}, {@link com.launchdarkly.sdk.server.interfaces.EventProcessorFactory}, - * or {@link com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory} may choose to contribute + * Any class that implements {@link ComponentConfigurer} may choose to contribute * values to this representation, although the SDK may or may not use them. For components that do not * implement this interface, the SDK may instead describe them using {@code getClass().getSimpleName()}. *

- * The {@link #describeConfiguration(BasicConfiguration)} method should return either null or a JSON value. For + * The {@link #describeConfiguration(ClientContext)} method should return either null or a JSON value. For * custom components, the value must be a string that describes the basic nature of this component * implementation (e.g. "Redis"). Built-in LaunchDarkly components may instead return a JSON object * containing multiple properties specific to the LaunchDarkly diagnostic schema. @@ -22,8 +20,8 @@ public interface DiagnosticDescription { /** * Used internally by the SDK to inspect the configuration. - * @param basicConfiguration general SDK configuration properties that are not specific to this component + * @param clientContext allows access to the client configuration * @return an {@link LDValue} or null */ - LDValue describeConfiguration(BasicConfiguration basicConfiguration); + LDValue describeConfiguration(ClientContext clientContext); } diff --git a/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java new file mode 100644 index 000000000..fafda3657 --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/EventProcessor.java @@ -0,0 +1,87 @@ +package com.launchdarkly.sdk.server.subsystems; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; + +import java.io.Closeable; + +/** + * Interface for an object that can send or store analytics events. + *

+ * Application code normally does not need to interact with this interface. It is provided + * to allow a custom implementation or test fixture to be substituted for the SDK's normal + * analytics event logic. + * + * @since 4.0.0 + */ +public interface EventProcessor extends Closeable { + /** + * Constant used with {@link #recordEvaluationEvent}. + */ + public static final int NO_VERSION = -1; + + /** + * Records the action of evaluating a feature flag. + *

+ * Depending on the feature flag properties and event properties, this may be transmitted to + * the events service as an individual event, or may only be added into summary data. + * + * @param context the evaluation context + * @param flagKey key of the feature flag that was evaluated + * @param flagVersion the version of the flag, or {@link #NO_VERSION} if the flag was not found + * @param variation the result variation index, or {@link EvaluationDetail#NO_VARIATION} if evaluation failed + * @param value the result value + * @param reason the evaluation reason, or null if the reason was not requested + * @param defaultValue the default value parameter for the evaluation + * @param prerequisiteOfFlagKey the key of the flag that this flag was evaluated as a prerequisite of, + * or null if this flag was evaluated for itself + * @param requireFullEvent true if full-fidelity analytics events should be sent for this flag + * @param debugEventsUntilDate if non-null, debug events are to be generated until this millisecond time + */ + void recordEvaluationEvent( + LDContext context, + String flagKey, + int flagVersion, + int variation, + LDValue value, + EvaluationReason reason, + LDValue defaultValue, + String prerequisiteOfFlagKey, + boolean requireFullEvent, + Long debugEventsUntilDate + ); + + /** + * Registers an evaluation context, as when the SDK's {@code identify} method is called. + * + * @param context the evaluation context + */ + void recordIdentifyEvent( + LDContext context + ); + + /** + * Creates a custom event, as when the SDK's {@code track} method is called. + * + * @param context the evaluation context + * @param eventKey the event key + * @param data optional custom data provided for the event, may be null or {@link LDValue#ofNull()} if not used + * @param metricValue optional numeric metric value provided for the event, or null + */ + void recordCustomEvent( + LDContext context, + String eventKey, + LDValue data, + Double metricValue + ); + + /** + * Specifies that any buffered events should be sent as soon as possible, rather than waiting + * for the next flush interval. This method is asynchronous, so events still may not be sent + * until a later time. However, calling {@link Closeable#close()} will synchronously deliver + * any events that were not yet delivered prior to shutting down. + */ + void flush(); +} diff --git a/src/main/java/com/launchdarkly/sdk/server/subsystems/EventSender.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/EventSender.java new file mode 100644 index 000000000..20b4e29f5 --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/EventSender.java @@ -0,0 +1,60 @@ +package com.launchdarkly.sdk.server.subsystems; + +import java.io.Closeable; +import java.net.URI; + +/** + * Interface for a component that can deliver preformatted event data. + *

+ * By default, the SDK sends event data to the LaunchDarkly events service via HTTP. You may + * provide a different implementation of event delivery by implementing this interface-- for + * instance, to create a test fixture, or to store the data somewhere else. + * + * @see com.launchdarkly.sdk.server.integrations.EventProcessorBuilder#eventSender(ComponentConfigurer) + * @since 4.14.0 + */ +public interface EventSender extends Closeable { + /** + * Result type for event sending methods. + */ + public enum Result { + /** + * The EventSender successfully delivered the event(s). + */ + SUCCESS, + + /** + * The EventSender was not able to deliver the events. + */ + FAILURE, + + /** + * The EventSender was not able to deliver the events, and the nature of the error indicates that + * the SDK should not attempt to send any more events. + */ + STOP + }; + + /** + * Attempt to deliver an analytics event data payload. + *

+ * This method will be called synchronously from an event delivery worker thread. + * + * @param data the preformatted JSON data, in UTF-8 encoding + * @param eventCount the number of individual events in the data + * @param eventsBaseUri the configured events endpoint base URI + * @return a {@link Result} + */ + Result sendAnalyticsEvents(byte[] data, int eventCount, URI eventsBaseUri); + + /** + * Attempt to deliver a diagnostic event data payload. + *

+ * This method will be called synchronously from an event delivery worker thread. + * + * @param data the preformatted JSON data, in UTF-8 encoding + * @param eventsBaseUri the configured events endpoint base URI + * @return a {@link Result} + */ + Result sendDiagnosticEvent(byte[] data, URI eventsBaseUri); +} diff --git a/src/main/java/com/launchdarkly/sdk/server/subsystems/HttpConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/HttpConfiguration.java new file mode 100644 index 000000000..d0b25ec4d --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/HttpConfiguration.java @@ -0,0 +1,141 @@ +package com.launchdarkly.sdk.server.subsystems; + +import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder; +import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; + +import java.net.Proxy; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.X509TrustManager; + +import static java.util.Collections.emptyMap; + +/** + * Encapsulates top-level HTTP configuration that applies to all SDK components. + *

+ * Use {@link HttpConfigurationBuilder} to construct an instance. + *

+ * The SDK's built-in components use OkHttp as the HTTP client implementation, but since OkHttp types + * are not surfaced in the public API and custom components might use some other implementation, this + * class only provides the properties that would be used to create an HTTP client; it does not create + * the client itself. SDK implementation code uses its own helper methods to do so. + * + * @since 4.13.0 + */ +public final class HttpConfiguration { + private final Duration connectTimeout; + private final Map defaultHeaders; + private final Proxy proxy; + private final HttpAuthentication proxyAuthentication; + private final SocketFactory socketFactory; + private final Duration socketTimeout; + private final SSLSocketFactory sslSocketFactory; + private final X509TrustManager trustManager; + + /** + * Creates an instance. + * + * @param connectTimeout see {@link #getConnectTimeout()} + * @param defaultHeaders see {@link #getDefaultHeaders()} + * @param proxy see {@link #getProxy()} + * @param proxyAuthentication see {@link #getProxyAuthentication()} + * @param socketFactory see {@link #getSocketFactory()} + * @param socketTimeout see {@link #getSocketTimeout()} + * @param sslSocketFactory see {@link #getSslSocketFactory()} + * @param trustManager see {@link #getTrustManager()} + */ + public HttpConfiguration(Duration connectTimeout, Map defaultHeaders, Proxy proxy, + HttpAuthentication proxyAuthentication, SocketFactory socketFactory, Duration socketTimeout, + SSLSocketFactory sslSocketFactory, X509TrustManager trustManager) { + super(); + this.connectTimeout = connectTimeout == null ? HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT : connectTimeout; + this.defaultHeaders = defaultHeaders == null ? emptyMap() : new HashMap<>(defaultHeaders); + this.proxy = proxy; + this.proxyAuthentication = proxyAuthentication; + this.socketFactory = socketFactory; + this.socketTimeout = socketTimeout == null ? HttpConfigurationBuilder.DEFAULT_SOCKET_TIMEOUT : socketTimeout; + this.sslSocketFactory = sslSocketFactory; + this.trustManager = trustManager; + } + + /** + * The connection timeout. This is the time allowed for the underlying HTTP client to connect + * to the LaunchDarkly server. + * + * @return the connection timeout; never null + */ + public Duration getConnectTimeout() { + return connectTimeout; + } + + /** + * Returns the basic headers that should be added to all HTTP requests from SDK components to + * LaunchDarkly services, based on the current SDK configuration. + * + * @return a list of HTTP header names and values + */ + public Iterable> getDefaultHeaders() { + return defaultHeaders.entrySet(); + } + + /** + * The proxy configuration, if any. + * + * @return a {@link Proxy} instance or null + */ + public Proxy getProxy() { + return proxy; + } + + /** + * The authentication method to use for a proxy, if any. Ignored if {@link #getProxy()} is null. + * + * @return an {@link HttpAuthentication} implementation or null + */ + public HttpAuthentication getProxyAuthentication() { + return proxyAuthentication; + } + + /** + * The socket timeout. This is the amount of time without receiving data on a connection that the + * SDK will tolerate before signaling an error. This does not apply to the streaming connection + * used by {@link com.launchdarkly.sdk.server.Components#streamingDataSource()}, which has its own + * non-configurable read timeout based on the expected behavior of the LaunchDarkly streaming service. + * + * @return the socket timeout; never null + */ + public Duration getSocketTimeout() { + return socketTimeout; + } + + /** + * The configured socket factory for insecure connections. + * + * @return a SocketFactory or null + */ + public SocketFactory getSocketFactory() { + return socketFactory; + } + + /** + * The configured socket factory for secure connections. + * + * @return a SSLSocketFactory or null + */ + public SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } + + /** + * The configured trust manager for secure connections, if custom certificate verification is needed. + * + * @return an X509TrustManager or null + */ + public X509TrustManager getTrustManager() { + return trustManager; + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/subsystems/LoggingConfiguration.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/LoggingConfiguration.java new file mode 100644 index 000000000..51bfc690a --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/LoggingConfiguration.java @@ -0,0 +1,65 @@ +package com.launchdarkly.sdk.server.subsystems; + +import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder; + +import java.time.Duration; + +/** + * Encapsulates the SDK's general logging configuration. + *

+ * Use {@link LoggingConfigurationBuilder} to construct an instance. + * + * @since 5.0.0 + */ +public final class LoggingConfiguration { + private final String baseLoggerName; + private final LDLogAdapter logAdapter; + private final Duration logDataSourceOutageAsErrorAfter; + + /** + * Creates an instance. + * + * @param baseLoggerName see {@link #getBaseLoggerName()} + * @param logAdapter see {@link #getLogAdapter()} + * @param logDataSourceOutageAsErrorAfter see {@link #getLogDataSourceOutageAsErrorAfter()} + */ + public LoggingConfiguration( + String baseLoggerName, + LDLogAdapter logAdapter, + Duration logDataSourceOutageAsErrorAfter + ) { + this.baseLoggerName = baseLoggerName; + this.logAdapter = logAdapter; + this.logDataSourceOutageAsErrorAfter = logDataSourceOutageAsErrorAfter; + } + + /** + * Returns the configured base logger name. + * @return the logger name + * @since 5.10.0 + */ + public String getBaseLoggerName() { + return baseLoggerName; + } + + /** + * Returns the configured logging adapter. + * @return the logging adapter + * @since 5.10.0 + */ + public LDLogAdapter getLogAdapter() { + return logAdapter; + } + + /** + * The time threshold, if any, after which the SDK will log a data source outage at {@code ERROR} + * level instead of {@code WARN} level. + * + * @return the error logging threshold, or null + * @see LoggingConfigurationBuilder#logDataSourceOutageAsErrorAfter(java.time.Duration) + */ + public Duration getLogDataSourceOutageAsErrorAfter() { + return logDataSourceOutageAsErrorAfter; + } +} diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/PersistentDataStore.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/PersistentDataStore.java similarity index 96% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/PersistentDataStore.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/PersistentDataStore.java index 03dea02d7..1f4cfdafa 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/PersistentDataStore.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/PersistentDataStore.java @@ -1,9 +1,9 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; import java.io.Closeable; diff --git a/src/main/java/com/launchdarkly/sdk/server/interfaces/SerializationException.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/SerializationException.java similarity index 94% rename from src/main/java/com/launchdarkly/sdk/server/interfaces/SerializationException.java rename to src/main/java/com/launchdarkly/sdk/server/subsystems/SerializationException.java index 89256a6fd..c714e9644 100644 --- a/src/main/java/com/launchdarkly/sdk/server/interfaces/SerializationException.java +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/SerializationException.java @@ -1,4 +1,4 @@ -package com.launchdarkly.sdk.server.interfaces; +package com.launchdarkly.sdk.server.subsystems; /** * General exception class for all errors in serializing or deserializing JSON. diff --git a/src/main/java/com/launchdarkly/sdk/server/subsystems/package-info.java b/src/main/java/com/launchdarkly/sdk/server/subsystems/package-info.java new file mode 100644 index 000000000..12031bd3b --- /dev/null +++ b/src/main/java/com/launchdarkly/sdk/server/subsystems/package-info.java @@ -0,0 +1,13 @@ +/** + * Interfaces for implementation of LaunchDarkly SDK components. + *

+ * Most applications will not need to refer to these types. You will use them if you are creating a + * plugin component, such as a database integration. They are also used as interfaces for the built-in + * SDK components, so that plugin components can be used interchangeably with those: for instance, the + * configuration method {@link com.launchdarkly.sdk.server.LDConfig.Builder#dataStore(ComponentConfigurer)} + * references {@link com.launchdarkly.sdk.server.subsystems.DataStore} as an abstraction for the data + * store component. + *

+ * The package also includes concrete types that are used as parameters within these interfaces. + */ +package com.launchdarkly.sdk.server.subsystems; diff --git a/src/test/java/com/launchdarkly/sdk/server/BaseTest.java b/src/test/java/com/launchdarkly/sdk/server/BaseTest.java index 0aa5ac17b..19c81c588 100644 --- a/src/test/java/com/launchdarkly/sdk/server/BaseTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/BaseTest.java @@ -1,6 +1,7 @@ package com.launchdarkly.sdk.server; import com.launchdarkly.logging.LDLogAdapter; +import com.launchdarkly.logging.LDLogLevel; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LogCapture; import com.launchdarkly.logging.Logs; @@ -36,7 +37,7 @@ protected LDConfig.Builder baseConfig() { return new LDConfig.Builder() .dataSource(Components.externalUpdatesOnly()) .events(Components.noEvents()) - .logging(Components.logging(testLogging)); + .logging(Components.logging(testLogging).level(LDLogLevel.DEBUG)); } class DumpLogIfTestFails extends TestWatcher { diff --git a/src/test/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapperTest.java b/src/test/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapperTest.java index fc4354ab1..60dc11e8f 100644 --- a/src/test/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapperTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/BigSegmentStoreWrapperTest.java @@ -1,26 +1,16 @@ package com.launchdarkly.sdk.server; -import static com.launchdarkly.sdk.server.TestComponents.clientContext; -import static com.launchdarkly.sdk.server.TestComponents.nullLogger; -import static com.launchdarkly.sdk.server.TestComponents.sharedExecutor; -import static com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.createMembershipFromSegmentRefs; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.isA; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; import com.launchdarkly.sdk.server.BigSegmentStoreWrapper.BigSegmentsQueryResult; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider.Status; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider.StatusListener; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.Membership; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.StoreMetadata; import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; -import com.launchdarkly.sdk.server.interfaces.ClientContext; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.Membership; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.StoreMetadata; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; import org.easymock.EasyMockSupport; import org.junit.Before; @@ -33,6 +23,16 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import static com.launchdarkly.sdk.server.TestComponents.clientContext; +import static com.launchdarkly.sdk.server.TestComponents.nullLogger; +import static com.launchdarkly.sdk.server.TestComponents.sharedExecutor; +import static com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.createMembershipFromSegmentRefs; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + @SuppressWarnings("javadoc") public class BigSegmentStoreWrapperTest extends BaseTest { private static final String SDK_KEY = "sdk-key"; @@ -41,9 +41,10 @@ public class BigSegmentStoreWrapperTest extends BaseTest { private AtomicBoolean storeUnavailable; private AtomicReference storeMetadata; private BigSegmentStore storeMock; - private BigSegmentStoreFactory storeFactoryMock; + private ComponentConfigurer storeFactoryMock; private EventBroadcasterImpl eventBroadcaster; + @SuppressWarnings("unchecked") @Before public void setup() { eventBroadcaster = EventBroadcasterImpl.forBigSegmentStoreStatus(sharedExecutor, nullLogger); @@ -56,8 +57,8 @@ public void setup() { } return storeMetadata.get(); }).anyTimes(); - storeFactoryMock = mocks.strictMock(BigSegmentStoreFactory.class); - expect(storeFactoryMock.createBigSegmentStore(isA(ClientContext.class))).andReturn(storeMock); + storeFactoryMock = mocks.strictMock(ComponentConfigurer.class); + expect(storeFactoryMock.build(isA(ClientContext.class))).andReturn(storeMock); } private BigSegmentStoreWrapper makeWrapper(BigSegmentsConfiguration bsConfig) { @@ -79,7 +80,7 @@ public void membershipQueryWithUncachedResultAndHealthyStatus() throws Exception storeMetadata.set(new StoreMetadata(System.currentTimeMillis())); BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .staleAfter(Duration.ofDays(1)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { BigSegmentsQueryResult res = wrapper.getUserMembership(userKey); assertEquals(expectedMembership, res.membership); @@ -96,7 +97,7 @@ public void membershipQueryReturnsNull() throws Exception { storeMetadata.set(new StoreMetadata(System.currentTimeMillis())); BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .staleAfter(Duration.ofDays(1)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { BigSegmentsQueryResult res = wrapper.getUserMembership(userKey); assertEquals(createMembershipFromSegmentRefs(null, null), res.membership); @@ -115,7 +116,7 @@ public void membershipQueryWithCachedResultAndHealthyStatus() throws Exception { storeMetadata.set(new StoreMetadata(System.currentTimeMillis())); BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .staleAfter(Duration.ofDays(1)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { BigSegmentsQueryResult res1 = wrapper.getUserMembership(userKey); assertEquals(expectedMembership, res1.membership); @@ -138,7 +139,7 @@ public void membershipQueryWithStaleStatus() throws Exception { storeMetadata.set(new StoreMetadata(System.currentTimeMillis() - 1000)); BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .staleAfter(Duration.ofMillis(500)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { BigSegmentsQueryResult res = wrapper.getUserMembership(userKey); assertEquals(expectedMembership, res.membership); @@ -157,7 +158,7 @@ public void membershipQueryWithStaleStatusDueToNoStoreMetadata() throws Exceptio storeMetadata.set(null); BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .staleAfter(Duration.ofMillis(500)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { BigSegmentsQueryResult res = wrapper.getUserMembership(userKey); assertEquals(expectedMembership, res.membership); @@ -182,7 +183,7 @@ public void leastRecentUserIsEvictedFromCache() throws Exception { BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .userCacheSize(2) .staleAfter(Duration.ofDays(1)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { BigSegmentsQueryResult res1 = wrapper.getUserMembership(userKey1); assertEquals(expectedMembership1, res1.membership); @@ -218,7 +219,7 @@ public void pollingDetectsStoreUnavailability() throws Exception { BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .statusPollInterval(Duration.ofMillis(10)) .staleAfter(Duration.ofDays(1)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { assertTrue(wrapper.getStatus().isAvailable()); @@ -250,7 +251,7 @@ public void pollingDetectsStaleStatus() throws Exception { BigSegmentsConfiguration bsConfig = Components.bigSegments(storeFactoryMock) .statusPollInterval(Duration.ofMillis(10)) .staleAfter(Duration.ofMillis(200)) - .createBigSegmentsConfiguration(clientContext(SDK_KEY, new LDConfig.Builder().build())); + .build(clientContext(SDK_KEY, new LDConfig.Builder().build())); try (BigSegmentStoreWrapper wrapper = makeWrapper(bsConfig)) { assertFalse(wrapper.getStatus().isStale()); diff --git a/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java b/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java index 097e28ce9..49d9975cb 100644 --- a/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/ClientContextImplTest.java @@ -2,10 +2,9 @@ import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.sdk.server.interfaces.LoggingConfiguration; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import org.junit.Test; @@ -27,12 +26,11 @@ public class ClientContextImplTest { public void getBasicDefaultProperties() { LDConfig config = new LDConfig.Builder().build(); - ClientContext c = new ClientContextImpl(SDK_KEY, config, null, null); + ClientContext c = ClientContextImpl.fromConfig(SDK_KEY, config, null); - BasicConfiguration basicConfig = c.getBasic(); - assertEquals(SDK_KEY, basicConfig.getSdkKey()); - assertFalse(basicConfig.isOffline()); - assertEquals(Thread.MIN_PRIORITY, basicConfig.getThreadPriority()); + assertEquals(SDK_KEY, c.getSdkKey()); + assertFalse(c.isOffline()); + assertEquals(Thread.MIN_PRIORITY, c.getThreadPriority()); HttpConfiguration httpConfig = c.getHttp(); assertEquals(HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT, httpConfig.getConnectTimeout()); @@ -51,12 +49,11 @@ public void getBasicPropertiesWithCustomConfig() { .threadPriority(Thread.MAX_PRIORITY) .build(); - ClientContext c = new ClientContextImpl(SDK_KEY, config, sharedExecutor, null); + ClientContext c = ClientContextImpl.fromConfig(SDK_KEY, config, sharedExecutor); - BasicConfiguration basicConfig = c.getBasic(); - assertEquals(SDK_KEY, basicConfig.getSdkKey()); - assertTrue(basicConfig.isOffline()); - assertEquals(Thread.MAX_PRIORITY, basicConfig.getThreadPriority()); + assertEquals(SDK_KEY, c.getSdkKey()); + assertTrue(c.isOffline()); + assertEquals(Thread.MAX_PRIORITY, c.getThreadPriority()); HttpConfiguration httpConfig = c.getHttp(); assertEquals(Duration.ofSeconds(10), httpConfig.getConnectTimeout()); @@ -69,7 +66,7 @@ public void getBasicPropertiesWithCustomConfig() { public void getPackagePrivateSharedExecutor() { LDConfig config = new LDConfig.Builder().build(); - ClientContext c = new ClientContextImpl(SDK_KEY, config, sharedExecutor, null); + ClientContext c = ClientContextImpl.fromConfig(SDK_KEY, config, sharedExecutor); assertSame(sharedExecutor, ClientContextImpl.get(c).sharedExecutor); } @@ -78,70 +75,36 @@ public void getPackagePrivateSharedExecutor() { public void getPackagePrivateDiagnosticAccumulator() { LDConfig config = new LDConfig.Builder().build(); - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - - ClientContext c = new ClientContextImpl(SDK_KEY, config, sharedExecutor, diagnosticAccumulator); + ClientContext c = ClientContextImpl.fromConfig(SDK_KEY, config, sharedExecutor); - assertSame(diagnosticAccumulator, ClientContextImpl.get(c).diagnosticAccumulator); + assertNotNull(ClientContextImpl.get(c).diagnosticStore); } @Test - public void diagnosticAccumulatorIsNullIfOptedOut() { + public void diagnosticStoreIsNullIfOptedOut() { LDConfig config = new LDConfig.Builder() .diagnosticOptOut(true) .build(); - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - - ClientContext c = new ClientContextImpl(SDK_KEY, config, sharedExecutor, diagnosticAccumulator); + ClientContext c = ClientContextImpl.fromConfig(SDK_KEY, config, sharedExecutor); - assertNull(ClientContextImpl.get(c).diagnosticAccumulator); - assertNull(ClientContextImpl.get(c).diagnosticInitEvent); - } - - @Test - public void getPackagePrivateDiagnosticInitEvent() { - LDConfig config = new LDConfig.Builder().build(); - - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - - ClientContext c = new ClientContextImpl(SDK_KEY, config, sharedExecutor, diagnosticAccumulator); - - assertNotNull(ClientContextImpl.get(c).diagnosticInitEvent); + assertNull(ClientContextImpl.get(c).diagnosticStore); } @Test public void packagePrivatePropertiesHaveDefaultsIfContextIsNotOurImplementation() { // This covers a scenario where a user has created their own ClientContext and it has been // passed to one of our SDK components. - ClientContext c = new SomeOtherContextImpl(); + ClientContext c = new ClientContext(SDK_KEY); ClientContextImpl impl = ClientContextImpl.get(c); assertNotNull(impl.sharedExecutor); - assertNull(impl.diagnosticAccumulator); - assertNull(impl.diagnosticInitEvent); + assertNull(impl.diagnosticStore); ClientContextImpl impl2 = ClientContextImpl.get(c); assertNotNull(impl2.sharedExecutor); assertSame(impl.sharedExecutor, impl2.sharedExecutor); } - - private static final class SomeOtherContextImpl implements ClientContext { - public BasicConfiguration getBasic() { - return new BasicConfiguration(SDK_KEY, false, Thread.MIN_PRIORITY, null, null); - } - - public HttpConfiguration getHttp() { - return null; - } - - public LoggingConfiguration getLogging() { - return null; - } - } } diff --git a/src/test/java/com/launchdarkly/sdk/server/DataModelDependenciesTest.java b/src/test/java/com/launchdarkly/sdk/server/DataModelDependenciesTest.java index 280d37227..e92603b33 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataModelDependenciesTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataModelDependenciesTest.java @@ -5,7 +5,6 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Operator; import com.launchdarkly.sdk.server.DataModel.Segment; @@ -13,9 +12,9 @@ import com.launchdarkly.sdk.server.DataModelDependencies.KindAndKey; import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; import com.launchdarkly.sdk.server.DataStoreTestTypes.TestItem; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import org.junit.Test; @@ -30,10 +29,12 @@ import static com.launchdarkly.sdk.server.DataStoreTestTypes.TEST_ITEMS; import static com.launchdarkly.sdk.server.DataStoreTestTypes.toDataMap; import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.prerequisite; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; +import static com.launchdarkly.sdk.server.ModelBuilders.segmentRuleBuilder; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.emptyIterable; @@ -65,13 +66,13 @@ public void computeDependenciesFromFlag() { .rules( ruleBuilder() .clauses( - clause(UserAttribute.KEY, Operator.in, LDValue.of("ignore")), - clause(null, Operator.segmentMatch, LDValue.of("segment1"), LDValue.of("segment2")) + clause("key", Operator.in, LDValue.of("ignore")), + clauseMatchingSegment("segment1", "segment2") ) .build(), ruleBuilder() .clauses( - clause(null, Operator.segmentMatch, LDValue.of("segment3")) + clauseMatchingSegment("segment3") ) .build() ) @@ -208,7 +209,13 @@ public void dependencyTrackerReturnsSingleValueResultForUnknownItem() { public void dependencyTrackerBuildsGraph() { DependencyTracker dt = new DependencyTracker(); - FeatureFlag flag1 = ModelBuilders.flagBuilder("flag1") + Segment segment1 = segmentBuilder("segment1").build(); + Segment segment2 = segmentBuilder("segment2"). + rules(segmentRuleBuilder().clauses(clauseMatchingSegment("segment3")).build()) + .build(); + Segment segment3 = segmentBuilder("segment3").build(); + + FeatureFlag flag1 = flagBuilder("flag1") .prerequisites( prerequisite("flag2", 0), prerequisite("flag3", 0) @@ -216,27 +223,31 @@ public void dependencyTrackerBuildsGraph() { .rules( ruleBuilder() .clauses( - clause(null, Operator.segmentMatch, LDValue.of("segment1"), LDValue.of("segment2")) + clauseMatchingSegment("segment1", "segment2") ) .build() ) .build(); - dt.updateDependenciesFrom(FEATURES, flag1.getKey(), new ItemDescriptor(flag1.getVersion(), flag1)); - FeatureFlag flag2 = ModelBuilders.flagBuilder("flag2") + FeatureFlag flag2 = flagBuilder("flag2") .prerequisites( prerequisite("flag4", 0) ) .rules( ruleBuilder() .clauses( - clause(null, Operator.segmentMatch, LDValue.of("segment2")) + clauseMatchingSegment("segment2") ) .build() ) .build(); - dt.updateDependenciesFrom(FEATURES, flag2.getKey(), new ItemDescriptor(flag2.getVersion(), flag2)); + for (Segment s: new Segment[] {segment1, segment2, segment3}) { + dt.updateDependenciesFrom(SEGMENTS, s.getKey(), new ItemDescriptor(s.getVersion(), s)); + } + for (FeatureFlag f: new FeatureFlag[] {flag1, flag2}) { + dt.updateDependenciesFrom(FEATURES, f.getKey(), new ItemDescriptor(f.getVersion(), f)); + } // a change to flag1 affects only flag1 verifyAffectedItems(dt, FEATURES, "flag1", @@ -262,6 +273,13 @@ public void dependencyTrackerBuildsGraph() { new KindAndKey(SEGMENTS, "segment2"), new KindAndKey(FEATURES, "flag1"), new KindAndKey(FEATURES, "flag2")); + + // a change to segment3 affects segment3, segment2, flag1, and flag2 + verifyAffectedItems(dt, SEGMENTS, "segment3", + new KindAndKey(SEGMENTS, "segment3"), + new KindAndKey(SEGMENTS, "segment2"), + new KindAndKey(FEATURES, "flag1"), + new KindAndKey(FEATURES, "flag2")); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/DataModelPreprocessingTest.java b/src/test/java/com/launchdarkly/sdk/server/DataModelPreprocessingTest.java index ea6171e1b..4e835eb5c 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataModelPreprocessingTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataModelPreprocessingTest.java @@ -5,7 +5,6 @@ import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Operator; @@ -22,6 +21,9 @@ import java.time.ZonedDateTime; import java.util.List; +import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; +import static com.launchdarkly.sdk.server.ModelBuilders.segmentRuleBuilder; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; @@ -37,7 +39,7 @@ public class DataModelPreprocessingTest { private static final LDValue aValue = LDValue.of("a"), bValue = LDValue.of("b"); private FeatureFlag flagFromClause(Clause c) { - return new FeatureFlag("key", 0, false, null, null, null, rulesFromClause(c), + return new FeatureFlag("key", 0, false, null, null, null, null, rulesFromClause(c), null, null, null, false, false, false, null, false); } @@ -47,7 +49,7 @@ private List rulesFromClause(Clause c) { @Test public void preprocessFlagAddsPrecomputedOffResult() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, ImmutableList.of(), null, 0, ImmutableList.of(aValue, bValue), @@ -62,7 +64,7 @@ public void preprocessFlagAddsPrecomputedOffResult() { @Test public void preprocessFlagAddsPrecomputedOffResultForNullOffVariation() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, ImmutableList.of(), null, null, ImmutableList.of(aValue, bValue), @@ -77,7 +79,7 @@ public void preprocessFlagAddsPrecomputedOffResultForNullOffVariation() { @Test public void preprocessFlagAddsPrecomputedFallthroughResults() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, ImmutableList.of(), null, 0, ImmutableList.of(aValue, bValue), false, false, false, null, false); @@ -103,8 +105,8 @@ public void preprocessFlagAddsPrecomputedFallthroughResults() { @Test public void preprocessFlagAddsPrecomputedTargetMatchResults() { FeatureFlag f = new FeatureFlag("key", 0, false, null, null, - ImmutableList.of(new Target(ImmutableSet.of(), 1)), - ImmutableList.of(), null, 0, + ImmutableList.of(new Target(null, ImmutableSet.of(), 1)), + null, ImmutableList.of(), null, 0, ImmutableList.of(aValue, bValue), false, false, false, null, false); @@ -120,7 +122,7 @@ public void preprocessFlagAddsPrecomputedTargetMatchResults() { public void preprocessFlagAddsPrecomputedPrerequisiteFailedResults() { FeatureFlag f = new FeatureFlag("key", 0, false, ImmutableList.of(new Prerequisite("abc", 1)), - null, null, + null, null, null, ImmutableList.of(), null, 0, ImmutableList.of(aValue, bValue), false, false, false, null, false); @@ -135,7 +137,7 @@ public void preprocessFlagAddsPrecomputedPrerequisiteFailedResults() { @Test public void preprocessFlagAddsPrecomputedResultsToFlagRules() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, + FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, ImmutableList.of(new Rule("ruleid0", ImmutableList.of(), null, null, false)), null, null, ImmutableList.of(aValue, bValue), @@ -162,11 +164,10 @@ public void preprocessFlagAddsPrecomputedResultsToFlagRules() { @Test public void preprocessFlagCreatesClauseValuesMapForMultiValueEqualityTest() { - Clause c = new Clause( - UserAttribute.forName("x"), + Clause c = clause( + "x", Operator.in, - ImmutableList.of(LDValue.of("a"), LDValue.of(0)), - false + LDValue.of("a"), LDValue.of(0) ); FeatureFlag f = flagFromClause(c); @@ -181,11 +182,10 @@ public void preprocessFlagCreatesClauseValuesMapForMultiValueEqualityTest() { @Test public void preprocessFlagDoesNotCreateClauseValuesMapForSingleValueEqualityTest() { - Clause c = new Clause( - UserAttribute.forName("x"), + Clause c = clause( + "x", Operator.in, - ImmutableList.of(LDValue.of("a")), - false + LDValue.of("a") ); FeatureFlag f = flagFromClause(c); @@ -198,11 +198,9 @@ public void preprocessFlagDoesNotCreateClauseValuesMapForSingleValueEqualityTest @Test public void preprocessFlagDoesNotCreateClauseValuesMapForEmptyEqualityTest() { - Clause c = new Clause( - UserAttribute.forName("x"), - Operator.in, - ImmutableList.of(), - false + Clause c = clause( + "x", + Operator.in ); FeatureFlag f = flagFromClause(c); @@ -215,21 +213,20 @@ public void preprocessFlagDoesNotCreateClauseValuesMapForEmptyEqualityTest() { @Test public void preprocessFlagDoesNotCreateClauseValuesMapForNonEqualityOperators() { - for (Operator op: Operator.values()) { + for (Operator op: Operator.getBuiltins()) { if (op == Operator.in) { continue; } - Clause c = new Clause( - UserAttribute.forName("x"), + Clause c = clause( + "x", op, - ImmutableList.of(LDValue.of("a"), LDValue.of("b")), - false + LDValue.of("a"), LDValue.of("b") ); // The values & types aren't very important here because we won't actually evaluate the clause; all that // matters is that there's more than one of them, so that it *would* build a map if the operator were "in" FeatureFlag f = flagFromClause(c); - assertNull(op.name(), f.getRules().get(0).getClauses().get(0).preprocessed); + assertNull(op.toString(), f.getRules().get(0).getClauses().get(0).preprocessed); f.afterDeserialized(); @@ -249,11 +246,10 @@ public void preprocessFlagParsesClauseDate() { Instant time2 = Instant.ofEpochMilli(time2Num); for (Operator op: new Operator[] { Operator.after, Operator.before }) { - Clause c = new Clause( - UserAttribute.forName("x"), + Clause c = clause( + "x", op, - ImmutableList.of(LDValue.of(time1Str), LDValue.of(time2Num), LDValue.of("x"), LDValue.of(false)), - false + LDValue.of(time1Str), LDValue.of(time2Num), LDValue.of("x"), LDValue.of(false) ); FeatureFlag f = flagFromClause(c); @@ -274,11 +270,10 @@ public void preprocessFlagParsesClauseDate() { @Test public void preprocessFlagParsesClauseRegex() { - Clause c = new Clause( - UserAttribute.forName("x"), + Clause c = clause( + "x", Operator.matches, - ImmutableList.of(LDValue.of("x*"), LDValue.of("***not a regex"), LDValue.of(3)), - false + LDValue.of("x*"), LDValue.of("***not a regex"), LDValue.of(3) ); FeatureFlag f = flagFromClause(c); @@ -303,11 +298,10 @@ public void preprocessFlagParsesClauseSemVer() { assertNotNull(expected); for (Operator op: new Operator[] { Operator.semVerEqual, Operator.semVerGreaterThan, Operator.semVerLessThan }) { - Clause c = new Clause( - UserAttribute.forName("x"), + Clause c = clause( + "x", op, - ImmutableList.of(LDValue.of("1.2.3"), LDValue.of("x"), LDValue.of(false)), - false + LDValue.of("1.2.3"), LDValue.of("x"), LDValue.of(false) ); FeatureFlag f = flagFromClause(c); @@ -329,14 +323,13 @@ public void preprocessFlagParsesClauseSemVer() { @Test public void preprocessSegmentPreprocessesClausesInRules() { // We'll just check one kind of clause, and assume that the preprocessing works the same as in flag rules - Clause c = new Clause( - UserAttribute.forName("x"), + Clause c = clause( + "x", Operator.matches, - ImmutableList.of(LDValue.of("x*")), - false + LDValue.of("x*") ); - SegmentRule rule = new SegmentRule(ImmutableList.of(c), null, null); - Segment s = new Segment("key", null, null, null, ImmutableList.of(rule), 0, false, false, null); + SegmentRule rule = segmentRuleBuilder().clauses(c).build(); + Segment s = segmentBuilder("key").disablePreprocessing(true).rules(rule).build(); assertNull(s.getRules().get(0).getClauses().get(0).preprocessed); diff --git a/src/test/java/com/launchdarkly/sdk/server/DataModelSerializationTest.java b/src/test/java/com/launchdarkly/sdk/server/DataModelSerializationTest.java index 9f0741a99..cd4b1a0f4 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataModelSerializationTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataModelSerializationTest.java @@ -3,9 +3,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.gson.JsonElement; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Operator; @@ -18,10 +19,10 @@ import com.launchdarkly.sdk.server.DataModel.VersionedData; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; -import com.launchdarkly.sdk.server.interfaces.SerializationException; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import org.junit.Test; @@ -159,7 +160,7 @@ public void flagIsDeserializedWithOptionalExperimentProperties() { assertEquals(0, flag.getTargets().size()); assertNotNull(flag.getRules()); assertEquals(1, flag.getRules().size()); - assertNull(flag.getRules().get(0).getRollout().getKind()); + assertEquals(RolloutKind.rollout, flag.getRules().get(0).getRollout().getKind()); assertFalse(flag.getRules().get(0).getRollout().isExperiment()); assertNull(flag.getRules().get(0).getRollout().getSeed()); assertEquals(2, flag.getRules().get(0).getRollout().getVariations().get(0).getVariation()); @@ -173,6 +174,154 @@ public void flagIsDeserializedWithOptionalExperimentProperties() { assertNull(flag.getDebugEventsUntilDate()); } + @Test + public void flagRuleBasicProperties() { + LDValue ruleJson = LDValue.buildObject() + .put("id", "id0") + .put("variation", 2) + .put("clauses", LDValue.arrayOf()) + .build(); + assertFlagRuleFromJson(ruleJson, r -> { + assertEquals("id0", r.getId()); + assertEquals(Integer.valueOf(2), r.getVariation()); + assertNull(r.getRollout()); + assertFalse(r.isTrackEvents()); + }); + } + + @Test + public void flagRuleTrackEvents() { + LDValue ruleJson = LDValue.buildObject() + .put("id", "id0") + .put("variation", 2) + .put("clauses", LDValue.arrayOf()) + .put("trackEvents", true) + .build(); + assertFlagRuleFromJson(ruleJson, r -> { + assertTrue(r.isTrackEvents()); + }); + } + + @Test + public void flagRuleRollout() { + LDValue ruleJson = LDValue.buildObject() + .put("id", "id0") + .put("rollout", LDValue.buildObject() + .put("variations", LDValue.arrayOf( + LDValue.buildObject() + .put("variation", 2) + .put("weight", 100000) + .build())) + .build()) + .put("clauses", LDValue.arrayOf()) + .build(); + assertFlagRuleFromJson(ruleJson, r -> { + assertNull(r.getVariation()); + assertNotNull(r.getRollout()); + assertEquals(RolloutKind.rollout, r.getRollout().getKind()); + assertNull(r.getRollout().getSeed()); + assertNull(r.getRollout().getContextKind()); + assertNull(r.getRollout().getBucketBy()); + assertEquals(1, r.getRollout().getVariations().size()); + assertEquals(2, r.getRollout().getVariations().get(0).getVariation()); + assertEquals(100000, r.getRollout().getVariations().get(0).getWeight()); + }); + } + + @Test + public void flagRuleRolloutBucketByWithoutContextKind() { + LDValue ruleJson = LDValue.buildObject() + .put("id", "id0") + .put("rollout", LDValue.buildObject() + .put("bucketBy", "/attr1") + .put("variations", LDValue.arrayOf( + LDValue.buildObject() + .put("variation", 2) + .put("weight", 100000) + .build())) + .build()) + .put("clauses", LDValue.arrayOf()) + .build(); + assertFlagRuleFromJson(ruleJson, r -> { + assertNotNull(r.getRollout()); + assertEquals(AttributeRef.fromLiteral("/attr1"), r.getRollout().getBucketBy()); + }); + } + + @Test + public void flagRuleRolloutContextKind() { + LDValue ruleJson = LDValue.buildObject() + .put("id", "id0") + .put("rollout", LDValue.buildObject() + .put("contextKind", "org") + .put("bucketBy", "/address/street") + .put("variations", LDValue.arrayOf( + LDValue.buildObject() + .put("variation", 2) + .put("weight", 100000) + .build())) + .build()) + .put("clauses", LDValue.arrayOf()) + .build(); + assertFlagRuleFromJson(ruleJson, r -> { + assertNotNull(r.getRollout()); + assertEquals(ContextKind.of("org"), r.getRollout().getContextKind()); + assertEquals(AttributeRef.fromPath("/address/street"), r.getRollout().getBucketBy()); + }); + } + + @Test + public void flagRuleExperiment() { + LDValue ruleJson = LDValue.buildObject() + .put("id", "id0") + .put("rollout", LDValue.buildObject() + .put("kind", "experiment") + .put("variations", LDValue.arrayOf( + LDValue.buildObject() + .put("variation", 2) + .put("weight", 100000) + .build())) + .put("seed", 123) + .build()) + .put("clauses", LDValue.arrayOf()) + .build(); + assertFlagRuleFromJson(ruleJson, r -> { + assertNotNull(r.getRollout()); + assertEquals(RolloutKind.experiment, r.getRollout().getKind()); + assertEquals(Integer.valueOf(123), r.getRollout().getSeed()); + }); + } + + @Test + public void flagClauseWithContextKind() { + LDValue clauseJson = LDValue.buildObject().put("contextKind", "org") + .put("attribute", "/address/street").put("op", "in").put("values", LDValue.arrayOf()).build(); + assertClauseFromJson(clauseJson, c -> { + assertEquals(ContextKind.of("org"), c.getContextKind()); + assertEquals(AttributeRef.fromPath("/address/street"), c.getAttribute()); + }); + } + + @Test + public void flagClauseWithoutContextKind() { + // When there's no context kind, the attribute is interpreted as a literal name even if it has a slash + LDValue clauseJson = LDValue.buildObject() + .put("attribute", "/attr1").put("op", "in").put("values", LDValue.arrayOf()).build(); + assertClauseFromJson(clauseJson, c -> { + assertNull(c.getContextKind()); + assertEquals(AttributeRef.fromLiteral("/attr1"), c.getAttribute()); + }); + } + + @Test + public void flagClauseNegated() { + LDValue clauseJson = LDValue.buildObject().put("negate", true) + .put("attribute", "attr1").put("op", "in").put("values", LDValue.arrayOf()).build(); + assertClauseFromJson(clauseJson, c -> { + assertTrue(c.isNegate()); + }); + } + @Test public void deletedFlagIsConvertedToAndFromJsonPlaceholder() { String json0 = LDValue.buildObject().put("version", 99) @@ -213,6 +362,75 @@ public void segmentIsDeserializedWithMinimalProperties() { assertNull(segment.getGeneration()); } + @Test + public void segmentUnboundedWithoutContextKind() { + LDValue segmentJson = LDValue.buildObject().put("key", "segmentkey").put("version", 1) + .put("unbounded", true).put("generation", 10).build(); + assertSegmentFromJson(segmentJson, s -> { + assertTrue(s.isUnbounded()); + assertNull(s.getUnboundedContextKind()); + assertEquals(Integer.valueOf(10), s.getGeneration()); + }); + } + + @Test + public void segmentUnboundedWithContextKind() { + LDValue segmentJson = LDValue.buildObject().put("key", "segmentkey").put("version", 1) + .put("unbounded", true).put("unboundedContextKind", "org").put("generation", 10).build(); + assertSegmentFromJson(segmentJson, s -> { + assertTrue(s.isUnbounded()); + assertEquals(ContextKind.of("org"), s.getUnboundedContextKind()); + assertEquals(Integer.valueOf(10), s.getGeneration()); + }); + } + + @Test + public void segmentRuleByWithoutRollout() { + LDValue ruleJson = LDValue.buildObject() + .put("clauses", LDValue.arrayOf( + LDValue.buildObject().put("attribute", "attr1").put("op", "in").put("values", LDValue.arrayOf(LDValue.of(3))).build() + )) + .build(); + assertSegmentRuleFromJson(ruleJson, r -> { + assertNull(r.getWeight()); + assertNull(r.getRolloutContextKind()); + assertNull(r.getBucketBy()); + assertEquals(1, r.getClauses().size()); + assertEquals(AttributeRef.fromLiteral("attr1"), r.getClauses().get(0).getAttribute()); + assertEquals(Operator.in, r.getClauses().get(0).getOp()); + assertEquals(ImmutableList.of(LDValue.of(3)), r.getClauses().get(0).getValues()); + }); + } + + @Test + public void segmentRuleRolloutBucketByWithoutContextKind() { + LDValue ruleJson = LDValue.buildObject() + .put("weight", 50000) + .put("bucketBy", "/attr1") + .put("clauses", LDValue.arrayOf()) + .build(); + assertSegmentRuleFromJson(ruleJson, r -> { + assertEquals(Integer.valueOf(50000), r.getWeight()); + assertNull(r.getRolloutContextKind()); + assertEquals(AttributeRef.fromLiteral("/attr1"), r.getBucketBy()); + }); + } + + @Test + public void segmentRuleRolloutWithContextKind() { + LDValue ruleJson = LDValue.buildObject() + .put("weight", 50000) + .put("rolloutContextKind", "org") + .put("bucketBy", "/address/street") + .put("clauses", LDValue.arrayOf()) + .build(); + assertSegmentRuleFromJson(ruleJson, r -> { + assertEquals(Integer.valueOf(50000), r.getWeight()); + assertEquals(ContextKind.of("org"), r.getRolloutContextKind()); + assertEquals(AttributeRef.fromPath("/address/street"), r.getBucketBy()); + }); + } + @Test public void deletedSegmentIsConvertedToAndFromJsonPlaceholder() { String json0 = LDValue.buildObject().put("version", 99) @@ -398,10 +616,31 @@ private void assertFlagFromJson(LDValue flagJson, Consumer action) action.accept(flag); } + private void assertFlagRuleFromJson(LDValue ruleJson, Consumer action) { + LDValue flagJson = LDValue.buildObject().put("rules", LDValue.arrayOf(ruleJson)).build(); + assertFlagFromJson(flagJson, f -> { + action.accept(f.getRules().get(0)); + }); + } + + private void assertClauseFromJson(LDValue clauseJson, Consumer action) { + LDValue ruleJson = LDValue.buildObject().put("clauses", LDValue.arrayOf(clauseJson)).build(); + assertFlagRuleFromJson(ruleJson, r -> { + action.accept(r.getClauses().get(0)); + }); + } + private void assertSegmentFromJson(LDValue segmentJson, Consumer action) { Segment segment = (Segment)SEGMENTS.deserialize(segmentJson.toJsonString()).getItem(); action.accept(segment); } + + private void assertSegmentRuleFromJson(LDValue ruleJson, Consumer action) { + LDValue segmentJson = LDValue.buildObject().put("rules", LDValue.arrayOf(ruleJson)).build(); + assertSegmentFromJson(segmentJson, s -> { + action.accept(s.getRules().get(0)); + }); + } private ObjectBuilder baseBuilder(String key) { return LDValue.buildObject().put("key", key).put("version", 99); @@ -421,35 +660,14 @@ private LDValue flagWithAllPropertiesJson() { .put("values", LDValue.buildArray().add("key1").add("key2").build()) .build()) .build()) - .put("rules", LDValue.buildArray() - .add(LDValue.buildObject() - .put("id", "id0") - .put("trackEvents", true) - .put("variation", 2) - .put("clauses", LDValue.buildArray() - .add(LDValue.buildObject() - .put("attribute", "name") - .put("op", "in") - .put("values", LDValue.buildArray().add("Lucy").add("Mina").build()) - .put("negate", true) - .build()) - .build()) - .build()) + .put("contextTargets", LDValue.buildArray() .add(LDValue.buildObject() - .put("id", "id1") - .put("rollout", LDValue.buildObject() - .put("variations", LDValue.buildArray() - .add(LDValue.buildObject() - .put("variation", 2) - .put("weight", 100000) - .build()) - .build()) - .put("bucketBy", "email") - .put("kind", "experiment") - .put("seed", 123) - .build()) + .put("contextKind", "org") + .put("variation", 1) + .put("values", LDValue.buildArray().add("key3").add("key4").build()) .build()) .build()) + .put("rules", LDValue.arrayOf()) .put("fallthrough", LDValue.buildObject() .put("variation", 1) .build()) @@ -467,47 +685,24 @@ private void assertFlagHasAllProperties(FeatureFlag flag) { assertEquals(99, flag.getVersion()); assertTrue(flag.isOn()); assertEquals("123", flag.getSalt()); - + assertNotNull(flag.getTargets()); assertEquals(1, flag.getTargets().size()); Target t0 = flag.getTargets().get(0); + assertNull(t0.getContextKind()); assertEquals(1, t0.getVariation()); assertEquals(ImmutableSet.of("key1", "key2"), t0.getValues()); + + assertNotNull(flag.getContextTargets()); + assertEquals(1, flag.getContextTargets().size()); + Target ct0 = flag.getContextTargets().get(0); + assertEquals(ContextKind.of("org"), ct0.getContextKind()); + assertEquals(1, ct0.getVariation()); + assertEquals(ImmutableSet.of("key3", "key4"), ct0.getValues()); assertNotNull(flag.getRules()); - assertEquals(2, flag.getRules().size()); - Rule r0 = flag.getRules().get(0); - assertEquals("id0", r0.getId()); - assertTrue(r0.isTrackEvents()); - assertEquals(Integer.valueOf(2), r0.getVariation()); - assertNull(r0.getRollout()); - - assertNotNull(r0.getClauses()); - Clause c0 = r0.getClauses().get(0); - assertEquals(UserAttribute.NAME, c0.getAttribute()); - assertEquals(Operator.in, c0.getOp()); - assertEquals(ImmutableList.of(LDValue.of("Lucy"), LDValue.of("Mina")), c0.getValues()); - assertTrue(c0.isNegate()); - - // Check for just one example of preprocessing, to verify that preprocessing has happened in - // general for this flag - the details are covered in EvaluatorPreprocessingTest. - assertNotNull(c0.preprocessed); - assertEquals(ImmutableSet.of(LDValue.of("Lucy"), LDValue.of("Mina")), c0.preprocessed.valuesSet); - - Rule r1 = flag.getRules().get(1); - assertEquals("id1", r1.getId()); - assertFalse(r1.isTrackEvents()); - assertNull(r1.getVariation()); - assertNotNull(r1.getRollout()); - assertNotNull(r1.getRollout().getVariations()); - assertEquals(1, r1.getRollout().getVariations().size()); - assertEquals(2, r1.getRollout().getVariations().get(0).getVariation()); - assertEquals(100000, r1.getRollout().getVariations().get(0).getWeight()); - assertEquals(UserAttribute.EMAIL, r1.getRollout().getBucketBy()); - assertEquals(RolloutKind.experiment, r1.getRollout().getKind()); - assert(r1.getRollout().isExperiment()); - assertEquals(Integer.valueOf(123), r1.getRollout().getSeed()); - + assertEquals(0, flag.getRules().size()); + assertNotNull(flag.getFallthrough()); assertEquals(Integer.valueOf(1), flag.getFallthrough().getVariation()); assertNull(flag.getFallthrough().getRollout()); @@ -525,25 +720,12 @@ private LDValue segmentWithAllPropertiesJson() { .put("version", 99) .put("included", LDValue.buildArray().add("key1").add("key2").build()) .put("excluded", LDValue.buildArray().add("key3").add("key4").build()) + .put("includedContexts", LDValue.arrayOf( + LDValue.buildObject().put("contextKind", "kind1").put("values", LDValue.arrayOf(LDValue.of("key5"))).build())) + .put("excludedContexts", LDValue.arrayOf( + LDValue.buildObject().put("contextKind", "kind2").put("values", LDValue.arrayOf(LDValue.of("key6"))).build())) .put("salt", "123") - .put("rules", LDValue.buildArray() - .add(LDValue.buildObject() - .put("weight", 50000) - .put("bucketBy", "email") - .put("clauses", LDValue.buildArray() - .add(LDValue.buildObject() - .put("attribute", "name") - .put("op", "in") - .put("values", LDValue.buildArray().add("Lucy").add("Mina").build()) - .put("negate", true) - .build()) - .build()) - .build()) - .add(LDValue.buildObject() - .build()) - .build()) - .put("unbounded", true) - .put("generation", 10) + .put("rules", LDValue.arrayOf()) // Extra fields should be ignored .put("fallthrough", LDValue.buildObject() .put("variation", 1) @@ -558,30 +740,19 @@ private void assertSegmentHasAllProperties(Segment segment) { assertEquals("123", segment.getSalt()); assertEquals(ImmutableSet.of("key1", "key2"), segment.getIncluded()); assertEquals(ImmutableSet.of("key3", "key4"), segment.getExcluded()); - + + assertEquals(1, segment.getIncludedContexts().size()); + assertEquals(ContextKind.of("kind1"), segment.getIncludedContexts().get(0).getContextKind()); + assertEquals(ImmutableSet.of("key5"), segment.getIncludedContexts().get(0).getValues()); + assertEquals(1, segment.getExcludedContexts().size()); + assertEquals(ContextKind.of("kind2"), segment.getExcludedContexts().get(0).getContextKind()); + assertEquals(ImmutableSet.of("key6"), segment.getExcludedContexts().get(0).getValues()); + assertNotNull(segment.getRules()); - assertEquals(2, segment.getRules().size()); - SegmentRule r0 = segment.getRules().get(0); - assertEquals(Integer.valueOf(50000), r0.getWeight()); - assertNotNull(r0.getClauses()); - - assertEquals(1, r0.getClauses().size()); - Clause c0 = r0.getClauses().get(0); - assertEquals(UserAttribute.NAME, c0.getAttribute()); - assertEquals(Operator.in, c0.getOp()); - assertEquals(ImmutableList.of(LDValue.of("Lucy"), LDValue.of("Mina")), c0.getValues()); - assertTrue(c0.isNegate()); - - assertTrue(segment.isUnbounded()); - assertEquals((Integer)10, segment.getGeneration()); - - // Check for just one example of preprocessing, to verify that preprocessing has happened in - // general for this segment - the details are covered in EvaluatorPreprocessingTest. - assertNotNull(c0.preprocessed); - assertEquals(ImmutableSet.of(LDValue.of("Lucy"), LDValue.of("Mina")), c0.preprocessed.valuesSet); - - SegmentRule r1 = segment.getRules().get(1); - assertNull(r1.getWeight()); - assertNull(r1.getBucketBy()); + assertEquals(0, segment.getRules().size()); + + assertFalse(segment.isUnbounded()); + assertNull(segment.getUnboundedContextKind()); + assertNull(segment.getGeneration()); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/DataModelTest.java b/src/test/java/com/launchdarkly/sdk/server/DataModelTest.java index 712a2d871..9cbbe9cd5 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataModelTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataModelTest.java @@ -19,31 +19,32 @@ public class DataModelTest { @Test public void flagPrerequisitesListCanNeverBeNull() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, null, null, null, false, false, false, null, false); - assertEquals(ImmutableList.of(), f.getPrerequisites()); + assertEquals(ImmutableList.of(), flagWithAllZeroValuedFields().getPrerequisites()); } @Test public void flagTargetsListCanNeverBeNull() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, null, null, null, false, false, false, null, false); - assertEquals(ImmutableList.of(), f.getTargets()); + assertEquals(ImmutableList.of(), flagWithAllZeroValuedFields().getTargets()); + } + + @Test + public void flagContextTargetsListCanNeverBeNull() { + assertEquals(ImmutableList.of(), flagWithAllZeroValuedFields().getContextTargets()); } @Test public void flagRulesListCanNeverBeNull() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, null, null, null, false, false, false, null, false); - assertEquals(ImmutableList.of(), f.getRules()); + assertEquals(ImmutableList.of(), flagWithAllZeroValuedFields().getRules()); } @Test public void flagVariationsListCanNeverBeNull() { - FeatureFlag f = new FeatureFlag("key", 0, false, null, null, null, null, null, null, null, false, false, false, null, false); - assertEquals(ImmutableList.of(), f.getVariations()); + assertEquals(ImmutableList.of(), flagWithAllZeroValuedFields().getVariations()); } @Test public void targetKeysSetCanNeverBeNull() { - Target t = new Target(null, 0); + Target t = new Target(null, null, 0); assertEquals(ImmutableSet.of(), t.getValues()); } @@ -55,37 +56,56 @@ public void ruleClausesListCanNeverBeNull() { @Test public void clauseValuesListCanNeverBeNull() { - Clause c = new Clause(null, null, null, false); + Clause c = new Clause(null, null, null, null, false); assertEquals(ImmutableList.of(), c.getValues()); } @Test public void segmentIncludedCanNeverBeNull() { - Segment s = new Segment("key", null, null, null, null, 0, false, false, null); - assertEquals(ImmutableSet.of(), s.getIncluded()); + assertEquals(ImmutableSet.of(), segmentWithAllZeroValuedFields().getIncluded()); } @Test public void segmentExcludedCanNeverBeNull() { - Segment s = new Segment("key", null, null, null, null, 0, false, false, null); - assertEquals(ImmutableSet.of(), s.getExcluded()); + assertEquals(ImmutableSet.of(), segmentWithAllZeroValuedFields().getExcluded()); + } + + @Test + public void segmentIncludedContextsCanNeverBeNull() { + assertEquals(ImmutableList.of(), segmentWithAllZeroValuedFields().getIncludedContexts()); + } + + @Test + public void segmentExcludedContextsCanNeverBeNull() { + assertEquals(ImmutableList.of(), segmentWithAllZeroValuedFields().getExcludedContexts()); } @Test public void segmentRulesListCanNeverBeNull() { - Segment s = new Segment("key", null, null, null, null, 0, false, false, null); - assertEquals(ImmutableList.of(), s.getRules()); + assertEquals(ImmutableList.of(), segmentWithAllZeroValuedFields().getRules()); } @Test public void segmentRuleClausesListCanNeverBeNull() { - SegmentRule r = new SegmentRule(null, null, null); + SegmentRule r = new SegmentRule(null, null, null, null); assertEquals(ImmutableList.of(), r.getClauses()); } @Test public void rolloutVariationsListCanNeverBeNull() { - Rollout r = new Rollout(null, null, RolloutKind.rollout); + Rollout r = new Rollout(null, null, null, RolloutKind.rollout, null); assertEquals(ImmutableList.of(), r.getVariations()); } + + private FeatureFlag flagWithAllZeroValuedFields() { + // This calls the empty constructor directly to simulate a condition where Gson did not set any fields + // and no preprocessing has happened. + return new FeatureFlag(); + } + + private Segment segmentWithAllZeroValuedFields() { + // This calls the empty constructor directly to simulate a condition where Gson did not set any fields + // and no preprocessing has happened. + return new Segment(); + } } diff --git a/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java b/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java index 25ae6cee5..a4fd47cc2 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataSourceUpdatesImplTest.java @@ -1,16 +1,14 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.DataModel.Operator; import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.Status; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; @@ -282,7 +280,7 @@ public void sendsEventsOnInitForFlagsWhoseSegmentsChanged() throws Exception { flagBuilder("flag1").version(1).build(), flagBuilder("flag2").version(1).rules( ruleBuilder().clauses( - ModelBuilders.clause(null, Operator.segmentMatch, LDValue.of("segment1")) + ModelBuilders.clauseMatchingSegment("segment1") ).build() ).build(), flagBuilder("flag3").version(1).build(), @@ -311,7 +309,7 @@ public void sendsEventsOnUpdateForFlagsWhoseSegmentsChanged() throws Exception { flagBuilder("flag1").version(1).build(), flagBuilder("flag2").version(1).rules( ruleBuilder().clauses( - ModelBuilders.clause(null, Operator.segmentMatch, LDValue.of("segment1")) + ModelBuilders.clauseMatchingSegment("segment1") ).build() ).build(), flagBuilder("flag3").version(1).build(), diff --git a/src/test/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImplTest.java b/src/test/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImplTest.java index de9208ea8..abbc5a473 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImplTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataStoreStatusProviderImplTest.java @@ -1,13 +1,13 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.Status; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import org.junit.Test; diff --git a/src/test/java/com/launchdarkly/sdk/server/DataStoreTestBase.java b/src/test/java/com/launchdarkly/sdk/server/DataStoreTestBase.java index 38fef5927..1b9df6cd4 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataStoreTestBase.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataStoreTestBase.java @@ -2,9 +2,9 @@ import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; import com.launchdarkly.sdk.server.DataStoreTestTypes.TestItem; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import org.junit.After; import org.junit.Before; diff --git a/src/test/java/com/launchdarkly/sdk/server/DataStoreTestTypes.java b/src/test/java/com/launchdarkly/sdk/server/DataStoreTestTypes.java index 4a4a28595..4e5bd0f7a 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DataStoreTestTypes.java +++ b/src/test/java/com/launchdarkly/sdk/server/DataStoreTestTypes.java @@ -6,11 +6,11 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.server.DataModel.VersionedData; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; import java.util.AbstractMap; import java.util.HashMap; diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorDiagnosticsTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorDiagnosticsTest.java deleted file mode 100644 index 0b4669aea..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorDiagnosticsTest.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableSet; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventSender; - -import org.junit.Test; - -import java.net.URI; -import java.time.Duration; - -import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; -import static com.launchdarkly.sdk.server.TestComponents.clientContext; -import static com.launchdarkly.sdk.server.TestComponents.sharedExecutor; -import static com.launchdarkly.sdk.server.TestUtil.simpleEvaluation; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.samePropertyValuesAs; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -/** - * These DefaultEventProcessor tests cover diagnostic event behavior. - */ -@SuppressWarnings("javadoc") -public class DefaultEventProcessorDiagnosticsTest extends DefaultEventProcessorTestBase { - @Test - public void diagnosticEventsSentToDiagnosticEndpoint() throws Exception { - MockEventSender es = new MockEventSender(); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(new DiagnosticId(SDK_KEY)); - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es), diagnosticAccumulator)) { - MockEventSender.Params initReq = es.awaitRequest(); - ep.postDiagnostic(); - MockEventSender.Params periodicReq = es.awaitRequest(); - - assertThat(initReq.kind, equalTo(EventSender.EventDataKind.DIAGNOSTICS)); - assertThat(periodicReq.kind, equalTo(EventSender.EventDataKind.DIAGNOSTICS)); - } - } - - @Test - public void initialDiagnosticEventHasInitBody() throws Exception { - MockEventSender es = new MockEventSender(); - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es), diagnosticAccumulator)) { - MockEventSender.Params req = es.awaitRequest(); - - DiagnosticEvent.Init initEvent = gson.fromJson(req.data, DiagnosticEvent.Init.class); - - assertNotNull(initEvent); - assertThat(initEvent.kind, equalTo("diagnostic-init")); - assertThat(initEvent.id, samePropertyValuesAs(diagnosticId)); - assertNotNull(initEvent.configuration); - assertNotNull(initEvent.sdk); - assertNotNull(initEvent.platform); - } - } - - @Test - public void periodicDiagnosticEventHasStatisticsBody() throws Exception { - MockEventSender es = new MockEventSender(); - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - long dataSinceDate = diagnosticAccumulator.dataSinceDate; - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es), diagnosticAccumulator)) { - // Ignore the initial diagnostic event - es.awaitRequest(); - ep.postDiagnostic(); - MockEventSender.Params periodicReq = es.awaitRequest(); - - assertNotNull(periodicReq); - DiagnosticEvent.Statistics statsEvent = gson.fromJson(periodicReq.data, DiagnosticEvent.Statistics.class); - - assertNotNull(statsEvent); - assertThat(statsEvent.kind, equalTo("diagnostic")); - assertThat(statsEvent.id, samePropertyValuesAs(diagnosticId)); - assertThat(statsEvent.dataSinceDate, equalTo(dataSinceDate)); - assertThat(statsEvent.creationDate, equalTo(diagnosticAccumulator.dataSinceDate)); - assertThat(statsEvent.deduplicatedUsers, equalTo(0L)); - assertThat(statsEvent.eventsInLastBatch, equalTo(0L)); - assertThat(statsEvent.droppedEvents, equalTo(0L)); - } - } - - @Test - public void periodicDiagnosticEventGetsEventsInLastBatchAndDeduplicatedUsers() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag1 = flagBuilder("flagkey1").version(11).trackEvents(true).build(); - DataModel.FeatureFlag flag2 = flagBuilder("flagkey2").version(22).trackEvents(true).build(); - LDValue value = LDValue.of("value"); - Event.FeatureRequest fe1 = EventFactory.DEFAULT.newFeatureRequestEvent(flag1, user, - simpleEvaluation(1, value), LDValue.ofNull()); - Event.FeatureRequest fe2 = EventFactory.DEFAULT.newFeatureRequestEvent(flag2, user, - simpleEvaluation(1, value), LDValue.ofNull()); - - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es), diagnosticAccumulator)) { - // Ignore the initial diagnostic event - es.awaitRequest(); - - ep.sendEvent(fe1); - ep.sendEvent(fe2); - ep.flush(); - // Ignore normal events - es.awaitRequest(); - - ep.postDiagnostic(); - MockEventSender.Params periodicReq = es.awaitRequest(); - - assertNotNull(periodicReq); - DiagnosticEvent.Statistics statsEvent = gson.fromJson(periodicReq.data, DiagnosticEvent.Statistics.class); - - assertNotNull(statsEvent); - assertThat(statsEvent.deduplicatedUsers, equalTo(1L)); - assertThat(statsEvent.eventsInLastBatch, equalTo(3L)); - assertThat(statsEvent.droppedEvents, equalTo(0L)); - } - } - - @Test - public void periodicDiagnosticEventsAreSentAutomatically() throws Exception { - MockEventSender es = new MockEventSender(); - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - ClientContext context = clientContext(SDK_KEY, LDConfig.DEFAULT); - DiagnosticEvent.Init initEvent = new DiagnosticEvent.Init(0, diagnosticId, LDConfig.DEFAULT, - context.getBasic(), context.getHttp()); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - - EventsConfiguration eventsConfig = makeEventsConfigurationWithBriefDiagnosticInterval(es); - - try (DefaultEventProcessor ep = new DefaultEventProcessor(eventsConfig, sharedExecutor, Thread.MAX_PRIORITY, - diagnosticAccumulator, initEvent, testLogger)) { - // Ignore the initial diagnostic event - es.awaitRequest(); - - MockEventSender.Params periodicReq = es.awaitRequest(); - - assertNotNull(periodicReq); - DiagnosticEvent.Statistics statsEvent = gson.fromJson(periodicReq.data, DiagnosticEvent.Statistics.class); - assertEquals("diagnostic", statsEvent.kind); - } - } - - private EventsConfiguration makeEventsConfigurationWithBriefDiagnosticInterval(EventSender es) { - // Can't use the regular config builder for this, because it will enforce a minimum flush interval - return new EventsConfiguration( - false, - 100, - es, - FAKE_URI, - Duration.ofSeconds(5), - true, - ImmutableSet.of(), - 100, - Duration.ofSeconds(5), - Duration.ofMillis(50) - ); - } - - @Test - public void diagnosticEventsStopAfter401Error() throws Exception { - // This is easier to test with a mock component than it would be in LDClientEndToEndTest, because - // we don't have to worry about the latency of a real HTTP request which could allow the periodic - // task to fire again before we received a response. In real life, that wouldn't matter because - // the minimum diagnostic interval is so long, but in a test we need to be able to use a short - // interval. - MockEventSender es = new MockEventSender(); - es.result = new EventSender.Result(false, true, null); // mustShutdown=true; this is what would be returned for a 401 error - - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - ClientContext context = clientContext(SDK_KEY, LDConfig.DEFAULT); - DiagnosticEvent.Init initEvent = new DiagnosticEvent.Init(0, diagnosticId, LDConfig.DEFAULT, - context.getBasic(), context.getHttp()); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - - EventsConfiguration eventsConfig = makeEventsConfigurationWithBriefDiagnosticInterval(es); - - try (DefaultEventProcessor ep = new DefaultEventProcessor(eventsConfig, sharedExecutor, Thread.MAX_PRIORITY, - diagnosticAccumulator, initEvent, testLogger)) { - // Ignore the initial diagnostic event - es.awaitRequest(); - - es.expectNoRequests(Duration.ofMillis(100)); - } - } - - @Test - public void customBaseUriIsPassedToEventSenderForDiagnosticEvents() throws Exception { - MockEventSender es = new MockEventSender(); - URI uri = URI.create("fake-uri"); - DiagnosticId diagnosticId = new DiagnosticId(SDK_KEY); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).baseURI(uri), diagnosticAccumulator)) { - } - - MockEventSender.Params p = es.awaitRequest(); - assertThat(p.eventsBaseUri, equalTo(uri)); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorOutputTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorOutputTest.java deleted file mode 100644 index 4088bc6fa..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorOutputTest.java +++ /dev/null @@ -1,512 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventSender; - -import org.hamcrest.Matchers; -import org.junit.Test; - -import java.util.Date; - -import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; -import static com.launchdarkly.sdk.server.TestUtil.simpleEvaluation; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.contains; - -/** - * These DefaultEventProcessor tests cover the specific content that should appear in event payloads. - */ -@SuppressWarnings("javadoc") -public class DefaultEventProcessorOutputTest extends DefaultEventProcessorTestBase { - private static final LDUser userWithNullKey = new LDUser(null); - - @Test - public void identifyEventIsQueued() throws Exception { - MockEventSender es = new MockEventSender(); - Event e = EventFactory.DEFAULT.newIdentifyEvent(user); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(e); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIdentifyEvent(e, userJson) - )); - } - - @Test - public void userIsFilteredInIdentifyEvent() throws Exception { - MockEventSender es = new MockEventSender(); - Event e = EventFactory.DEFAULT.newIdentifyEvent(user); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).allAttributesPrivate(true))) { - ep.sendEvent(e); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIdentifyEvent(e, filteredUserJson) - )); - } - - @SuppressWarnings("unchecked") - @Test - public void identifyEventWithNullUserOrNullUserKeyDoesNotCauseError() throws Exception { - // This should never happen because LDClient.identify() rejects such a user, but just in case, - // we want to make sure it doesn't blow up the event processor. - MockEventSender es = new MockEventSender(); - Event event1 = EventFactory.DEFAULT.newIdentifyEvent(userWithNullKey); - Event event2 = EventFactory.DEFAULT.newIdentifyEvent(null); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).allAttributesPrivate(true))) { - ep.sendEvent(event1); - ep.sendEvent(event2); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIdentifyEvent(event1, LDValue.buildObject().build()), - isIdentifyEvent(event2, LDValue.ofNull()) - )); - } - - @SuppressWarnings("unchecked") - @Test - public void individualFeatureEventIsQueuedWithIndexEvent() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - isFeatureEvent(fe, flag, false, null), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void userIsFilteredInIndexEvent() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).allAttributesPrivate(true))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, filteredUserJson), - isFeatureEvent(fe, flag, false, null), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void featureEventCanContainInlineUser() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).inlineUsersInEvents(true))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isFeatureEvent(fe, flag, false, userJson), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void userIsFilteredInFeatureEvent() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es) - .inlineUsersInEvents(true).allAttributesPrivate(true))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isFeatureEvent(fe, flag, false, filteredUserJson), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void featureEventCanBeForPrerequisite() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag mainFlag = flagBuilder("flagkey").version(11).build(); - DataModel.FeatureFlag prereqFlag = flagBuilder("prereqkey").version(12).trackEvents(true).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newPrerequisiteFeatureRequestEvent(prereqFlag, user, - simpleEvaluation(1, LDValue.of("value")), - mainFlag); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - allOf(isFeatureEvent(fe, prereqFlag, false, null), isPrerequisiteOf(mainFlag.getKey())), - isSummaryEvent() - )); - } - - @Test - public void featureEventWithNullUserOrNullUserKeyIsIgnored() throws Exception { - // This should never happen because LDClient rejects such a user, but just in case, - // we want to make sure it doesn't blow up the event processor. - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).build(); - Event.FeatureRequest event1 = EventFactory.DEFAULT.newFeatureRequestEvent(flag, userWithNullKey, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - Event.FeatureRequest event2 = EventFactory.DEFAULT.newFeatureRequestEvent(flag, null, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es) - .inlineUsersInEvents(true).allAttributesPrivate(true))) { - ep.sendEvent(event1); - ep.sendEvent(event2); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void featureEventCanContainReason() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - EvaluationReason reason = EvaluationReason.ruleMatch(1, null); - Event.FeatureRequest fe = EventFactory.DEFAULT_WITH_REASONS.newFeatureRequestEvent(flag, user, - EvalResult.of(LDValue.of("value"), 1, reason), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - isFeatureEvent(fe, flag, false, null, reason), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void indexEventIsStillGeneratedIfInlineUsersIsTrueButFeatureEventIsNotTracked() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(false).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), null); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).inlineUsersInEvents(true))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void eventKindIsDebugIfFlagIsTemporarilyInDebugMode() throws Exception { - MockEventSender es = new MockEventSender(); - long futureTime = System.currentTimeMillis() + 1000000; - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).debugEventsUntilDate(futureTime).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - isFeatureEvent(fe, flag, true, userJson), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void eventCanBeBothTrackedAndDebugged() throws Exception { - MockEventSender es = new MockEventSender(); - long futureTime = System.currentTimeMillis() + 1000000; - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true) - .debugEventsUntilDate(futureTime).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - isFeatureEvent(fe, flag, false, null), - isFeatureEvent(fe, flag, true, userJson), - isSummaryEvent() - )); - } - - @SuppressWarnings("unchecked") - @Test - public void debugModeExpiresBasedOnClientTimeIfClientTimeIsLaterThanServerTime() throws Exception { - MockEventSender es = new MockEventSender(); - - // Pick a server time that is somewhat behind the client time - long serverTime = System.currentTimeMillis() - 20000; - es.result = new EventSender.Result(true, false, new Date(serverTime)); - - long debugUntil = serverTime + 1000; - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).debugEventsUntilDate(debugUntil).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - // Send and flush an event we don't care about, just so we'll receive "resp1" which sets the last server time - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(new LDUser.Builder("otherUser").build())); - ep.flush(); - ep.waitUntilInactive(); // this ensures that it has received the first response, with the date - - es.receivedParams.clear(); - es.result = new EventSender.Result(true, false, null); - - // Now send an event with debug mode on, with a "debug until" time that is further in - // the future than the server time, but in the past compared to the client. - ep.sendEvent(fe); - } - - // Should get a summary event only, not a full feature event - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - isSummaryEvent(fe.getCreationDate(), fe.getCreationDate()) - )); - } - - @SuppressWarnings("unchecked") - @Test - public void debugModeExpiresBasedOnServerTimeIfServerTimeIsLaterThanClientTime() throws Exception { - MockEventSender es = new MockEventSender(); - - // Pick a server time that is somewhat ahead of the client time - long serverTime = System.currentTimeMillis() + 20000; - es.result = new EventSender.Result(true, false, new Date(serverTime)); - - long debugUntil = serverTime - 1000; - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).debugEventsUntilDate(debugUntil).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - // Send and flush an event we don't care about, just to set the last server time - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(new LDUser.Builder("otherUser").build())); - ep.flush(); - ep.waitUntilInactive(); // this ensures that it has received the first response, with the date - - es.receivedParams.clear(); - es.result = new EventSender.Result(true, false, null); - - // Now send an event with debug mode on, with a "debug until" time that is further in - // the future than the client time, but in the past compared to the server. - ep.sendEvent(fe); - } - - // Should get a summary event only, not a full feature event - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe, userJson), - isSummaryEvent(fe.getCreationDate(), fe.getCreationDate()) - )); - } - - @SuppressWarnings("unchecked") - @Test - public void twoFeatureEventsForSameUserGenerateOnlyOneIndexEvent() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag1 = flagBuilder("flagkey1").version(11).trackEvents(true).build(); - DataModel.FeatureFlag flag2 = flagBuilder("flagkey2").version(22).trackEvents(true).build(); - LDValue value = LDValue.of("value"); - Event.FeatureRequest fe1 = EventFactory.DEFAULT.newFeatureRequestEvent(flag1, user, - simpleEvaluation(1, value), LDValue.ofNull()); - Event.FeatureRequest fe2 = EventFactory.DEFAULT.newFeatureRequestEvent(flag2, user, - simpleEvaluation(1, value), LDValue.ofNull()); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(fe1); - ep.sendEvent(fe2); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe1, userJson), - isFeatureEvent(fe1, flag1, false, null), - isFeatureEvent(fe2, flag2, false, null), - isSummaryEvent(fe1.getCreationDate(), fe2.getCreationDate()) - )); - } - - @SuppressWarnings("unchecked") - @Test - public void identifyEventMakesIndexEventUnnecessary() throws Exception { - MockEventSender es = new MockEventSender(); - Event ie = EventFactory.DEFAULT.newIdentifyEvent(user); - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), null); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(ie); - ep.sendEvent(fe); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIdentifyEvent(ie, userJson), - isFeatureEvent(fe, flag, false, null), - isSummaryEvent() - )); - } - - - @SuppressWarnings("unchecked") - @Test - public void nonTrackedEventsAreSummarized() throws Exception { - MockEventSender es = new MockEventSender(); - DataModel.FeatureFlag flag1 = flagBuilder("flagkey1").version(11).build(); - DataModel.FeatureFlag flag2 = flagBuilder("flagkey2").version(22).build(); - LDValue value1 = LDValue.of("value1"); - LDValue value2 = LDValue.of("value2"); - LDValue default1 = LDValue.of("default1"); - LDValue default2 = LDValue.of("default2"); - Event fe1a = EventFactory.DEFAULT.newFeatureRequestEvent(flag1, user, - simpleEvaluation(1, value1), default1); - Event fe1b = EventFactory.DEFAULT.newFeatureRequestEvent(flag1, user, - simpleEvaluation(1, value1), default1); - Event fe1c = EventFactory.DEFAULT.newFeatureRequestEvent(flag1, user, - simpleEvaluation(2, value2), default1); - Event fe2 = EventFactory.DEFAULT.newFeatureRequestEvent(flag2, user, - simpleEvaluation(2, value2), default2); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(fe1a); - ep.sendEvent(fe1b); - ep.sendEvent(fe1c); - ep.sendEvent(fe2); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(fe1a, userJson), - allOf( - isSummaryEvent(fe1a.getCreationDate(), fe2.getCreationDate()), - hasSummaryFlag(flag1.getKey(), default1, - Matchers.containsInAnyOrder( - isSummaryEventCounter(flag1, 1, value1, 2), - isSummaryEventCounter(flag1, 2, value2, 1) - )), - hasSummaryFlag(flag2.getKey(), default2, - contains(isSummaryEventCounter(flag2, 2, value2, 1))) - ) - )); - } - - @SuppressWarnings("unchecked") - @Test - public void customEventIsQueuedWithUser() throws Exception { - MockEventSender es = new MockEventSender(); - LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); - double metric = 1.5; - Event.Custom ce = EventFactory.DEFAULT.newCustomEvent("eventkey", user, data, metric); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(ce); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(ce, userJson), - isCustomEvent(ce, null) - )); - } - - @Test - public void customEventCanContainInlineUser() throws Exception { - MockEventSender es = new MockEventSender(); - LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); - Event.Custom ce = EventFactory.DEFAULT.newCustomEvent("eventkey", user, data, null); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).inlineUsersInEvents(true))) { - ep.sendEvent(ce); - } - - assertThat(es.getEventsFromLastRequest(), contains(isCustomEvent(ce, userJson))); - } - - @Test - public void userIsFilteredInCustomEvent() throws Exception { - MockEventSender es = new MockEventSender(); - LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); - Event.Custom ce = EventFactory.DEFAULT.newCustomEvent("eventkey", user, data, null); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es) - .inlineUsersInEvents(true).allAttributesPrivate(true))) { - ep.sendEvent(ce); - } - - assertThat(es.getEventsFromLastRequest(), contains(isCustomEvent(ce, filteredUserJson))); - } - - @SuppressWarnings("unchecked") - @Test - public void customEventWithNullUserOrNullUserKeyDoesNotCauseError() throws Exception { - // This should never happen because LDClient rejects such a user, but just in case, - // we want to make sure it doesn't blow up the event processor. - MockEventSender es = new MockEventSender(); - Event.Custom event1 = EventFactory.DEFAULT.newCustomEvent("eventkey", userWithNullKey, LDValue.ofNull(), null); - Event.Custom event2 = EventFactory.DEFAULT.newCustomEvent("eventkey", null, LDValue.ofNull(), null); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es) - .inlineUsersInEvents(true).allAttributesPrivate(true))) { - ep.sendEvent(event1); - ep.sendEvent(event2); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isCustomEvent(event1, LDValue.buildObject().build()), - isCustomEvent(event2, LDValue.ofNull()) - )); - } - - @Test - public void aliasEventIsQueued() throws Exception { - MockEventSender es = new MockEventSender(); - LDUser user1 = new LDUser.Builder("anon-user").anonymous(true).build(); - LDUser user2 = new LDUser("non-anon-user"); - Event.AliasEvent event = EventFactory.DEFAULT.newAliasEvent(user2, user1); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(event); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isAliasEvent(event) - )); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorTest.java deleted file mode 100644 index 14884e026..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorTest.java +++ /dev/null @@ -1,431 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterables; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; -import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory; -import com.launchdarkly.sdk.server.interfaces.EventSender; -import com.launchdarkly.testhelpers.JsonTestValue; - -import org.hamcrest.Matchers; -import org.junit.Test; - -import java.io.IOException; -import java.net.URI; -import java.time.Duration; -import java.util.concurrent.CountDownLatch; - -import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; -import static com.launchdarkly.sdk.server.TestComponents.clientContext; -import static com.launchdarkly.sdk.server.TestComponents.sharedExecutor; -import static com.launchdarkly.sdk.server.TestUtil.simpleEvaluation; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.sameInstance; -import static org.junit.Assert.assertEquals; - -/** - * These tests cover all of the basic DefaultEventProcessor behavior that is not covered by - * DefaultEventProcessorOutputTest or DefaultEventProcessorDiagnosticTest. - */ -@SuppressWarnings("javadoc") -public class DefaultEventProcessorTest extends DefaultEventProcessorTestBase { - @Test - public void builderHasDefaultConfiguration() throws Exception { - EventProcessorFactory epf = Components.sendEvents(); - try (DefaultEventProcessor ep = (DefaultEventProcessor)epf.createEventProcessor(clientContext(SDK_KEY, LDConfig.DEFAULT))) { - EventsConfiguration ec = ep.dispatcher.eventsConfig; - assertThat(ec.allAttributesPrivate, is(false)); - assertThat(ec.capacity, equalTo(EventProcessorBuilder.DEFAULT_CAPACITY)); - assertThat(ec.diagnosticRecordingInterval, equalTo(EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL)); - assertThat(ec.eventSender, instanceOf(DefaultEventSender.class)); - assertThat(ec.eventsUri, equalTo(StandardEndpoints.DEFAULT_EVENTS_BASE_URI)); - assertThat(ec.flushInterval, equalTo(EventProcessorBuilder.DEFAULT_FLUSH_INTERVAL)); - assertThat(ec.inlineUsersInEvents, is(false)); - assertThat(ec.privateAttributes, equalTo(ImmutableSet.of())); - assertThat(ec.userKeysCapacity, equalTo(EventProcessorBuilder.DEFAULT_USER_KEYS_CAPACITY)); - assertThat(ec.userKeysFlushInterval, equalTo(EventProcessorBuilder.DEFAULT_USER_KEYS_FLUSH_INTERVAL)); - } - } - - @Test - public void builderCanSpecifyConfiguration() throws Exception { - MockEventSender es = new MockEventSender(); - EventProcessorFactory epf = Components.sendEvents() - .allAttributesPrivate(true) - .baseURI(FAKE_URI) - .capacity(3333) - .diagnosticRecordingInterval(Duration.ofSeconds(480)) - .eventSender(senderFactory(es)) - .flushInterval(Duration.ofSeconds(99)) - .privateAttributeNames("name", "dogs") - .userKeysCapacity(555) - .userKeysFlushInterval(Duration.ofSeconds(101)); - try (DefaultEventProcessor ep = (DefaultEventProcessor)epf.createEventProcessor(clientContext(SDK_KEY, LDConfig.DEFAULT))) { - EventsConfiguration ec = ep.dispatcher.eventsConfig; - assertThat(ec.allAttributesPrivate, is(true)); - assertThat(ec.capacity, equalTo(3333)); - assertThat(ec.diagnosticRecordingInterval, equalTo(Duration.ofSeconds(480))); - assertThat(ec.eventSender, sameInstance((EventSender)es)); - assertThat(ec.eventsUri, equalTo(FAKE_URI)); - assertThat(ec.flushInterval, equalTo(Duration.ofSeconds(99))); - assertThat(ec.inlineUsersInEvents, is(false)); // will test this separately below - assertThat(ec.privateAttributes, equalTo(ImmutableSet.of(UserAttribute.NAME, UserAttribute.forName("dogs")))); - assertThat(ec.userKeysCapacity, equalTo(555)); - assertThat(ec.userKeysFlushInterval, equalTo(Duration.ofSeconds(101))); - } - // Test inlineUsersInEvents separately to make sure it and the other boolean property (allAttributesPrivate) - // are really independently settable, since there's no way to distinguish between two true values - EventProcessorFactory epf1 = Components.sendEvents().inlineUsersInEvents(true); - try (DefaultEventProcessor ep = (DefaultEventProcessor)epf1.createEventProcessor(clientContext(SDK_KEY, LDConfig.DEFAULT))) { - EventsConfiguration ec = ep.dispatcher.eventsConfig; - assertThat(ec.allAttributesPrivate, is(false)); - assertThat(ec.inlineUsersInEvents, is(true)); - } - } - - @SuppressWarnings("unchecked") - @Test - public void eventsAreFlushedAutomatically() throws Exception { - MockEventSender es = new MockEventSender(); - Duration briefFlushInterval = Duration.ofMillis(50); - - // Can't use the regular config builder for this, because it will enforce a minimum flush interval - EventsConfiguration eventsConfig = new EventsConfiguration( - false, - 100, - es, - FAKE_URI, - briefFlushInterval, - true, - ImmutableSet.of(), - 100, - Duration.ofSeconds(5), - null - ); - try (DefaultEventProcessor ep = new DefaultEventProcessor(eventsConfig, sharedExecutor, Thread.MAX_PRIORITY, - null, null, testLogger)) { - Event.Custom event1 = EventFactory.DEFAULT.newCustomEvent("event1", user, null, null); - Event.Custom event2 = EventFactory.DEFAULT.newCustomEvent("event2", user, null, null); - ep.sendEvent(event1); - ep.sendEvent(event2); - - // getEventsFromLastRequest will block until the MockEventSender receives a payload - we expect - // both events to be in one payload, but if some unusual delay happened in between the two - // sendEvent calls, they might be in two - Iterable payload1 = es.getEventsFromLastRequest(); - if (Iterables.size(payload1) == 1) { - assertThat(payload1, contains(isCustomEvent(event1, userJson))); - assertThat(es.getEventsFromLastRequest(), contains(isCustomEvent(event2, userJson))); - } else { - assertThat(payload1, contains(isCustomEvent(event1, userJson), isCustomEvent(event2, userJson))); - } - - Event.Custom event3 = EventFactory.DEFAULT.newCustomEvent("event3", user, null, null); - ep.sendEvent(event3); - assertThat(es.getEventsFromLastRequest(), contains(isCustomEvent(event3, userJson))); - } - - LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); - double metric = 1.5; - Event.Custom ce = EventFactory.DEFAULT.newCustomEvent("eventkey", user, data, metric); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(ce); - } - - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(ce, userJson), - isCustomEvent(ce, null) - )); - } - - @Test - public void closingEventProcessorForcesSynchronousFlush() throws Exception { - MockEventSender es = new MockEventSender(); - Event e = EventFactory.DEFAULT.newIdentifyEvent(user); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(e); - } - - assertThat(es.getEventsFromLastRequest(), contains(isIdentifyEvent(e, userJson))); - } - - @Test - public void nothingIsSentIfThereAreNoEvents() throws Exception { - MockEventSender es = new MockEventSender(); - DefaultEventProcessor ep = makeEventProcessor(baseConfig(es)); - ep.close(); - - assertEquals(0, es.receivedParams.size()); - } - - @SuppressWarnings("unchecked") - @Test - public void userKeysAreFlushedAutomatically() throws Exception { - // This test overrides the user key flush interval to a small value and verifies that a new - // index event is generated for a user after the user keys have been flushed. - MockEventSender es = new MockEventSender(); - Duration briefUserKeyFlushInterval = Duration.ofMillis(60); - - // Can't use the regular config builder for this, because it will enforce a minimum flush interval - EventsConfiguration eventsConfig = new EventsConfiguration( - false, - 100, - es, - FAKE_URI, - Duration.ofSeconds(5), - false, // do not inline users in events - ImmutableSet.of(), - 100, - briefUserKeyFlushInterval, - null - ); - try (DefaultEventProcessor ep = new DefaultEventProcessor(eventsConfig, sharedExecutor, Thread.MAX_PRIORITY, - null, null, testLogger)) { - Event.Custom event1 = EventFactory.DEFAULT.newCustomEvent("event1", user, null, null); - Event.Custom event2 = EventFactory.DEFAULT.newCustomEvent("event2", user, null, null); - ep.sendEvent(event1); - ep.sendEvent(event2); - - // We're relying on the user key flush not happening in between event1 and event2, so we should get - // a single index event for the user. - ep.flush(); - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(event1, userJson), - isCustomEvent(event1, null), - isCustomEvent(event2, null) - )); - - // Now wait long enough for the user key cache to be flushed - Thread.sleep(briefUserKeyFlushInterval.toMillis() * 2); - - // Referencing the same user in a new even should produce a new index event - Event.Custom event3 = EventFactory.DEFAULT.newCustomEvent("event3", user, null, null); - ep.sendEvent(event3); - ep.flush(); - assertThat(es.getEventsFromLastRequest(), contains( - isIndexEvent(event3, userJson), - isCustomEvent(event3, null) - )); - } - } - - @Test - public void eventSenderIsClosedWithEventProcessor() throws Exception { - MockEventSender es = new MockEventSender(); - assertThat(es.closed, is(false)); - DefaultEventProcessor ep = makeEventProcessor(baseConfig(es)); - ep.close(); - assertThat(es.closed, is(true)); - } - - @Test - public void eventProcessorCatchesExceptionWhenClosingEventSender() throws Exception { - MockEventSender es = new MockEventSender(); - es.fakeErrorOnClose = new IOException("sorry"); - assertThat(es.closed, is(false)); - DefaultEventProcessor ep = makeEventProcessor(baseConfig(es)); - ep.close(); - assertThat(es.closed, is(true)); - } - - @Test - public void customBaseUriIsPassedToEventSenderForAnalyticsEvents() throws Exception { - MockEventSender es = new MockEventSender(); - Event e = EventFactory.DEFAULT.newIdentifyEvent(user); - URI uri = URI.create("fake-uri"); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es).baseURI(uri))) { - ep.sendEvent(e); - } - - MockEventSender.Params p = es.awaitRequest(); - assertThat(p.eventsBaseUri, equalTo(uri)); - } - - @Test - public void eventCapacityIsEnforced() throws Exception { - int capacity = 10; - MockEventSender es = new MockEventSender(); - EventProcessorBuilder config = baseConfig(es).capacity(capacity) - .flushInterval(Duration.ofSeconds(1)); - // The flush interval setting is a failsafe in case we do get a queue overflow due to the tiny buffer size - - // that might cause the special message that's generated by ep.flush() to be missed, so we just want to make - // sure a flush will happen within a few seconds so getEventsFromLastRequest() won't time out. - - try (DefaultEventProcessor ep = makeEventProcessor(config)) { - for (int i = 0; i < capacity + 2; i++) { - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user)); - - // Using such a tiny buffer means there's also a tiny inbox queue, so we'll add a slight - // delay to keep EventDispatcher from being overwhelmed - Thread.sleep(10); - } - ep.flush(); - assertThat(es.getEventsFromLastRequest(), Matchers.iterableWithSize(capacity)); - } - } - - @Test - public void eventCapacityDoesNotPreventSummaryEventFromBeingSent() throws Exception { - int capacity = 10; - MockEventSender es = new MockEventSender(); - EventProcessorBuilder config = baseConfig(es).capacity(capacity).inlineUsersInEvents(true) - .flushInterval(Duration.ofSeconds(1)); - // The flush interval setting is a failsafe in case we do get a queue overflow due to the tiny buffer size - - // that might cause the special message that's generated by ep.flush() to be missed, so we just want to make - // sure a flush will happen within a few seconds so getEventsFromLastRequest() won't time out. - - DataModel.FeatureFlag flag = flagBuilder("flagkey").version(11).trackEvents(true).build(); - - try (DefaultEventProcessor ep = makeEventProcessor(config)) { - for (int i = 0; i < capacity; i++) { - Event.FeatureRequest fe = EventFactory.DEFAULT.newFeatureRequestEvent(flag, user, - simpleEvaluation(1, LDValue.of("value")), LDValue.ofNull()); - ep.sendEvent(fe); - - // Using such a tiny buffer means there's also a tiny inbox queue, so we'll add a slight - // delay to keep EventDispatcher from being overwhelmed - Thread.sleep(10); - } - - ep.flush(); - Iterable eventsReceived = es.getEventsFromLastRequest(); - - assertThat(eventsReceived, Matchers.iterableWithSize(capacity + 1)); - assertThat(Iterables.get(eventsReceived, capacity), isSummaryEvent()); - } - } - - @Test - public void noMoreEventsAreProcessedAfterUnrecoverableError() throws Exception { - MockEventSender es = new MockEventSender(); - es.result = new EventSender.Result(false, true, null); // mustShutdown == true - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user)); - ep.flush(); - es.awaitRequest(); - - // allow a little time for the event processor to pass the "must shut down" signal back from the sender - Thread.sleep(50); - - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user)); - ep.flush(); - es.expectNoRequests(Duration.ofMillis(100)); - } - } - - @Test - public void noMoreEventsAreProcessedAfterClosingEventProcessor() throws Exception { - MockEventSender es = new MockEventSender(); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - ep.close(); - - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user)); - ep.flush(); - - es.expectNoRequests(Duration.ofMillis(100)); - } - } - - @Test - public void uncheckedExceptionFromEventSenderDoesNotStopWorkerThread() throws Exception { - MockEventSender es = new MockEventSender(); - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - es.fakeError = new RuntimeException("sorry"); - - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user)); - ep.flush(); - es.awaitRequest(); - // MockEventSender now throws an unchecked exception up to EventProcessor's flush worker - - // verify that a subsequent flush still works - - es.fakeError = null; - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user)); - ep.flush(); - es.awaitRequest(); - } - } - - @SuppressWarnings("unchecked") - @Test - public void eventsAreKeptInBufferIfAllFlushWorkersAreBusy() throws Exception { - // Note that in the current implementation, although the intention was that we would cancel a flush - // if there's not an available flush worker, instead what happens is that we will queue *one* flush - // in that case, and then cancel the *next* flush if the workers are still busy. This is because we - // used a BlockingQueue with a size of 1, rather than a SynchronousQueue. The test below verifies - // the current behavior. - - int numWorkers = 5; // must equal EventDispatcher.MAX_FLUSH_THREADS - LDUser testUser1 = new LDUser("me"); - LDValue testUserJson1 = LDValue.buildObject().put("key", "me").build(); - LDUser testUser2 = new LDUser("you"); - LDValue testUserJson2 = LDValue.buildObject().put("key", "you").build(); - LDUser testUser3 = new LDUser("everyone we know"); - LDValue testUserJson3 = LDValue.buildObject().put("key", "everyone we know").build(); - - Object sendersWaitOnThis = new Object(); - CountDownLatch sendersSignalThisWhenWaiting = new CountDownLatch(numWorkers); - MockEventSender es = new MockEventSender(); - es.waitSignal = sendersWaitOnThis; - es.receivedCounter = sendersSignalThisWhenWaiting; - - try (DefaultEventProcessor ep = makeEventProcessor(baseConfig(es))) { - for (int i = 0; i < 5; i++) { - ep.sendEvent(EventFactory.DEFAULT.newIdentifyEvent(user)); - ep.flush(); - es.awaitRequest(); // we don't need to see this payload, just throw it away - } - - // When our CountDownLatch reaches zero, it means all of the worker threads are blocked in MockEventSender - sendersSignalThisWhenWaiting.await(); - es.waitSignal = null; - es.receivedCounter = null; - - // Now, put an event in the buffer and try to flush again. In the current implementation (see - // above) this payload gets queued in a holding area, and will be flushed after a worker - // becomes free. - Event.Identify event1 = EventFactory.DEFAULT.newIdentifyEvent(testUser1); - ep.sendEvent(event1); - ep.flush(); - - // Do an additional flush with another event. This time, the event processor should see that there's - // no space available and simply ignore the flush request. There's no way to verify programmatically - // that this has happened, so just give it a short delay. - Event.Identify event2 = EventFactory.DEFAULT.newIdentifyEvent(testUser2); - ep.sendEvent(event2); - ep.flush(); - Thread.sleep(100); - - // Enqueue a third event. The current payload should now be event2 + event3. - Event.Identify event3 = EventFactory.DEFAULT.newIdentifyEvent(testUser3); - ep.sendEvent(event3); - - // Now allow the workers to unblock - synchronized (sendersWaitOnThis) { - sendersWaitOnThis.notifyAll(); - } - - // The first unblocked worker should pick up the queued payload with event1. - assertThat(es.getEventsFromLastRequest(), contains(isIdentifyEvent(event1, testUserJson1))); - - // Now a flush should succeed and send the current payload. - ep.flush(); - assertThat(es.getEventsFromLastRequest(), contains( - isIdentifyEvent(event2, testUserJson2), - isIdentifyEvent(event3, testUserJson3))); - } - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorTestBase.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorTestBase.java deleted file mode 100644 index d4222c247..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventProcessorTestBase.java +++ /dev/null @@ -1,257 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableList; -import com.google.gson.Gson; -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventSender; -import com.launchdarkly.sdk.server.interfaces.EventSenderFactory; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.testhelpers.JsonTestValue; - -import org.hamcrest.Matcher; - -import java.io.IOException; -import java.net.URI; -import java.time.Duration; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; - -import static com.launchdarkly.sdk.server.Components.sendEvents; -import static com.launchdarkly.sdk.server.TestComponents.clientContext; -import static com.launchdarkly.testhelpers.ConcurrentHelpers.assertNoMoreValues; -import static com.launchdarkly.testhelpers.ConcurrentHelpers.awaitValue; -import static com.launchdarkly.testhelpers.JsonAssertions.isJsonArray; -import static com.launchdarkly.testhelpers.JsonAssertions.jsonEqualsValue; -import static com.launchdarkly.testhelpers.JsonAssertions.jsonProperty; -import static com.launchdarkly.testhelpers.JsonAssertions.jsonUndefined; -import static com.launchdarkly.testhelpers.JsonTestValue.jsonFromValue; -import static org.hamcrest.Matchers.allOf; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -@SuppressWarnings("javadoc") -public abstract class DefaultEventProcessorTestBase extends BaseTest { - public static final String SDK_KEY = "SDK_KEY"; - public static final URI FAKE_URI = URI.create("http://fake"); - public static final LDUser user = new LDUser.Builder("userkey").name("Red").build(); - public static final Gson gson = new Gson(); - public static final LDValue userJson = LDValue.buildObject().put("key", "userkey").put("name", "Red").build(); - public static final LDValue filteredUserJson = LDValue.buildObject().put("key", "userkey") - .put("privateAttrs", LDValue.buildArray().add("name").build()).build(); - - // Note that all of these events depend on the fact that DefaultEventProcessor does a synchronous - // flush when it is closed; in this case, it's closed implicitly by the try-with-resources block. - - public static EventProcessorBuilder baseConfig(MockEventSender es) { - return sendEvents().eventSender(senderFactory(es)); - } - - public DefaultEventProcessor makeEventProcessor(EventProcessorBuilder ec) { - LDConfig config = new LDConfig.Builder().diagnosticOptOut(true) - .logging(Components.logging(testLogging)).build(); - return makeEventProcessor(ec, config); - } - - public DefaultEventProcessor makeEventProcessor(EventProcessorBuilder ec, LDConfig config) { - return (DefaultEventProcessor)ec.createEventProcessor(clientContext(SDK_KEY, config)); - } - - public DefaultEventProcessor makeEventProcessor(EventProcessorBuilder ec, DiagnosticAccumulator diagnosticAccumulator) { - LDConfig config = new LDConfig.Builder().diagnosticOptOut(false) - .logging(Components.logging(testLogging)).build(); - return (DefaultEventProcessor)ec.createEventProcessor( - clientContext(SDK_KEY, config, diagnosticAccumulator)); - } - - public static EventSenderFactory senderFactory(final MockEventSender es) { - return new EventSenderFactory() { - @Override - public EventSender createEventSender(BasicConfiguration basicConfiguration, HttpConfiguration httpConfiguration) { - return es; - } - }; - } - - public static final class MockEventSender implements EventSender { - volatile boolean closed; - volatile Result result = new Result(true, false, null); - volatile RuntimeException fakeError = null; - volatile IOException fakeErrorOnClose = null; - volatile CountDownLatch receivedCounter = null; - volatile Object waitSignal = null; - - final BlockingQueue receivedParams = new LinkedBlockingQueue<>(); - - static final class Params { - final EventDataKind kind; - final String data; - final int eventCount; - final URI eventsBaseUri; - - Params(EventDataKind kind, String data, int eventCount, URI eventsBaseUri) { - this.kind = kind; - this.data = data; - this.eventCount = eventCount; - assertNotNull(eventsBaseUri); - this.eventsBaseUri = eventsBaseUri; - } - } - - @Override - public void close() throws IOException { - closed = true; - if (fakeErrorOnClose != null) { - throw fakeErrorOnClose; - } - } - - @Override - public Result sendEventData(EventDataKind kind, String data, int eventCount, URI eventsBaseUri) { - receivedParams.add(new Params(kind, data, eventCount, eventsBaseUri)); - if (waitSignal != null) { - // this is used in DefaultEventProcessorTest.eventsAreKeptInBufferIfAllFlushWorkersAreBusy - synchronized (waitSignal) { - if (receivedCounter != null) { - receivedCounter.countDown(); - } - try { - waitSignal.wait(); - } catch (InterruptedException e) {} - } - } - if (fakeError != null) { - throw fakeError; - } - return result; - } - - Params awaitRequest() { - return awaitValue(receivedParams, 5, TimeUnit.SECONDS); - } - - void expectNoRequests(Duration timeout) { - assertNoMoreValues(receivedParams, timeout.toMillis(), TimeUnit.MILLISECONDS); - } - - Iterable getEventsFromLastRequest() { - Params p = awaitRequest(); - LDValue a = LDValue.parse(p.data); - assertEquals(p.eventCount, a.size()); - ImmutableList.Builder ret = ImmutableList.builder(); - for (LDValue v: a.values()) { - ret.add(jsonFromValue(v)); - } - return ret.build(); - } - } - - public static Matcher isIdentifyEvent(Event sourceEvent, LDValue user) { - return allOf( - jsonProperty("kind", "identify"), - jsonProperty("creationDate", (double)sourceEvent.getCreationDate()), - jsonProperty("user", (user == null || user.isNull()) ? jsonUndefined() : jsonEqualsValue(user)) - ); - } - - public static Matcher isIndexEvent(Event sourceEvent, LDValue user) { - return allOf( - jsonProperty("kind", "index"), - jsonProperty("creationDate", (double)sourceEvent.getCreationDate()), - jsonProperty("user", jsonFromValue(user)) - ); - } - - public static Matcher isFeatureEvent(Event.FeatureRequest sourceEvent, DataModel.FeatureFlag flag, boolean debug, LDValue inlineUser) { - return isFeatureEvent(sourceEvent, flag, debug, inlineUser, null); - } - - @SuppressWarnings("unchecked") - public static Matcher isFeatureEvent(Event.FeatureRequest sourceEvent, DataModel.FeatureFlag flag, boolean debug, LDValue inlineUser, - EvaluationReason reason) { - return allOf( - jsonProperty("kind", debug ? "debug" : "feature"), - jsonProperty("creationDate", (double)sourceEvent.getCreationDate()), - jsonProperty("key", flag.getKey()), - jsonProperty("version", (double)flag.getVersion()), - jsonProperty("variation", sourceEvent.getVariation()), - jsonProperty("value", jsonFromValue(sourceEvent.getValue())), - hasUserOrUserKey(sourceEvent, inlineUser), - jsonProperty("reason", reason == null ? jsonUndefined() : jsonEqualsValue(reason)) - ); - } - - public static Matcher isPrerequisiteOf(String parentFlagKey) { - return jsonProperty("prereqOf", parentFlagKey); - } - - public static Matcher isCustomEvent(Event.Custom sourceEvent, LDValue inlineUser) { - boolean hasData = sourceEvent.getData() != null && !sourceEvent.getData().isNull(); - return allOf( - jsonProperty("kind", "custom"), - jsonProperty("creationDate", (double)sourceEvent.getCreationDate()), - jsonProperty("key", sourceEvent.getKey()), - hasUserOrUserKey(sourceEvent, inlineUser), - jsonProperty("data", hasData ? jsonEqualsValue(sourceEvent.getData()) : jsonUndefined()), - jsonProperty("metricValue", sourceEvent.getMetricValue() == null ? jsonUndefined() : jsonEqualsValue(sourceEvent.getMetricValue())) - ); - } - - public static Matcher isAliasEvent(Event.AliasEvent sourceEvent) { - return allOf( - jsonProperty("kind", "alias"), - jsonProperty("creationDate", (double)sourceEvent.getCreationDate()), - jsonProperty("key", sourceEvent.getKey()), - jsonProperty("previousKey", sourceEvent.getPreviousKey()), - jsonProperty("contextKind", sourceEvent.getContextKind()), - jsonProperty("previousContextKind", sourceEvent.getPreviousContextKind()) - ); - } - - public static Matcher hasUserOrUserKey(Event sourceEvent, LDValue inlineUser) { - if (inlineUser != null && !inlineUser.isNull()) { - return allOf( - jsonProperty("user", jsonEqualsValue(inlineUser)), - jsonProperty("userKey", jsonUndefined())); - } - return allOf( - jsonProperty("user", jsonUndefined()), - jsonProperty("userKey", sourceEvent.getUser() == null ? jsonUndefined() : - jsonEqualsValue(sourceEvent.getUser().getKey()))); - } - - public static Matcher isSummaryEvent() { - return jsonProperty("kind", "summary"); - } - - public static Matcher isSummaryEvent(long startDate, long endDate) { - return allOf( - jsonProperty("kind", "summary"), - jsonProperty("startDate", (double)startDate), - jsonProperty("endDate", (double)endDate) - ); - } - - public static Matcher hasSummaryFlag(String key, LDValue defaultVal, Matcher> counters) { - return jsonProperty("features", - jsonProperty(key, allOf( - jsonProperty("default", jsonFromValue(defaultVal)), - jsonProperty("counters", isJsonArray(counters)) - ))); - } - - public static Matcher isSummaryEventCounter(DataModel.FeatureFlag flag, Integer variation, LDValue value, int count) { - return allOf( - jsonProperty("variation", variation), - jsonProperty("version", (double)flag.getVersion()), - jsonProperty("value", jsonFromValue(value)), - jsonProperty("count", (double)count) - ); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java deleted file mode 100644 index e66715069..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultEventSenderTest.java +++ /dev/null @@ -1,402 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.EventSender; -import com.launchdarkly.sdk.server.interfaces.EventSenderFactory; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.testhelpers.httptest.Handler; -import com.launchdarkly.testhelpers.httptest.Handlers; -import com.launchdarkly.testhelpers.httptest.HttpServer; -import com.launchdarkly.testhelpers.httptest.RequestInfo; - -import org.junit.Test; - -import java.net.URI; -import java.text.SimpleDateFormat; -import java.time.Duration; -import java.util.Date; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; - -import static com.launchdarkly.sdk.server.TestComponents.clientContext; -import static com.launchdarkly.sdk.server.interfaces.EventSender.EventDataKind.ANALYTICS; -import static com.launchdarkly.sdk.server.interfaces.EventSender.EventDataKind.DIAGNOSTICS; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.equalToIgnoringCase; -import static org.hamcrest.Matchers.isA; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("javadoc") -public class DefaultEventSenderTest extends BaseTest { - private static final String SDK_KEY = "SDK_KEY"; - private static final String FAKE_DATA = "some data"; - private static final SimpleDateFormat httpDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", - Locale.US); - private static final Duration BRIEF_RETRY_DELAY = Duration.ofMillis(50); - - private EventSender makeEventSender() { - return makeEventSender(LDConfig.DEFAULT); - } - - private EventSender makeEventSender(LDConfig config) { - return new DefaultEventSender( - clientContext(SDK_KEY, config).getHttp(), - BRIEF_RETRY_DELAY, - testLogger - ); - } - - @Test - public void factoryCreatesDefaultSenderWithDefaultRetryDelay() throws Exception { - EventSenderFactory f = new DefaultEventSender.Factory(); - ClientContext context = clientContext(SDK_KEY, LDConfig.DEFAULT); - try (EventSender es = f.createEventSender(context.getBasic(), context.getHttp())) { - assertThat(es, isA(EventSender.class)); - assertThat(((DefaultEventSender)es).retryDelay, equalTo(DefaultEventSender.DEFAULT_RETRY_DELAY)); - } - } - - @Test - public void constructorUsesDefaultRetryDelayIfNotSpecified() throws Exception { - ClientContext context = clientContext(SDK_KEY, LDConfig.DEFAULT); - try (EventSender es = new DefaultEventSender(context.getHttp(), null, testLogger)) { - assertThat(((DefaultEventSender)es).retryDelay, equalTo(DefaultEventSender.DEFAULT_RETRY_DELAY)); - } - } - - @Test - public void analyticsDataIsDelivered() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - EventSender.Result result = es.sendEventData(ANALYTICS, FAKE_DATA, 1, server.getUri()); - - assertTrue(result.isSuccess()); - assertFalse(result.isMustShutDown()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - assertEquals("/bulk", req.getPath()); - assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); - assertEquals(FAKE_DATA, req.getBody()); - } - } - - @Test - public void diagnosticDataIsDelivered() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - EventSender.Result result = es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, server.getUri()); - - assertTrue(result.isSuccess()); - assertFalse(result.isMustShutDown()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - assertEquals("/diagnostic", req.getPath()); - assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); - assertEquals(FAKE_DATA, req.getBody()); - } - } - - @Test - public void defaultHeadersAreSentForAnalytics() throws Exception { - HttpConfiguration httpConfig = clientContext(SDK_KEY, LDConfig.DEFAULT).getHttp(); - - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - es.sendEventData(ANALYTICS, FAKE_DATA, 1, server.getUri()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - for (Map.Entry kv: httpConfig.getDefaultHeaders()) { - assertThat(req.getHeader(kv.getKey()), equalTo(kv.getValue())); - } - } - } - - @Test - public void defaultHeadersAreSentForDiagnostics() throws Exception { - HttpConfiguration httpConfig = clientContext(SDK_KEY, LDConfig.DEFAULT).getHttp(); - - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, server.getUri()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - for (Map.Entry kv: httpConfig.getDefaultHeaders()) { - assertThat(req.getHeader(kv.getKey()), equalTo(kv.getValue())); - } - } - } - - @Test - public void eventSchemaIsSentForAnalytics() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - es.sendEventData(ANALYTICS, FAKE_DATA, 1, server.getUri()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - assertThat(req.getHeader("X-LaunchDarkly-Event-Schema"), equalTo("3")); - } - } - - @Test - public void eventPayloadIdIsSentForAnalytics() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - es.sendEventData(ANALYTICS, FAKE_DATA, 1, server.getUri()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - String payloadHeaderValue = req.getHeader("X-LaunchDarkly-Payload-ID"); - assertThat(payloadHeaderValue, notNullValue(String.class)); - assertThat(UUID.fromString(payloadHeaderValue), notNullValue(UUID.class)); - } - } - - @Test - public void eventPayloadIdReusedOnRetry() throws Exception { - Handler errorResponse = Handlers.status(429); - Handler errorThenSuccess = Handlers.sequential(errorResponse, eventsSuccessResponse(), eventsSuccessResponse()); - - try (HttpServer server = HttpServer.start(errorThenSuccess)) { - try (EventSender es = makeEventSender()) { - es.sendEventData(ANALYTICS, FAKE_DATA, 1, server.getUri()); - es.sendEventData(ANALYTICS, FAKE_DATA, 1, server.getUri()); - } - - // Failed response request - RequestInfo req = server.getRecorder().requireRequest(); - String payloadId = req.getHeader("X-LaunchDarkly-Payload-ID"); - // Retry request has same payload ID as failed request - req = server.getRecorder().requireRequest(); - String retryId = req.getHeader("X-LaunchDarkly-Payload-ID"); - assertThat(retryId, equalTo(payloadId)); - // Second request has different payload ID from first request - req = server.getRecorder().requireRequest(); - payloadId = req.getHeader("X-LaunchDarkly-Payload-ID"); - assertThat(retryId, not(equalTo(payloadId))); - } - } - - @Test - public void eventSchemaNotSetOnDiagnosticEvents() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, server.getUri()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - assertNull(req.getHeader("X-LaunchDarkly-Event-Schema")); - } - } - - @Test - public void http400ErrorIsRecoverable() throws Exception { - testRecoverableHttpError(400); - } - - @Test - public void http401ErrorIsUnrecoverable() throws Exception { - testUnrecoverableHttpError(401); - } - - @Test - public void http403ErrorIsUnrecoverable() throws Exception { - testUnrecoverableHttpError(403); - } - - // Cannot test our retry logic for 408, because OkHttp insists on doing its own retry on 408 so that - // we never actually see that response status. -// @Test -// public void http408ErrorIsRecoverable() throws Exception { -// testRecoverableHttpError(408); -// } - - @Test - public void http429ErrorIsRecoverable() throws Exception { - testRecoverableHttpError(429); - } - - @Test - public void http500ErrorIsRecoverable() throws Exception { - testRecoverableHttpError(500); - } - - @Test - public void serverDateIsParsed() throws Exception { - long fakeTime = ((new Date().getTime() - 100000) / 1000) * 1000; // don't expect millisecond precision - Handler resp = Handlers.all(eventsSuccessResponse(), addDateHeader(new Date(fakeTime))); - - try (HttpServer server = HttpServer.start(resp)) { - try (EventSender es = makeEventSender()) { - EventSender.Result result = es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, server.getUri()); - - assertNotNull(result.getTimeFromServer()); - assertEquals(fakeTime, result.getTimeFromServer().getTime()); - } - } - } - - @Test - public void invalidServerDateIsIgnored() throws Exception { - Handler resp = Handlers.all(eventsSuccessResponse(), Handlers.header("Date", "not a date")); - - try (HttpServer server = HttpServer.start(resp)) { - try (EventSender es = makeEventSender()) { - EventSender.Result result = es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, server.getUri()); - - assertTrue(result.isSuccess()); - assertNull(result.getTimeFromServer()); - } - } - } - - @Test - public void testSpecialHttpConfigurations() throws Exception { - Handler handler = eventsSuccessResponse(); - - TestHttpUtil.testWithSpecialHttpConfigurations(handler, - (targetUri, goodHttpConfig) -> { - LDConfig config = new LDConfig.Builder().http(goodHttpConfig).build(); - - try (EventSender es = makeEventSender(config)) { - EventSender.Result result = es.sendEventData(ANALYTICS, FAKE_DATA, 1, targetUri); - - assertTrue(result.isSuccess()); - assertFalse(result.isMustShutDown()); - } - }, - - (targetUri, badHttpConfig) -> { - LDConfig config = new LDConfig.Builder().http(badHttpConfig).build(); - - try (EventSender es = makeEventSender(config)) { - EventSender.Result result = es.sendEventData(ANALYTICS, FAKE_DATA, 1, targetUri); - - assertFalse(result.isSuccess()); - assertFalse(result.isMustShutDown()); - } - } - ); - } - - @Test - public void baseUriDoesNotNeedTrailingSlash() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - URI uriWithoutSlash = URI.create(server.getUri().toString().replaceAll("/$", "")); - EventSender.Result result = es.sendEventData(ANALYTICS, FAKE_DATA, 1, uriWithoutSlash); - - assertTrue(result.isSuccess()); - assertFalse(result.isMustShutDown()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - assertEquals("/bulk", req.getPath()); - assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); - assertEquals(FAKE_DATA, req.getBody()); - } - } - - @Test - public void baseUriCanHaveContextPath() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - URI baseUri = server.getUri().resolve("/context/path"); - EventSender.Result result = es.sendEventData(ANALYTICS, FAKE_DATA, 1, baseUri); - - assertTrue(result.isSuccess()); - assertFalse(result.isMustShutDown()); - } - - RequestInfo req = server.getRecorder().requireRequest(); - assertEquals("/context/path/bulk", req.getPath()); - assertThat(req.getHeader("content-type"), equalToIgnoringCase("application/json; charset=utf-8")); - assertEquals(FAKE_DATA, req.getBody()); - } - } - - @Test - public void nothingIsSentForNullData() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - EventSender.Result result1 = es.sendEventData(ANALYTICS, null, 0, server.getUri()); - EventSender.Result result2 = es.sendEventData(DIAGNOSTICS, null, 0, server.getUri()); - - assertTrue(result1.isSuccess()); - assertTrue(result2.isSuccess()); - assertEquals(0, server.getRecorder().count()); - } - } - } - - @Test - public void nothingIsSentForEmptyData() throws Exception { - try (HttpServer server = HttpServer.start(eventsSuccessResponse())) { - try (EventSender es = makeEventSender()) { - EventSender.Result result1 = es.sendEventData(ANALYTICS, "", 0, server.getUri()); - EventSender.Result result2 = es.sendEventData(DIAGNOSTICS, "", 0, server.getUri()); - - assertTrue(result1.isSuccess()); - assertTrue(result2.isSuccess()); - assertEquals(0, server.getRecorder().count()); - } - } - } - - private void testUnrecoverableHttpError(int status) throws Exception { - Handler errorResponse = Handlers.status(status); - - try (HttpServer server = HttpServer.start(errorResponse)) { - try (EventSender es = makeEventSender()) { - EventSender.Result result = es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, server.getUri()); - - assertFalse(result.isSuccess()); - assertTrue(result.isMustShutDown()); - } - - server.getRecorder().requireRequest(); - - // it does not retry after this type of error, so there are no more requests - server.getRecorder().requireNoRequests(Duration.ofMillis(100)); - } - } - - private void testRecoverableHttpError(int status) throws Exception { - Handler errorResponse = Handlers.status(status); - Handler errorsThenSuccess = Handlers.sequential(errorResponse, errorResponse, eventsSuccessResponse()); - // send two errors in a row, because the flush will be retried one time - - try (HttpServer server = HttpServer.start(errorsThenSuccess)) { - try (EventSender es = makeEventSender()) { - EventSender.Result result = es.sendEventData(DIAGNOSTICS, FAKE_DATA, 1, server.getUri()); - - assertFalse(result.isSuccess()); - assertFalse(result.isMustShutDown()); - } - - server.getRecorder().requireRequest(); - server.getRecorder().requireRequest(); - server.getRecorder().requireNoRequests(Duration.ofMillis(100)); // only 2 requests total - } - } - - private Handler eventsSuccessResponse() { - return Handlers.status(202); - } - - private Handler addDateHeader(Date date) { - return Handlers.header("Date", httpDateFormat.format(date)); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java index 5a29a42a2..15d33c059 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/DefaultFeatureRequestorTest.java @@ -1,16 +1,20 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.internal.http.HttpErrors.HttpErrorException; +import com.launchdarkly.sdk.internal.http.HttpProperties; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Segment; import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import com.launchdarkly.testhelpers.httptest.Handler; import com.launchdarkly.testhelpers.httptest.Handlers; import com.launchdarkly.testhelpers.httptest.HttpServer; import com.launchdarkly.testhelpers.httptest.RequestInfo; +import com.launchdarkly.testhelpers.httptest.SpecialHttpConfigurations; import org.junit.Test; @@ -27,7 +31,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; @SuppressWarnings("javadoc") public class DefaultFeatureRequestorTest extends BaseTest { @@ -52,8 +55,8 @@ private DefaultFeatureRequestor makeRequestor(HttpServer server, LDConfig config return new DefaultFeatureRequestor(makeHttpConfig(config), server.getUri(), testLogger); } - private HttpConfiguration makeHttpConfig(LDConfig config) { - return config.httpConfigFactory.createHttpConfiguration(new BasicConfiguration(sdkKey, false, 0, null, null)); + private HttpProperties makeHttpConfig(LDConfig config) { + return ComponentsImpl.toHttpProperties(config.http.build(new ClientContext(sdkKey))); } private void verifyExpectedData(FullDataSet data) { @@ -146,30 +149,19 @@ public void responseIsCachedButWeWantDataAnyway() throws Exception { public void testSpecialHttpConfigurations() throws Exception { Handler handler = Handlers.bodyJson(allDataJson); - TestHttpUtil.testWithSpecialHttpConfigurations(handler, - (targetUri, goodHttpConfig) -> { - LDConfig config = new LDConfig.Builder().http(goodHttpConfig).build(); - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(makeHttpConfig(config), targetUri, testLogger)) { - try { - FullDataSet data = r.getAllData(false); - verifyExpectedData(data); - } catch (Exception e) { - throw new RuntimeException(e); - } + SpecialHttpConfigurations.testAll(handler, + (URI serverUri, SpecialHttpConfigurations.Params params) -> { + LDConfig config = new LDConfig.Builder().http(TestUtil.makeHttpConfigurationFromTestParams(params)).build(); + try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(makeHttpConfig(config), serverUri, testLogger)) { + FullDataSet data = r.getAllData(false); + verifyExpectedData(data); + return true; + } catch (SerializationException e) { + throw new SpecialHttpConfigurations.UnexpectedResponseException(e.toString()); + } catch (HttpErrorException e) { + throw new SpecialHttpConfigurations.UnexpectedResponseException(e.toString()); } - }, - - (targetUri, badHttpConfig) -> { - LDConfig config = new LDConfig.Builder().http(badHttpConfig).build(); - try (DefaultFeatureRequestor r = new DefaultFeatureRequestor(makeHttpConfig(config), targetUri, testLogger)) { - try { - r.getAllData(false); - fail("expected exception"); - } catch (Exception e) { - } - } - } - ); + }); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/DiagnosticAccumulatorTest.java b/src/test/java/com/launchdarkly/sdk/server/DiagnosticAccumulatorTest.java deleted file mode 100644 index 06f9baa81..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DiagnosticAccumulatorTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.server.DiagnosticAccumulator; -import com.launchdarkly.sdk.server.DiagnosticEvent; -import com.launchdarkly.sdk.server.DiagnosticId; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertSame; - -@SuppressWarnings("javadoc") -public class DiagnosticAccumulatorTest { - @Test - public void createsDiagnosticStatisticsEvent() { - DiagnosticId diagnosticId = new DiagnosticId("SDK_KEY"); - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(diagnosticId); - long startDate = diagnosticAccumulator.dataSinceDate; - DiagnosticEvent.Statistics diagnosticStatisticsEvent = diagnosticAccumulator.createEventAndReset(10, 15); - assertSame(diagnosticId, diagnosticStatisticsEvent.id); - assertEquals(10, diagnosticStatisticsEvent.droppedEvents); - assertEquals(15, diagnosticStatisticsEvent.deduplicatedUsers); - assertEquals(0, diagnosticStatisticsEvent.eventsInLastBatch); - assertEquals(startDate, diagnosticStatisticsEvent.dataSinceDate); - } - - @Test - public void canRecordStreamInit() { - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(new DiagnosticId("SDK_KEY")); - diagnosticAccumulator.recordStreamInit(1000, 200, false); - DiagnosticEvent.Statistics statsEvent = diagnosticAccumulator.createEventAndReset(0, 0); - assertEquals(1, statsEvent.streamInits.size()); - assertEquals(1000, statsEvent.streamInits.get(0).timestamp); - assertEquals(200, statsEvent.streamInits.get(0).durationMillis); - assertEquals(false, statsEvent.streamInits.get(0).failed); - } - - @Test - public void canRecordEventsInBatch() { - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(new DiagnosticId("SDK_KEY")); - diagnosticAccumulator.recordEventsInBatch(100); - DiagnosticEvent.Statistics statsEvent = diagnosticAccumulator.createEventAndReset(0, 0); - assertEquals(100, statsEvent.eventsInLastBatch); - } - - @Test - public void resetsAccumulatorFieldsOnCreate() throws InterruptedException { - DiagnosticAccumulator diagnosticAccumulator = new DiagnosticAccumulator(new DiagnosticId("SDK_KEY")); - diagnosticAccumulator.recordStreamInit(1000, 200, false); - diagnosticAccumulator.recordEventsInBatch(100); - long startDate = diagnosticAccumulator.dataSinceDate; - Thread.sleep(2); - diagnosticAccumulator.createEventAndReset(0, 0); - assertNotEquals(startDate, diagnosticAccumulator.dataSinceDate); - DiagnosticEvent.Statistics resetEvent = diagnosticAccumulator.createEventAndReset(0,0); - assertEquals(0, resetEvent.streamInits.size()); - assertEquals(0, resetEvent.eventsInLastBatch); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/DiagnosticIdTest.java b/src/test/java/com/launchdarkly/sdk/server/DiagnosticIdTest.java deleted file mode 100644 index a81bd95eb..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DiagnosticIdTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.launchdarkly.sdk.server.DiagnosticId; - -import org.junit.Test; - -import java.util.UUID; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -@SuppressWarnings("javadoc") -public class DiagnosticIdTest { - private static final Gson gson = new Gson(); - - @Test - public void hasUUID() { - DiagnosticId diagnosticId = new DiagnosticId("SDK_KEY"); - assertNotNull(diagnosticId.diagnosticId); - assertNotNull(UUID.fromString(diagnosticId.diagnosticId)); - } - - @Test - public void nullKeyIsSafe() { - // We can't send diagnostics without a key anyway, so we're just validating that the - // constructor won't crash with a null key - new DiagnosticId(null); - } - - @Test - public void shortKeyIsSafe() { - DiagnosticId diagnosticId = new DiagnosticId("foo"); - assertEquals("foo", diagnosticId.sdkKeySuffix); - } - - @Test - public void keyIsSuffix() { - DiagnosticId diagnosticId = new DiagnosticId("this_is_a_fake_key"); - assertEquals("ke_key", diagnosticId.sdkKeySuffix); - } - - @Test - public void gsonSerialization() { - DiagnosticId diagnosticId = new DiagnosticId("this_is_a_fake_key"); - JsonObject jsonObject = gson.toJsonTree(diagnosticId).getAsJsonObject(); - assertEquals(2, jsonObject.size()); - String id = jsonObject.getAsJsonPrimitive("diagnosticId").getAsString(); - assertNotNull(UUID.fromString(id)); - assertEquals("ke_key", jsonObject.getAsJsonPrimitive("sdkKeySuffix").getAsString()); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/DiagnosticSdkTest.java b/src/test/java/com/launchdarkly/sdk/server/DiagnosticSdkTest.java deleted file mode 100644 index 1c3f07dca..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/DiagnosticSdkTest.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; -import com.launchdarkly.sdk.server.DiagnosticEvent.Init.DiagnosticSdk; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; - -import org.junit.Test; - -import static com.launchdarkly.sdk.server.TestComponents.clientContext; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -@SuppressWarnings("javadoc") -public class DiagnosticSdkTest { - private static final Gson gson = new Gson(); - - private static HttpConfiguration makeHttpConfig(LDConfig config) { - // the SDK key doesn't matter for these tests - return clientContext("SDK_KEY", config).getHttp(); - } - - @Test - public void defaultFieldValues() { - DiagnosticSdk diagnosticSdk = new DiagnosticSdk(makeHttpConfig(LDConfig.DEFAULT)); - assertEquals("java-server-sdk", diagnosticSdk.name); - assertEquals(Version.SDK_VERSION, diagnosticSdk.version); - assertNull(diagnosticSdk.wrapperName); - assertNull(diagnosticSdk.wrapperVersion); - } - - @Test - public void getsWrapperValuesFromConfig() { - LDConfig config1 = new LDConfig.Builder() - .http(Components.httpConfiguration().wrapper("Scala", "0.1.0")) - .build(); - DiagnosticSdk diagnosticSdk1 = new DiagnosticSdk(makeHttpConfig(config1)); - assertEquals("java-server-sdk", diagnosticSdk1.name); - assertEquals(Version.SDK_VERSION, diagnosticSdk1.version); - assertEquals(diagnosticSdk1.wrapperName, "Scala"); - assertEquals(diagnosticSdk1.wrapperVersion, "0.1.0"); - - LDConfig config2 = new LDConfig.Builder() - .http(Components.httpConfiguration().wrapper("Scala", null)) - .build(); - DiagnosticSdk diagnosticSdk2 = new DiagnosticSdk(makeHttpConfig(config2)); - assertEquals(diagnosticSdk2.wrapperName, "Scala"); - assertNull(diagnosticSdk2.wrapperVersion); - } - - @Test - public void gsonSerializationNoWrapper() { - DiagnosticSdk diagnosticSdk = new DiagnosticSdk(makeHttpConfig(LDConfig.DEFAULT)); - JsonObject jsonObject = gson.toJsonTree(diagnosticSdk).getAsJsonObject(); - assertEquals(2, jsonObject.size()); - assertEquals("java-server-sdk", jsonObject.getAsJsonPrimitive("name").getAsString()); - assertEquals(Version.SDK_VERSION, jsonObject.getAsJsonPrimitive("version").getAsString()); - } - - @Test - public void gsonSerializationWithWrapper() { - LDConfig config = new LDConfig.Builder() - .http(Components.httpConfiguration().wrapper("Scala", "0.1.0")) - .build(); - DiagnosticSdk diagnosticSdk = new DiagnosticSdk(makeHttpConfig(config)); - JsonObject jsonObject = gson.toJsonTree(diagnosticSdk).getAsJsonObject(); - assertEquals(4, jsonObject.size()); - assertEquals("java-server-sdk", jsonObject.getAsJsonPrimitive("name").getAsString()); - assertEquals(Version.SDK_VERSION, jsonObject.getAsJsonPrimitive("version").getAsString()); - assertEquals("Scala", jsonObject.getAsJsonPrimitive("wrapperName").getAsString()); - assertEquals("0.1.0", jsonObject.getAsJsonPrimitive("wrapperVersion").getAsString()); - } - - @Test - public void platformOsNames() { - String realOsName = System.getProperty("os.name"); - try { - System.setProperty("os.name", "Mac OS X"); - assertEquals("MacOS", new DiagnosticEvent.Init.DiagnosticPlatform().osName); - - System.setProperty("os.name", "Windows 10"); - assertEquals("Windows", new DiagnosticEvent.Init.DiagnosticPlatform().osName); - - System.setProperty("os.name", "Linux"); - assertEquals("Linux", new DiagnosticEvent.Init.DiagnosticPlatform().osName); - - System.clearProperty("os.name"); - assertNull(new DiagnosticEvent.Init.DiagnosticPlatform().osName); - } finally { - System.setProperty("os.name", realOsName); - } - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java index a4d6a8e8c..28d056437 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBigSegmentTest.java @@ -1,23 +1,27 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Segment; +import com.launchdarkly.sdk.server.DataModel.SegmentRule; import org.junit.Test; -import java.util.Collections; - import static com.launchdarkly.sdk.server.Evaluator.makeBigSegmentRef; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingContext; import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment; -import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingUser; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.segmentRuleBuilder; -import static com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.createMembershipFromSegmentRefs; +import static com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.createMembershipFromSegmentRefs; +import static java.util.Arrays.asList; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.strictMock; @@ -25,14 +29,14 @@ @SuppressWarnings("javadoc") public class EvaluatorBigSegmentTest extends EvaluatorTestBase { - private static final LDUser testUser = new LDUser("userkey"); + private static final LDContext testUser = LDContext.create("userkey"); @Test public void bigSegmentWithNoProviderIsNotMatched() { - DataModel.Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(1) + Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(1) .included(testUser.getKey()) // Included should be ignored for a big segment .build(); - DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); + FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), null).build(); EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); @@ -43,8 +47,8 @@ public void bigSegmentWithNoProviderIsNotMatched() { @Test public void bigSegmentWithNoGenerationIsNotMatched() { // Segment without generation - DataModel.Segment segment = segmentBuilder("segmentkey").unbounded(true).build(); - DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); + Segment segment = segmentBuilder("segmentkey").unbounded(true).build(); + FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).build(); EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); @@ -53,27 +57,51 @@ public void bigSegmentWithNoGenerationIsNotMatched() { } @Test - public void matchedWithInclude() { - DataModel.Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(2).build(); - DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); + public void matchedWithIncludeForDefaultKind() { + testMatchedWithInclude(false, false); + testMatchedWithInclude(false, true); + } + + @Test + public void matchedWithIncludeForNonDefaultKind() { + testMatchedWithInclude(true, false); + testMatchedWithInclude(true, true); + } + + private void testMatchedWithInclude(boolean nonDefaultKind, boolean multiKindContext) { + String targetKey = "contextkey"; + ContextKind kind1 = ContextKind.of("kind1"); + LDContext singleKindContext = nonDefaultKind ? LDContext.create(kind1, targetKey) : LDContext.create(targetKey); + LDContext evalContext = multiKindContext ? + LDContext.createMulti(singleKindContext, LDContext.create(ContextKind.of("kind2"), "key2")) : + singleKindContext; + + Segment segment = segmentBuilder("segmentkey") + .unbounded(true) + .unboundedContextKind(nonDefaultKind ? kind1 : null) + .generation(2) + .build(); + FeatureFlag flag = booleanFlagWithClauses("flagkey", clauseMatchingSegment(segment)); + BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = new BigSegmentStoreWrapper.BigSegmentsQueryResult(); queryResult.status = BigSegmentsStatus.HEALTHY; - queryResult.membership = createMembershipFromSegmentRefs(Collections.singleton(makeBigSegmentRef(segment)), null); - Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), queryResult).build(); + queryResult.membership = createMembershipFromSegmentRefs(asList(makeBigSegmentRef(segment)), null); + Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment) + .withBigSegmentQueryResult(targetKey, queryResult).build(); - EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); + EvalResult result = evaluator.evaluate(flag, evalContext, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(true), result.getValue()); assertEquals(BigSegmentsStatus.HEALTHY, result.getReason().getBigSegmentsStatus()); } - + @Test public void matchedWithRule() { - DataModel.Clause clause = clauseMatchingUser(testUser); - DataModel.SegmentRule segmentRule = segmentRuleBuilder().clauses(clause).build(); - DataModel.Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(2) + Clause clause = clauseMatchingContext(testUser); + SegmentRule segmentRule = segmentRuleBuilder().clauses(clause).build(); + Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(2) .rules(segmentRule) .build(); - DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); + FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = new BigSegmentStoreWrapper.BigSegmentsQueryResult(); queryResult.status = BigSegmentsStatus.HEALTHY; queryResult.membership = createMembershipFromSegmentRefs(null, null); @@ -86,15 +114,15 @@ public void matchedWithRule() { @Test public void unmatchedByExcludeRegardlessOfRule() { - DataModel.Clause clause = clauseMatchingUser(testUser); - DataModel.SegmentRule segmentRule = segmentRuleBuilder().clauses(clause).build(); - DataModel.Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(2) + Clause clause = clauseMatchingContext(testUser); + SegmentRule segmentRule = segmentRuleBuilder().clauses(clause).build(); + Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(2) .rules(segmentRule) .build(); - DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); + FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = new BigSegmentStoreWrapper.BigSegmentsQueryResult(); queryResult.status = BigSegmentsStatus.HEALTHY; - queryResult.membership = createMembershipFromSegmentRefs(null, Collections.singleton(makeBigSegmentRef(segment))); + queryResult.membership = createMembershipFromSegmentRefs(null, asList(makeBigSegmentRef(segment))); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), queryResult).build(); EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); @@ -104,11 +132,11 @@ public void unmatchedByExcludeRegardlessOfRule() { @Test public void bigSegmentStatusIsReturnedFromProvider() { - DataModel.Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(2).build(); - DataModel.FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); + Segment segment = segmentBuilder("segmentkey").unbounded(true).generation(2).build(); + FeatureFlag flag = booleanFlagWithClauses("key", clauseMatchingSegment(segment)); BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = new BigSegmentStoreWrapper.BigSegmentsQueryResult(); queryResult.status = BigSegmentsStatus.STALE; - queryResult.membership = createMembershipFromSegmentRefs(Collections.singleton(makeBigSegmentRef(segment)), null); + queryResult.membership = createMembershipFromSegmentRefs(asList(makeBigSegmentRef(segment)), null); Evaluator evaluator = evaluatorBuilder().withStoredSegments(segment).withBigSegmentQueryResult(testUser.getKey(), queryResult).build(); EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); @@ -117,31 +145,50 @@ public void bigSegmentStatusIsReturnedFromProvider() { } @Test - public void bigSegmentStateIsQueriedOnlyOncePerUserEvenIfFlagReferencesMultipleSegments() { - DataModel.Segment segment1 = segmentBuilder("segmentkey1").unbounded(true).generation(2).build(); - DataModel.Segment segment2 = segmentBuilder("segmentkey2").unbounded(true).generation(3).build(); - DataModel.FeatureFlag flag = flagBuilder("key") + public void bigSegmentStateIsQueriedOnlyOncePerKeyEvenIfFlagReferencesMultipleSegments() { + ContextKind kind1 = ContextKind.of("kind1"), kind2 = ContextKind.of("kind2"), kind3 = ContextKind.of("kind3"); + String key1 = "contextkey1", key2 = "contextkey2"; + LDContext context = LDContext.createMulti( + LDContext.create(kind1, key1), + LDContext.create(kind2, key2), + LDContext.create(kind3, key2) // deliberately using same key for kind2 and kind3 + ); + + Segment segment1 = segmentBuilder("segmentkey1").unbounded(true).unboundedContextKind(kind1).generation(2).build(); + Segment segment2 = segmentBuilder("segmentkey2").unbounded(true).unboundedContextKind(kind2).generation(3).build(); + Segment segment3 = segmentBuilder("segmentkey3").unbounded(true).unboundedContextKind(kind3).generation(4).build(); + + // Set up the flag with a rule for each segment + FeatureFlag flag = flagBuilder("key") .on(true) .fallthroughVariation(0) .variations(false, true) .rules( ruleBuilder().variation(1).clauses(clauseMatchingSegment(segment1)).build(), - ruleBuilder().variation(1).clauses(clauseMatchingSegment(segment2)).build() + ruleBuilder().variation(1).clauses(clauseMatchingSegment(segment2)).build(), + ruleBuilder().variation(1).clauses(clauseMatchingSegment(segment3)).build() ) .build(); - BigSegmentStoreWrapper.BigSegmentsQueryResult queryResult = new BigSegmentStoreWrapper.BigSegmentsQueryResult(); - queryResult.status = BigSegmentsStatus.HEALTHY; - queryResult.membership = createMembershipFromSegmentRefs(Collections.singleton(makeBigSegmentRef(segment2)), null); + // Set up the fake big segment store so that it will report a match only for segment3 with key2. + // Since segment1 and segment2 won't match, all three rules will be evaluated, and since each + // segment uses a different ContextKind, we will be testing keys from all three of the individual + // contexts. But two of those are the same key, and since big segment queries are cached per key, + // we should only see a single query for that one. + BigSegmentStoreWrapper.BigSegmentsQueryResult queryResultForKey2 = new BigSegmentStoreWrapper.BigSegmentsQueryResult(); + queryResultForKey2.status = BigSegmentsStatus.HEALTHY; + queryResultForKey2.membership = createMembershipFromSegmentRefs(asList(makeBigSegmentRef(segment3)), null); Evaluator.Getters mockGetters = strictMock(Evaluator.Getters.class); expect(mockGetters.getSegment(segment1.getKey())).andReturn(segment1); - expect(mockGetters.getBigSegments(testUser.getKey())).andReturn(queryResult); + expect(mockGetters.getBigSegments(key1)).andReturn(null).times(1); expect(mockGetters.getSegment(segment2.getKey())).andReturn(segment2); + expect(mockGetters.getBigSegments(key2)).andReturn(queryResultForKey2).times(1); + expect(mockGetters.getSegment(segment3.getKey())).andReturn(segment3); replay(mockGetters); Evaluator evaluator = new Evaluator(mockGetters, testLogger); - EvalResult result = evaluator.evaluate(flag, testUser, expectNoPrerequisiteEvals()); + EvalResult result = evaluator.evaluate(flag, context, expectNoPrerequisiteEvals()); assertEquals(LDValue.of(true), result.getValue()); assertEquals(BigSegmentsStatus.HEALTHY, result.getReason().getBigSegmentsStatus()); } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java index fcadc8e0c..6e0a16477 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java @@ -1,11 +1,9 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.AttributeRef; import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.DataModel.Operator; import com.launchdarkly.sdk.server.DataModel.Rollout; import com.launchdarkly.sdk.server.DataModel.RolloutKind; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; @@ -15,14 +13,15 @@ import java.util.Arrays; import java.util.List; +import static com.launchdarkly.sdk.server.EvaluatorBucketing.computeBucketValue; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingContext; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotEquals; @SuppressWarnings("javadoc") public class EvaluatorBucketingTest { @@ -30,13 +29,13 @@ public class EvaluatorBucketingTest { @Test public void variationIndexIsReturnedForBucket() { - LDUser user = new LDUser.Builder("userkey").build(); + LDContext context = LDContext.create("userkey"); String flagKey = "flagkey"; String salt = "salt"; // First verify that with our test inputs, the bucket value will be greater than zero and less than 100000, // so we can construct a rollout whose second bucket just barely contains that value - int bucketValue = (int)(EvaluatorBucketing.bucketUser(noSeed, user, flagKey, UserAttribute.KEY, salt) * 100000); + int bucketValue = (int)(computeBucketValue(false, noSeed, context, null, flagKey, null, salt) * 100000); assertThat(bucketValue, greaterThanOrEqualTo(1)); assertThat(bucketValue, lessThan(100000)); @@ -45,107 +44,98 @@ public void variationIndexIsReturnedForBucket() { new WeightedVariation(badVariationA, bucketValue, true), // end of bucket range is not inclusive, so it will *not* match the target value new WeightedVariation(matchedVariation, 1, true), // size of this bucket is 1, so it only matches that specific value new WeightedVariation(badVariationB, 100000 - (bucketValue + 1), true)); - Rollout rollout = new Rollout(variations, null, RolloutKind.rollout); + Rollout rollout = new Rollout(null, variations, null, RolloutKind.rollout, null); - assertVariationIndexFromRollout(matchedVariation, rollout, user, flagKey, salt); + assertVariationIndexFromRollout(matchedVariation, rollout, context, flagKey, salt); } @Test public void usingSeedIsDifferentThanSalt() { - LDUser user = new LDUser.Builder("userkey").build(); + LDContext context = LDContext.create("userkey"); String flagKey = "flagkey"; String salt = "salt"; Integer seed = 123; - float bucketValue1 = EvaluatorBucketing.bucketUser(noSeed, user, flagKey, UserAttribute.KEY, salt); - float bucketValue2 = EvaluatorBucketing.bucketUser(seed, user, flagKey, UserAttribute.KEY, salt); + float bucketValue1 = computeBucketValue(false, noSeed, context, null, flagKey, null, salt); + float bucketValue2 = computeBucketValue(true, seed, context, null, flagKey, null, salt); assert(bucketValue1 != bucketValue2); } @Test public void differentSeedsProduceDifferentAssignment() { - LDUser user = new LDUser.Builder("userkey").build(); + LDContext context = LDContext.create("userkey"); String flagKey = "flagkey"; String salt = "salt"; Integer seed1 = 123; Integer seed2 = 456; - float bucketValue1 = EvaluatorBucketing.bucketUser(seed1, user, flagKey, UserAttribute.KEY, salt); - float bucketValue2 = EvaluatorBucketing.bucketUser(seed2, user, flagKey, UserAttribute.KEY, salt); + float bucketValue1 = computeBucketValue(true, seed1, context, null, flagKey, null, salt); + float bucketValue2 = computeBucketValue(true, seed2, context, null, flagKey, null, salt); assert(bucketValue1 != bucketValue2); } @Test public void flagKeyAndSaltDoNotMatterWhenSeedIsUsed() { - LDUser user = new LDUser.Builder("userkey").build(); + LDContext context = LDContext.create("userkey"); String flagKey1 = "flagkey"; String flagKey2 = "flagkey2"; String salt1 = "salt"; String salt2 = "salt2"; Integer seed = 123; - float bucketValue1 = EvaluatorBucketing.bucketUser(seed, user, flagKey1, UserAttribute.KEY, salt1); - float bucketValue2 = EvaluatorBucketing.bucketUser(seed, user, flagKey2, UserAttribute.KEY, salt2); + float bucketValue1 = computeBucketValue(true, seed, context, null, flagKey1, null, salt1); + float bucketValue2 = computeBucketValue(true, seed, context, null, flagKey2, null, salt2); assert(bucketValue1 == bucketValue2); } @Test public void lastBucketIsUsedIfBucketValueEqualsTotalWeight() { - LDUser user = new LDUser.Builder("userkey").build(); + LDContext context = LDContext.create("userkey"); String flagKey = "flagkey"; String salt = "salt"; // We'll construct a list of variations that stops right at the target bucket value - int bucketValue = (int)(EvaluatorBucketing.bucketUser(noSeed, user, flagKey, UserAttribute.KEY, salt) * 100000); + int bucketValue = (int)(computeBucketValue(false, noSeed, context, null, flagKey, null, salt) * 100000); List variations = Arrays.asList(new WeightedVariation(0, bucketValue, true)); - Rollout rollout = new Rollout(variations, null, RolloutKind.rollout); + Rollout rollout = new Rollout(null, variations, null, RolloutKind.rollout, null); - assertVariationIndexFromRollout(0, rollout, user, flagKey, salt); + assertVariationIndexFromRollout(0, rollout, context, flagKey, salt); } @Test public void canBucketByIntAttributeSameAsString() { - LDUser user = new LDUser.Builder("key") - .custom("stringattr", "33333") - .custom("intattr", 33333) + LDContext context = LDContext.builder("key") + .set("stringattr", "33333") + .set("intattr", 33333) .build(); - float resultForString = EvaluatorBucketing.bucketUser(noSeed, user, "key", UserAttribute.forName("stringattr"), "salt"); - float resultForInt = EvaluatorBucketing.bucketUser(noSeed, user, "key", UserAttribute.forName("intattr"), "salt"); + float resultForString = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("stringattr"), "salt"); + float resultForInt = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("intattr"), "salt"); assertEquals(resultForString, resultForInt, Float.MIN_VALUE); } @Test public void cannotBucketByFloatAttribute() { - LDUser user = new LDUser.Builder("key") - .custom("floatattr", 33.5f) + LDContext context = LDContext.builder("key") + .set("floatattr", 33.5f) .build(); - float result = EvaluatorBucketing.bucketUser(noSeed, user, "key", UserAttribute.forName("floatattr"), "salt"); + float result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("floatattr"), "salt"); assertEquals(0f, result, Float.MIN_VALUE); } @Test public void cannotBucketByBooleanAttribute() { - LDUser user = new LDUser.Builder("key") - .custom("boolattr", true) + LDContext context = LDContext.builder("key") + .set("boolattr", true) .build(); - float result = EvaluatorBucketing.bucketUser(noSeed, user, "key", UserAttribute.forName("boolattr"), "salt"); + float result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("boolattr"), "salt"); assertEquals(0f, result, Float.MIN_VALUE); } - @Test - public void userSecondaryKeyAffectsBucketValue() { - LDUser user1 = new LDUser.Builder("key").build(); - LDUser user2 = new LDUser.Builder("key").secondary("other").build(); - float result1 = EvaluatorBucketing.bucketUser(noSeed, user1, "flagkey", UserAttribute.KEY, "salt"); - float result2 = EvaluatorBucketing.bucketUser(noSeed, user2, "flagkey", UserAttribute.KEY, "salt"); - assertNotEquals(result1, result2); - } - private static void assertVariationIndexFromRollout( int expectedVariation, Rollout rollout, - LDUser user, + LDContext context, String flagKey, String salt ) { @@ -155,7 +145,7 @@ private static void assertVariationIndexFromRollout( .fallthrough(rollout) .salt(salt) .build(); - EvalResult result1 = BASE_EVALUATOR.evaluate(flag1, user, expectNoPrerequisiteEvals()); + EvalResult result1 = BASE_EVALUATOR.evaluate(flag1, context, expectNoPrerequisiteEvals()); assertThat(result1.getReason(), equalTo(EvaluationReason.fallthrough())); assertThat(result1.getVariationIndex(), equalTo(expectedVariation)); @@ -165,11 +155,11 @@ private static void assertVariationIndexFromRollout( .generatedVariations(3) .rules(ModelBuilders.ruleBuilder() .rollout(rollout) - .clauses(ModelBuilders.clause(UserAttribute.KEY, Operator.in, LDValue.of(user.getKey()))) + .clauses(clauseMatchingContext(context)) .build()) .salt(salt) .build(); - EvalResult result2 = BASE_EVALUATOR.evaluate(flag2, user, expectNoPrerequisiteEvals()); + EvalResult result2 = BASE_EVALUATOR.evaluate(flag2, context, expectNoPrerequisiteEvals()); assertThat(result2.getReason().getKind(), equalTo(EvaluationReason.Kind.RULE_MATCH)); assertThat(result2.getVariationIndex(), equalTo(expectedVariation)); } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java index cbd6f37f0..4c1317f56 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorClauseTest.java @@ -1,10 +1,16 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Operator; +import com.launchdarkly.sdk.server.DataModel.Rule; +import com.launchdarkly.sdk.server.DataModel.Segment; import org.junit.Test; @@ -13,8 +19,10 @@ import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment; import static com.launchdarkly.sdk.server.ModelBuilders.fallthroughVariation; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; +import static com.launchdarkly.sdk.server.ModelBuilders.negateClause; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; import static com.launchdarkly.sdk.server.TestUtil.TEST_GSON_INSTANCE; @@ -24,137 +32,137 @@ @SuppressWarnings("javadoc") public class EvaluatorClauseTest extends EvaluatorTestBase { - private static void assertMatch(Evaluator eval, DataModel.FeatureFlag flag, LDUser user, boolean expectMatch) { - assertEquals(LDValue.of(expectMatch), eval.evaluate(flag, user, expectNoPrerequisiteEvals()).getValue()); + private static void assertMatch(Evaluator eval, FeatureFlag flag, LDContext context, boolean expectMatch) { + assertEquals(LDValue.of(expectMatch), eval.evaluate(flag, context, expectNoPrerequisiteEvals()).getValue()); } - private static DataModel.Segment makeSegmentThatMatchesUser(String segmentKey, String userKey) { + private static Segment makeSegmentThatMatchesUser(String segmentKey, String userKey) { return segmentBuilder(segmentKey).included(userKey).build(); } @Test public void clauseCanMatchBuiltInAttribute() throws Exception { - DataModel.Clause clause = clause(UserAttribute.NAME, DataModel.Operator.in, LDValue.of("Bob")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").name("Bob").build(); + Clause clause = clause("name", Operator.in, LDValue.of("Bob")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").name("Bob").build(); - assertMatch(BASE_EVALUATOR, f, user, true); + assertMatch(BASE_EVALUATOR, f, context, true); } @Test public void clauseCanMatchCustomAttribute() throws Exception { - DataModel.Clause clause = clause(UserAttribute.forName("legs"), DataModel.Operator.in, LDValue.of(4)); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").custom("legs", 4).build(); + Clause clause = clause("legs", Operator.in, LDValue.of(4)); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").set("legs", 4).build(); - assertMatch(BASE_EVALUATOR, f, user, true); + assertMatch(BASE_EVALUATOR, f, context, true); } @Test public void clauseReturnsFalseForMissingAttribute() throws Exception { - DataModel.Clause clause = clause(UserAttribute.forName("legs"), DataModel.Operator.in, LDValue.of(4)); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").name("Bob").build(); + Clause clause = clause("legs", Operator.in, LDValue.of(4)); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").name("Bob").build(); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test - public void clauseMatchesUserValueToAnyOfMultipleValues() throws Exception { - DataModel.Clause clause = clause(UserAttribute.NAME, DataModel.Operator.in, LDValue.of("Bob"), LDValue.of("Carol")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").name("Carol").build(); + public void clauseMatchesContextValueToAnyOfMultipleValues() throws Exception { + Clause clause = clause("name", Operator.in, LDValue.of("Bob"), LDValue.of("Carol")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").name("Carol").build(); - assertMatch(BASE_EVALUATOR, f, user, true); + assertMatch(BASE_EVALUATOR, f, context, true); } @Test - public void clauseMatchesUserValueToAnyOfMultipleValuesWithNonEqualityOperator() throws Exception { + public void clauseMatchesContextValueToAnyOfMultipleValuesWithNonEqualityOperator() throws Exception { // We check this separately because of the special preprocessing logic for equality matches. - DataModel.Clause clause = clause(UserAttribute.NAME, DataModel.Operator.contains, LDValue.of("Bob"), LDValue.of("Carol")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").name("Caroline").build(); + Clause clause = clause("name", Operator.contains, LDValue.of("Bob"), LDValue.of("Carol")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").name("Caroline").build(); - assertMatch(BASE_EVALUATOR, f, user, true); + assertMatch(BASE_EVALUATOR, f, context, true); } @Test - public void clauseMatchesArrayOfUserValuesToClauseValue() throws Exception { - DataModel.Clause clause = clause(UserAttribute.forName("alias"), DataModel.Operator.in, LDValue.of("Maurice")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").custom("alias", + public void clauseMatchesArrayOfContextValuesToClauseValue() throws Exception { + Clause clause = clause("alias", Operator.in, LDValue.of("Maurice")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").set("alias", LDValue.buildArray().add("Space Cowboy").add("Maurice").build()).build(); - assertMatch(BASE_EVALUATOR, f, user, true); + assertMatch(BASE_EVALUATOR, f, context, true); } @Test - public void clauseFindsNoMatchInArrayOfUserValues() throws Exception { - DataModel.Clause clause = clause(UserAttribute.forName("alias"), DataModel.Operator.in, LDValue.of("Ma")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").custom("alias", + public void clauseFindsNoMatchInArrayOfContextValues() throws Exception { + Clause clause = clause("alias", Operator.in, LDValue.of("Ma")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").set("alias", LDValue.buildArray().add("Mary").add("May").build()).build(); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test - public void userValueMustNotBeAnArrayOfArrays() throws Exception { + public void matchFailsIfContextValueIsAnArrayOfArrays() throws Exception { LDValue arrayValue = LDValue.buildArray().add("thing").build(); LDValue arrayOfArrays = LDValue.buildArray().add(arrayValue).build(); - DataModel.Clause clause = clause(UserAttribute.forName("data"), DataModel.Operator.in, arrayOfArrays); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").custom("data", arrayOfArrays).build(); + Clause clause = clause("data", Operator.in, arrayOfArrays); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").set("data", arrayOfArrays).build(); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test - public void userValueMustNotBeAnObject() throws Exception { + public void matchFailsIfContextValueIsAnObject() throws Exception { LDValue objectValue = LDValue.buildObject().put("thing", LDValue.of(true)).build(); - DataModel.Clause clause = clause(UserAttribute.forName("data"), DataModel.Operator.in, objectValue); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").custom("data", objectValue).build(); + Clause clause = clause("data", Operator.in, objectValue); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").set("data", objectValue).build(); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test - public void userValueMustNotBeAnArrayOfObjects() throws Exception { + public void matchFailsIfContextValueIsAnArrayOfObjects() throws Exception { LDValue objectValue = LDValue.buildObject().put("thing", LDValue.of(true)).build(); LDValue arrayOfObjects = LDValue.buildArray().add(objectValue).build(); - DataModel.Clause clause = clause(UserAttribute.forName("data"), DataModel.Operator.in, arrayOfObjects); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").custom("data", arrayOfObjects).build(); + Clause clause = clause("data", Operator.in, arrayOfObjects); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").set("data", arrayOfObjects).build(); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test public void clauseReturnsFalseForNullOperator() throws Exception { - DataModel.Clause clause = clause(UserAttribute.KEY, null, LDValue.of("key")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser("key"); + Clause clause = clause("key", null, LDValue.of("key")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.create("key"); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test public void clauseCanBeNegatedToReturnFalse() throws Exception { - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, true, LDValue.of("key")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").name("Bob").build(); + Clause clause = negateClause(clause("key", Operator.in, LDValue.of("key"))); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").name("Bob").build(); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test public void clauseCanBeNegatedToReturnTrue() throws Exception { - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, true, LDValue.of("other")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("key").name("Bob").build(); + Clause clause = negateClause(clause("key", Operator.in, LDValue.of("other"))); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key").name("Bob").build(); - assertMatch(BASE_EVALUATOR, f, user, true); + assertMatch(BASE_EVALUATOR, f, context, true); } @Test @@ -163,7 +171,7 @@ public void clauseWithUnsupportedOperatorStringIsUnmarshalledWithNullOperator() // so we fail as gracefully as possible if a new operator type has been added in the application // and the SDK hasn't been upgraded yet. String badClauseJson = "{\"attribute\":\"name\",\"operator\":\"doesSomethingUnsupported\",\"values\":[\"x\"]}"; - DataModel.Clause clause = TEST_GSON_INSTANCE.fromJson(badClauseJson, DataModel.Clause.class); + Clause clause = TEST_GSON_INSTANCE.fromJson(badClauseJson, DataModel.Clause.class); assertNotNull(clause); String json = TEST_GSON_INSTANCE.toJson(clause); @@ -173,65 +181,124 @@ public void clauseWithUnsupportedOperatorStringIsUnmarshalledWithNullOperator() @Test public void clauseWithNullOperatorDoesNotMatch() throws Exception { - DataModel.Clause badClause = clause(UserAttribute.NAME, null, LDValue.of("Bob")); - DataModel.FeatureFlag f = booleanFlagWithClauses("flag", badClause); - LDUser user = new LDUser.Builder("key").name("Bob").build(); + Clause badClause = clause("name", null, LDValue.of("Bob")); + FeatureFlag f = booleanFlagWithClauses("flag", badClause); + LDContext context = LDContext.builder("key").name("Bob").build(); - assertMatch(BASE_EVALUATOR, f, user, false); + assertMatch(BASE_EVALUATOR, f, context, false); } @Test public void clauseWithNullOperatorDoesNotStopSubsequentRuleFromMatching() throws Exception { - DataModel.Clause badClause = clause(UserAttribute.NAME, null, LDValue.of("Bob")); - DataModel.Rule badRule = ruleBuilder().id("rule1").clauses(badClause).variation(1).build(); - DataModel.Clause goodClause = clause(UserAttribute.NAME, DataModel.Operator.in, LDValue.of("Bob")); - DataModel.Rule goodRule = ruleBuilder().id("rule2").clauses(goodClause).variation(1).build(); - DataModel.FeatureFlag f = flagBuilder("feature") + Clause badClause = clause("name", null, LDValue.of("Bob")); + Rule badRule = ruleBuilder().id("rule1").clauses(badClause).variation(1).build(); + Clause goodClause = clause("name", Operator.in, LDValue.of("Bob")); + Rule goodRule = ruleBuilder().id("rule2").clauses(goodClause).variation(1).build(); + FeatureFlag f = flagBuilder("feature") .on(true) .rules(badRule, goodRule) .fallthrough(fallthroughVariation(0)) .offVariation(0) .variations(LDValue.of(false), LDValue.of(true)) .build(); - LDUser user = new LDUser.Builder("key").name("Bob").build(); + LDContext context = LDContext.builder("key").name("Bob").build(); - EvaluationDetail details = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()).getAnyType(); + EvaluationDetail details = BASE_EVALUATOR.evaluate(f, context, expectNoPrerequisiteEvals()).getAnyType(); assertEquals(fromValue(LDValue.of(true), 1, EvaluationReason.ruleMatch(1, "rule2")), details); } + + @Test + public void clauseCanGetValueWithAttributeReference() throws Exception { + Clause clause = clause(null, AttributeRef.fromPath("/address/city"), Operator.in, LDValue.of("Oakland")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.builder("key") + .set("address", LDValue.parse("{\"city\":\"Oakland\",\"state\":\"CA\"}")) + .build(); + + assertMatch(BASE_EVALUATOR, f, context, true); + } + + @Test + public void clauseMatchUsesContextKind() throws Exception { + Clause clause = clause(ContextKind.of("company"), "name", Operator.in, LDValue.of("Catco")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context1 = LDContext.builder("cc").kind("company").name("Catco").build(); + LDContext context2 = LDContext.builder("l").name("Lucy").build(); + LDContext context3 = LDContext.createMulti(context1, context2); + + assertMatch(BASE_EVALUATOR, f, context1, true); + assertMatch(BASE_EVALUATOR, f, context2, false); + assertMatch(BASE_EVALUATOR, f, context3, true); + } + + @Test + public void clauseMatchByKindAttribute() throws Exception { + Clause clause = clause(null, "kind", Operator.startsWith, LDValue.of("a")); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context1 = LDContext.create("key"); + LDContext context2 = LDContext.create(ContextKind.of("ab"), "key"); + LDContext context3 = LDContext.createMulti( + LDContext.create(ContextKind.of("cd"), "key"), + LDContext.create(ContextKind.of("ab"), "key")); + + assertMatch(BASE_EVALUATOR, f, context1, false); + assertMatch(BASE_EVALUATOR, f, context2, true); + assertMatch(BASE_EVALUATOR, f, context3, true); + } + + @Test + public void clauseReturnsMalformedFlagErrorForAttributeNotSpecified() { + Clause clause = clause(null, (AttributeRef)null, Operator.in, LDValue.of(4)); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.create("key"); + + EvalResult result = BASE_EVALUATOR.evaluate(f, context, expectNoPrerequisiteEvals()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); + } + + @Test + public void clauseReturnsMalformedFlagErrorForMalformedAttributeReference() { + Clause clause = clause(null, AttributeRef.fromPath("///"), Operator.in, LDValue.of(4)); + FeatureFlag f = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.create("key"); + + EvalResult result = BASE_EVALUATOR.evaluate(f, context, expectNoPrerequisiteEvals()); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); + } @Test public void testSegmentMatchClauseRetrievesSegmentFromStore() throws Exception { String segmentKey = "segkey"; - DataModel.Clause clause = clause(null, DataModel.Operator.segmentMatch, LDValue.of(segmentKey)); - DataModel.FeatureFlag flag = booleanFlagWithClauses("flag", clause); - DataModel.Segment segment = makeSegmentThatMatchesUser(segmentKey, "foo"); - LDUser user = new LDUser.Builder("foo").build(); + Clause clause = clauseMatchingSegment(segmentKey); + FeatureFlag flag = booleanFlagWithClauses("flag", clause); + Segment segment = makeSegmentThatMatchesUser(segmentKey, "foo"); + LDContext context = LDContext.create("foo"); Evaluator e = evaluatorBuilder().withStoredSegments(segment).build(); - assertMatch(e, flag, user, true); + assertMatch(e, flag, context, true); } @Test public void testSegmentMatchClauseFallsThroughIfSegmentNotFound() throws Exception { String segmentKey = "segkey"; - DataModel.Clause clause = clause(null, DataModel.Operator.segmentMatch, LDValue.of(segmentKey)); - DataModel.FeatureFlag flag = booleanFlagWithClauses("flag", clause); - LDUser user = new LDUser.Builder("foo").build(); + Clause clause = clauseMatchingSegment(segmentKey); + FeatureFlag flag = booleanFlagWithClauses("flag", clause); + LDContext context = LDContext.create("foo"); Evaluator e = evaluatorBuilder().withNonexistentSegment(segmentKey).build(); - assertMatch(e, flag, user, false); + assertMatch(e, flag, context, false); } @Test public void testSegmentMatchClauseIgnoresNonStringValues() throws Exception { String segmentKey = "segkey"; - DataModel.Clause clause = clause(null, DataModel.Operator.segmentMatch, + Clause clause = clause(null, (AttributeRef)null, Operator.segmentMatch, LDValue.of(123), LDValue.of(segmentKey)); - DataModel.FeatureFlag flag = booleanFlagWithClauses("flag", clause); - DataModel.Segment segment = makeSegmentThatMatchesUser(segmentKey, "foo"); - LDUser user = new LDUser.Builder("foo").build(); + FeatureFlag flag = booleanFlagWithClauses("flag", clause); + Segment segment = makeSegmentThatMatchesUser(segmentKey, "foo"); + LDContext context = LDContext.create("foo"); Evaluator e = evaluatorBuilder().withStoredSegments(segment).build(); - assertMatch(e, flag, user, true); + assertMatch(e, flag, context, true); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java index 1445cce23..a37f83444 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorOperatorsParameterizedTest.java @@ -1,8 +1,8 @@ package com.launchdarkly.sdk.server; import com.google.common.collect.ImmutableList; +import com.launchdarkly.sdk.AttributeRef; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.Operator; @@ -14,6 +14,7 @@ import java.util.Arrays; import java.util.List; +import static com.launchdarkly.sdk.server.EvaluatorHelpers.matchClauseWithoutSegments; import static org.junit.Assert.assertEquals; @SuppressWarnings("javadoc") @@ -21,7 +22,7 @@ public class EvaluatorOperatorsParameterizedTest { private static final LDValue invalidVer = LDValue.of("xbad%ver"); - private static final UserAttribute userAttr = UserAttribute.forName("attr"); + private static final AttributeRef userAttr = AttributeRef.fromLiteral("attr"); private final Operator op; private final LDValue userValue; @@ -176,11 +177,11 @@ public void parameterizedTestComparison() { } values.add(clauseValue); - Clause clause1 = new Clause(userAttr, op, values, false); - assertEquals("without preprocessing", shouldBe, Evaluator.clauseMatchAny(clause1, userValue)); + Clause clause1 = new Clause(null, userAttr, op, values, false); + assertEquals("without preprocessing", shouldBe, matchClauseWithoutSegments(clause1, userValue)); - Clause clause2 = new Clause(userAttr, op, values, false); + Clause clause2 = new Clause(null, userAttr, op, values, false); DataModelPreprocessing.preprocessClause(clause2); - assertEquals("without preprocessing", shouldBe, Evaluator.clauseMatchAny(clause2, userValue)); + assertEquals("without preprocessing", shouldBe, matchClauseWithoutSegments(clause2, userValue)); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorPrerequisiteTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorPrerequisiteTest.java new file mode 100644 index 000000000..38a716f50 --- /dev/null +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorPrerequisiteTest.java @@ -0,0 +1,226 @@ +package com.launchdarkly.sdk.server; + +import com.google.common.collect.Iterables; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.EvaluationReason.ErrorKind; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; +import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqEval; +import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqRecorder; + +import org.junit.Test; + +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_USER; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.FALLTHROUGH_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.FALLTHROUGH_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.GREEN_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.GREEN_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.OFF_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.OFF_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.RED_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.RED_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.buildRedGreenFlag; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.buildThreeWayFlag; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.evaluatorBuilder; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; +import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; +import static com.launchdarkly.sdk.server.ModelBuilders.prerequisite; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +@SuppressWarnings("javadoc") +public class EvaluatorPrerequisiteTest { + @Test + public void flagReturnsOffVariationIfPrerequisiteIsNotFound() throws Exception { + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("feature1", 1)) + .build(); + Evaluator e = evaluatorBuilder().withNonexistentFlag("feature1").build(); + EvalResult result = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); + + EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); + } + + @Test + public void flagReturnsOffVariationAndEventIfPrerequisiteIsOff() throws Exception { + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("feature1", GREEN_VARIATION)) + .build(); + FeatureFlag f1 = buildRedGreenFlag("feature1") + .on(false) + .offVariation(GREEN_VARIATION) + // note that even though it returns the desired variation, it is still off and therefore not a match + .build(); + Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + + EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); + + assertEquals(1, Iterables.size(recordPrereqs.evals)); + PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(f1, eval.flag); + assertEquals(f0, eval.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval.result.getValue()); + } + + @Test + public void flagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() throws Exception { + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("feature1", GREEN_VARIATION)) + .build(); + FeatureFlag f1 = buildRedGreenFlag("feature1") + .on(true) + .fallthroughVariation(RED_VARIATION) + .build(); + Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + + EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); + assertEquals(EvalResult.of(OFF_VALUE, OFF_VARIATION, expectedReason), result); + + assertEquals(1, Iterables.size(recordPrereqs.evals)); + PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(f1, eval.flag); + assertEquals(f0, eval.prereqOfFlag); + assertEquals(RED_VARIATION, eval.result.getVariationIndex()); + assertEquals(RED_VALUE, eval.result.getValue()); + } + + @Test + public void prerequisiteFailedResultInstanceIsReusedForSamePrerequisite() throws Exception { + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("feature1", GREEN_VARIATION)) + .build(); + Evaluator e = evaluatorBuilder().withNonexistentFlag("feature1").build(); + EvalResult result0 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); + EvalResult result1 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); + + EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); + assertEquals(expectedReason, result0.getReason()); + assertSame(result0, result1); + } + + @Test + public void prerequisiteFailedReasonInstanceCanBeCreatedFromScratch() throws Exception { + // Normally we will always do the preprocessing step that creates the reason instances ahead of time, + // but if somehow we didn't, it should create them as needed + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("feature1", GREEN_VARIATION)) + .disablePreprocessing(true) + .build(); + assertNull(f0.getPrerequisites().get(0).preprocessed); + + Evaluator e = evaluatorBuilder().withNonexistentFlag("feature1").build(); + EvalResult result0 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); + EvalResult result1 = e.evaluate(f0, BASE_USER, expectNoPrerequisiteEvals()); + + EvaluationReason expectedReason = EvaluationReason.prerequisiteFailed("feature1"); + assertEquals(expectedReason, result0.getReason()); + assertNotSame(result0.getReason(), result1.getReason()); // they were created individually + assertEquals(result0.getReason(), result1.getReason()); // but they're equal + } + + @Test + public void flagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAndThereAreNoRules() throws Exception { + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("feature1", GREEN_VARIATION)) + .build(); + FeatureFlag f1 = buildRedGreenFlag("feature1") + .on(true) + .fallthroughVariation(GREEN_VARIATION) + .version(2) + .build(); + Evaluator e = evaluatorBuilder().withStoredFlags(f1).build(); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + + assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); + + assertEquals(1, Iterables.size(recordPrereqs.evals)); + PrereqEval eval = recordPrereqs.evals.get(0); + assertEquals(f1, eval.flag); + assertEquals(f0, eval.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval.result.getValue()); + } + + @Test + public void multipleLevelsOfPrerequisitesProduceMultipleEvents() throws Exception { + FeatureFlag f0 = buildThreeWayFlag("feature") + .on(true) + .prerequisites(prerequisite("feature1", GREEN_VARIATION)) + .build(); + FeatureFlag f1 = buildRedGreenFlag("feature1") + .on(true) + .prerequisites(prerequisite("feature2", GREEN_VARIATION)) + .fallthroughVariation(GREEN_VARIATION) + .build(); + FeatureFlag f2 = buildRedGreenFlag("feature2") + .on(true) + .fallthroughVariation(GREEN_VARIATION) + .build(); + Evaluator e = evaluatorBuilder().withStoredFlags(f1, f2).build(); + PrereqRecorder recordPrereqs = new PrereqRecorder(); + EvalResult result = e.evaluate(f0, BASE_USER, recordPrereqs); + + assertEquals(EvalResult.of(FALLTHROUGH_VALUE, FALLTHROUGH_VARIATION, EvaluationReason.fallthrough()), result); + + assertEquals(2, Iterables.size(recordPrereqs.evals)); + + PrereqEval eval0 = recordPrereqs.evals.get(0); + assertEquals(f2, eval0.flag); + assertEquals(f1, eval0.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval0.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval0.result.getValue()); + + PrereqEval eval1 = recordPrereqs.evals.get(1); + assertEquals(f1, eval1.flag); + assertEquals(f0, eval1.prereqOfFlag); + assertEquals(GREEN_VARIATION, eval1.result.getVariationIndex()); + assertEquals(GREEN_VALUE, eval1.result.getValue()); + } + + @Test + public void prerequisiteCycleDetection() { + for (int depth = 1; depth <= 4; depth++) { + String[] flagKeys = new String[depth]; + for (int i = 0; i < depth; i++) { + flagKeys[i] = "flagkey" + i; + } + FeatureFlag[] flags = new FeatureFlag[depth]; + for (int i = 0; i < depth; i++) { + flags[i] = flagBuilder(flagKeys[i]) + .on(true) + .variations(false, true) + .offVariation(0) + .prerequisites( + new Prerequisite(flagKeys[(i + 1) % depth], 0) + ) + .build(); + } + + Evaluator e = evaluatorBuilder().withStoredFlags(flags).build(); + + LDContext context = LDContext.create("foo"); + EvalResult result = e.evaluate(flags[0], context, expectNoPrerequisiteEvals()); + assertEquals(EvalResult.error(ErrorKind.MALFORMED_FLAG), result); + // Note, we specified expectNoPrerequisiteEvals() above because we do not expect the evaluator + // to *finish* evaluating any of these prerequisites (it can't, because of the cycle), and so + // it won't get as far as emitting any prereq evaluation results. + } + } +} diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java index 10825353d..9712174ae 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorRuleTest.java @@ -1,17 +1,28 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Rollout; +import com.launchdarkly.sdk.server.DataModel.RolloutKind; +import com.launchdarkly.sdk.server.DataModel.Rule; +import com.launchdarkly.sdk.server.DataModel.WeightedVariation; import com.launchdarkly.sdk.server.ModelBuilders.FlagBuilder; import com.launchdarkly.sdk.server.ModelBuilders.RuleBuilder; import org.junit.Test; +import java.util.Arrays; + +import static com.launchdarkly.sdk.server.EvaluatorBucketing.computeBucketValue; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingContext; import static com.launchdarkly.sdk.server.ModelBuilders.emptyRollout; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; @@ -22,6 +33,8 @@ @SuppressWarnings("javadoc") public class EvaluatorRuleTest { + private static final LDContext BASE_USER = LDContext.create("userkey"); + private static final LDContext OTHER_USER = LDContext.create("otherkey"); private static final int FALLTHROUGH_VARIATION = 0; private static final int MATCH_VARIATION = 1; @@ -40,18 +53,16 @@ private RuleBuilder buildTestRule(String id, DataModel.Clause... clauses) { @Test public void ruleMatchResultInstanceIsReusedForSameRule() { - DataModel.Clause clause0 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("wrongkey")); - DataModel.Clause clause1 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); + Clause clause0 = clauseMatchingContext(OTHER_USER); + Clause clause1 = clauseMatchingContext(BASE_USER); DataModel.Rule rule0 = buildTestRule("ruleid0", clause0).build(); DataModel.Rule rule1 = buildTestRule("ruleid1", clause1).build(); DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule0, rule1).build(); - LDUser user = new LDUser.Builder("userkey").build(); - LDUser otherUser = new LDUser.Builder("wrongkey").build(); - EvalResult sameResult0 = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - EvalResult sameResult1 = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - EvalResult otherResult = BASE_EVALUATOR.evaluate(f, otherUser, expectNoPrerequisiteEvals()); + EvalResult sameResult0 = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); + EvalResult sameResult1 = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); + EvalResult otherResult = BASE_EVALUATOR.evaluate(f, OTHER_USER, expectNoPrerequisiteEvals()); assertEquals(EvaluationReason.ruleMatch(1, "ruleid1"), sameResult0.getReason()); assertSame(sameResult0, sameResult1); @@ -63,9 +74,9 @@ public void ruleMatchResultInstanceIsReusedForSameRule() { public void ruleMatchResultInstanceCanBeCreatedFromScratch() { // Normally we will always do the preprocessing step that creates the result instances ahead of time, // but if somehow we didn't, it should create them as needed - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); + DataModel.Clause clause = clause("key", DataModel.Operator.in, LDValue.of("userkey")); DataModel.Rule rule = buildTestRule("ruleid", clause).build(); - LDUser user = new LDUser("userkey"); + LDContext user = LDContext.create("userkey"); DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule) .disablePreprocessing(true) @@ -82,45 +93,106 @@ public void ruleMatchResultInstanceCanBeCreatedFromScratch() { @Test public void ruleWithTooHighVariationReturnsMalformedFlagError() { - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); - DataModel.Rule rule = buildTestRule("ruleid", clause).variation(999).build(); - DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); - LDUser user = new LDUser.Builder("userkey").build(); - EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + Clause clause = clauseMatchingContext(BASE_USER); + Rule rule = buildTestRule("ruleid", clause).variation(999).build(); + FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test public void ruleWithNegativeVariationReturnsMalformedFlagError() { - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); - DataModel.Rule rule = buildTestRule("ruleid", clause).variation(-1).build(); - DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); - LDUser user = new LDUser.Builder("userkey").build(); - EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + Clause clause = clauseMatchingContext(BASE_USER); + Rule rule = buildTestRule("ruleid", clause).variation(-1).build(); + FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test public void ruleWithNoVariationOrRolloutReturnsMalformedFlagError() { - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); - DataModel.Rule rule = buildTestRule("ruleid", clause).variation(null).build(); - DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); - LDUser user = new LDUser.Builder("userkey").build(); - EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + Clause clause = clauseMatchingContext(BASE_USER); + Rule rule = buildTestRule("ruleid", clause).variation(null).build(); + FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } @Test public void ruleWithRolloutWithEmptyVariationsListReturnsMalformedFlagError() { - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); - DataModel.Rule rule = buildTestRule("ruleid", clause).variation(null).rollout(emptyRollout()).build(); - DataModel.FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); - LDUser user = new LDUser.Builder("userkey").build(); - EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); + Clause clause = clauseMatchingContext(BASE_USER); + Rule rule = buildTestRule("ruleid", clause).variation(null).rollout(emptyRollout()).build(); + FeatureFlag f = buildBooleanFlagWithRules("feature", rule).build(); + EvalResult result = BASE_EVALUATOR.evaluate(f, BASE_USER, expectNoPrerequisiteEvals()); assertEquals(EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG), result); } + + @Test + public void rolloutUsesCorrectBucketValue() { + LDContext c = LDContext.create("foo"); + testRolloutBucketing("foo", c, null, null, RolloutKind.rollout); + } + + @Test + public void rolloutUsesContextKind() { + LDContext c1 = LDContext.create(ContextKind.of("kind1"), "foo"); + LDContext c2 = LDContext.create(ContextKind.of("kind2"), "bar"); + LDContext multi = LDContext.createMulti(c1, c2); + testRolloutBucketing("foo", multi, ContextKind.of("kind1"), null, RolloutKind.rollout); + } + + @Test + public void rolloutUsesBucketBy() { + LDContext c = LDContext.builder("xxx").set("attr1", LDValue.parse("{\"prop1\":\"foo\"}")).build(); + testRolloutBucketing("foo", c, null, AttributeRef.fromPath("/attr1/prop1"), RolloutKind.rollout); + } + + @Test + public void experimentIgnoresBucketBy() { + LDContext c = LDContext.builder("xxx").set("attr1", LDValue.parse("{\"prop1\":\"foo\"}")).build(); + testRolloutBucketing("xxx", c, null, AttributeRef.fromPath("/attr1/prop1"), RolloutKind.experiment); + } + + private static void testRolloutBucketing( + String bucketByValue, + LDContext context, + ContextKind contextKind, + AttributeRef bucketBy, + RolloutKind rolloutKind + ) { + String flagKey = "feature"; + String salt = "abc"; + float expectedBucketValue = computeBucketValue(false, null, LDContext.create(bucketByValue), null, + flagKey, null, salt); + int bucketValueAsInt = (int)(expectedBucketValue * 100000); + Clause clause = clauseMatchingContext(context); + + // To roughly verify that the right bucket value is being used, we'll construct a rollout + // where the target bucket is in a very small range around that value. + Rollout rollout = new Rollout( + contextKind, + Arrays.asList( + new WeightedVariation(0, bucketValueAsInt - 1, false), + new WeightedVariation(1, 2, false), + new WeightedVariation(2, 100000 - (bucketValueAsInt + 1), false) + ), + bucketBy, + rolloutKind, + null); + FeatureFlag flag = flagBuilder(flagKey) + .on(true) + .variations(LDValue.of("no"), LDValue.of("yes"), LDValue.of("no")) + .rules(ruleBuilder().id("rule").clauses(clause).rollout(rollout).build()) + .salt(salt) + .build(); + + EvalResult result = BASE_EVALUATOR.evaluate(flag, context, expectNoPrerequisiteEvals()); + assertEquals(LDValue.of("yes"), result.getValue()); + assertEquals(1, result.getVariationIndex()); + assertEquals(EvaluationReason.Kind.RULE_MATCH, result.getReason().getKind()); + } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java index e7bf68631..6e416593d 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorSegmentMatchTest.java @@ -1,119 +1,252 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.EvaluationReason.ErrorKind; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Segment; +import com.launchdarkly.sdk.server.DataModel.SegmentRule; +import com.launchdarkly.sdk.server.ModelBuilders.SegmentBuilder; import org.junit.Test; +import static com.launchdarkly.sdk.server.EvaluatorBucketing.computeBucketValue; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingContext; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.segmentRuleBuilder; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @SuppressWarnings("javadoc") public class EvaluatorSegmentMatchTest extends EvaluatorTestBase { - - private int maxWeight = 100000; + private static final String SEGMENT_KEY = "segmentkey"; + private static final String ARBITRARY_SALT = "abcdef"; + private static final int maxWeight = 100000; @Test public void explicitIncludeUser() { - DataModel.Segment s = segmentBuilder("test") - .included("foo") - .salt("abcdef") - .version(1) + LDContext c = LDContext.create("foo"); + Segment s = baseSegmentBuilder() + .included(c.getKey()) .build(); - LDUser u = new LDUser.Builder("foo").build(); - assertTrue(segmentMatchesUser(s, u)); + assertTrue(segmentMatchesContext(s, c)); } @Test public void explicitExcludeUser() { - DataModel.Segment s = segmentBuilder("test") - .excluded("foo") - .salt("abcdef") - .version(1) + LDContext c = LDContext.create("foo"); + Segment s = baseSegmentBuilder() + .excluded(c.getKey()) + .rules(segmentRuleBuilder().clauses(clauseMatchingContext(c)).build()) .build(); - LDUser u = new LDUser.Builder("foo").build(); - assertFalse(segmentMatchesUser(s, u)); + assertFalse(segmentMatchesContext(s, c)); } @Test public void explicitIncludeHasPrecedence() { - DataModel.Segment s = segmentBuilder("test") - .included("foo") - .excluded("foo") - .salt("abcdef") - .version(1) + LDContext c = LDContext.create("foo"); + Segment s = baseSegmentBuilder() + .included(c.getKey()) + .excluded(c.getKey()) + .build(); + + assertTrue(segmentMatchesContext(s, c)); + } + + @Test + public void includedKeyForContextKind() { + ContextKind kind1 = ContextKind.of("kind1"); + String key = "foo"; + LDContext c1 = LDContext.create(key); + LDContext c2 = LDContext.create(kind1, key); + LDContext c3 = LDContext.createMulti(c1, c2); + + Segment s = baseSegmentBuilder() + .includedContexts(kind1, key) + .build(); + + assertFalse(segmentMatchesContext(s, c1)); + assertTrue(segmentMatchesContext(s, c2)); + assertTrue(segmentMatchesContext(s, c3)); + } + + @Test + public void excludedKeyForContextKind() { + ContextKind kind1 = ContextKind.of("kind1"); + String key = "foo"; + LDContext c1 = LDContext.create(key); + LDContext c2 = LDContext.create(kind1, key); + LDContext c3 = LDContext.createMulti(c1, c2); + + Segment s = baseSegmentBuilder() + .excludedContexts(kind1, key) + .rules( + segmentRuleBuilder().clauses(clauseMatchingContext(c1)).build(), + segmentRuleBuilder().clauses(clauseMatchingContext(c2)).build(), + segmentRuleBuilder().clauses(clauseMatchingContext(c3)).build() + ) .build(); - LDUser u = new LDUser.Builder("foo").build(); - assertTrue(segmentMatchesUser(s, u)); + assertTrue(segmentMatchesContext(s, c1)); // rule matched, wasn't excluded + assertFalse(segmentMatchesContext(s, c2)); // rule matched but was excluded + assertFalse(segmentMatchesContext(s, c3)); // rule matched but was excluded } @Test public void matchingRuleWithFullRollout() { - DataModel.Clause clause = clause(UserAttribute.EMAIL, DataModel.Operator.in, LDValue.of("test@example.com")); - DataModel.SegmentRule rule = segmentRuleBuilder().clauses(clause).weight(maxWeight).build(); - DataModel.Segment s = segmentBuilder("test") - .salt("abcdef") + LDContext c = LDContext.create("foo"); + Clause clause = clauseMatchingContext(c); + SegmentRule rule = segmentRuleBuilder().clauses(clause).weight(maxWeight).build(); + Segment s = baseSegmentBuilder() .rules(rule) .build(); - LDUser u = new LDUser.Builder("foo").email("test@example.com").build(); - assertTrue(segmentMatchesUser(s, u)); + assertTrue(segmentMatchesContext(s, c)); } @Test public void matchingRuleWithZeroRollout() { - DataModel.Clause clause = clause(UserAttribute.EMAIL, DataModel.Operator.in, LDValue.of("test@example.com")); - DataModel.SegmentRule rule = segmentRuleBuilder().clauses(clause).weight(0).build(); - DataModel.Segment s = segmentBuilder("test") - .salt("abcdef") + LDContext c = LDContext.create("foo"); + Clause clause = clauseMatchingContext(c); + SegmentRule rule = segmentRuleBuilder().clauses(clause).weight(0).build(); + Segment s = baseSegmentBuilder() .rules(rule) .build(); - LDUser u = new LDUser.Builder("foo").email("test@example.com").build(); - assertFalse(segmentMatchesUser(s, u)); + assertFalse(segmentMatchesContext(s, c)); } - + @Test public void matchingRuleWithMultipleClauses() { - DataModel.Clause clause1 = clause(UserAttribute.EMAIL, DataModel.Operator.in, LDValue.of("test@example.com")); - DataModel.Clause clause2 = clause(UserAttribute.NAME, DataModel.Operator.in, LDValue.of("bob")); - DataModel.SegmentRule rule = segmentRuleBuilder().clauses(clause1, clause2).build(); - DataModel.Segment s = segmentBuilder("test") + Clause clause1 = clause("email", DataModel.Operator.in, LDValue.of("test@example.com")); + Clause clause2 = clause("name", DataModel.Operator.in, LDValue.of("bob")); + SegmentRule rule = segmentRuleBuilder().clauses(clause1, clause2).build(); + Segment s = segmentBuilder("test") .salt("abcdef") .rules(rule) .build(); - LDUser u = new LDUser.Builder("foo").email("test@example.com").name("bob").build(); + LDContext c = LDContext.builder("foo").set("email", "test@example.com").name("bob").build(); - assertTrue(segmentMatchesUser(s, u)); + assertTrue(segmentMatchesContext(s, c)); } @Test public void nonMatchingRuleWithMultipleClauses() { - DataModel.Clause clause1 = clause(UserAttribute.EMAIL, DataModel.Operator.in, LDValue.of("test@example.com")); - DataModel.Clause clause2 = clause(UserAttribute.NAME, DataModel.Operator.in, LDValue.of("bill")); - DataModel.SegmentRule rule = segmentRuleBuilder().clauses(clause1, clause2).build(); - DataModel.Segment s = segmentBuilder("test") + Clause clause1 = clause("email", DataModel.Operator.in, LDValue.of("test@example.com")); + Clause clause2 = clause("name", DataModel.Operator.in, LDValue.of("bill")); + SegmentRule rule = segmentRuleBuilder().clauses(clause1, clause2).build(); + Segment s = segmentBuilder("test") .salt("abcdef") .rules(rule) .build(); - LDUser u = new LDUser.Builder("foo").email("test@example.com").name("bob").build(); + LDContext c = LDContext.builder("foo").set("email", "test@example.com").name("bob").build(); + + assertFalse(segmentMatchesContext(s, c)); + } + + @Test + public void rolloutUsesCorrectBucketValue() { + LDContext c = LDContext.create("foo"); + testRolloutBucketing("foo", c, null, null); + } + + @Test + public void rolloutUsesContextKind() { + LDContext c1 = LDContext.create(ContextKind.of("kind1"), "foo"); + LDContext c2 = LDContext.create(ContextKind.of("kind2"), "bar"); + LDContext multi = LDContext.createMulti(c1, c2); + testRolloutBucketing("foo", multi, ContextKind.of("kind1"), null); + } + + @Test + public void rolloutUsesBucketBy() { + LDContext c = LDContext.builder("xxx").set("attr1", LDValue.parse("{\"prop1\":\"foo\"}")).build(); + testRolloutBucketing("foo", c, null, AttributeRef.fromPath("/attr1/prop1")); + } + + private void testRolloutBucketing(String bucketByValue, LDContext context, ContextKind contextKind, AttributeRef bucketBy) { + float expectedBucketValue = computeBucketValue(false, null, LDContext.create(bucketByValue), null, + SEGMENT_KEY, null, ARBITRARY_SALT); + int bucketValueAsInt = (int)(expectedBucketValue * 100000); + Clause clause = clauseMatchingContext(context); + + // When a segment rule has a weight, it matches only if the bucket value for the context (as an int + // from 0 to 100000) is *less than* that weight. So, to roughly verify that the right bucket value + // is being used, first we check that a rule with that value plus 1 is a match... + Segment s1 = baseSegmentBuilder() + .rules(segmentRuleBuilder().clauses(clause).weight(bucketValueAsInt + 1) + .rolloutContextKind(contextKind).bucketBy(bucketBy).build()) + .build(); + assertTrue(segmentMatchesContext(s1, context)); + + // ...and then, that a rule with that value minus 1 is not a match. + Segment s2 = baseSegmentBuilder() + .rules(segmentRuleBuilder().clauses(clause).weight(bucketValueAsInt - 1) + .rolloutContextKind(contextKind).bucketBy(bucketBy).build()) + .build(); + assertFalse(segmentMatchesContext(s2, context)); + } + + @Test + public void segmentReferencingSegment() { + LDContext context = LDContext.create("foo"); + Segment segment0 = segmentBuilder("segmentkey0") + .rules(segmentRuleBuilder().clauses(clauseMatchingSegment("segmentkey1")).build()) + .build(); + Segment segment1 = segmentBuilder("segmentkey1") + .included(context.getKey()) + .build(); + FeatureFlag flag = booleanFlagWithClauses("flag", clauseMatchingSegment(segment0)); - assertFalse(segmentMatchesUser(s, u)); + Evaluator e = evaluatorBuilder().withStoredSegments(segment0, segment1).build(); + EvalResult result = e.evaluate(flag, context, expectNoPrerequisiteEvals()); + assertTrue(result.getValue().booleanValue()); + } + + @Test + public void segmentCycleDetection() { + for (int depth = 1; depth <= 4; depth++) { + String[] segmentKeys = new String[depth]; + for (int i = 0; i < depth; i++) { + segmentKeys[i] = "segmentkey" + i; + } + Segment[] segments = new Segment[depth]; + for (int i = 0; i < depth; i++) { + segments[i] = segmentBuilder(segmentKeys[i]) + .rules( + segmentRuleBuilder().clauses( + clauseMatchingSegment(segmentKeys[(i + 1) % depth]) + ).build() + ) + .build(); + } + + FeatureFlag flag = booleanFlagWithClauses("flag", clauseMatchingSegment(segments[0])); + Evaluator e = evaluatorBuilder().withStoredSegments(segments).build(); + + LDContext context = LDContext.create("foo"); + EvalResult result = e.evaluate(flag, context, expectNoPrerequisiteEvals()); + assertEquals(EvalResult.error(ErrorKind.MALFORMED_FLAG), result); + } + } + + private static SegmentBuilder baseSegmentBuilder() { + return segmentBuilder(SEGMENT_KEY).version(1).salt(ARBITRARY_SALT); } - private boolean segmentMatchesUser(DataModel.Segment segment, LDUser user) { - DataModel.Clause clause = clause(null, DataModel.Operator.segmentMatch, LDValue.of(segment.getKey())); - DataModel.FeatureFlag flag = booleanFlagWithClauses("flag", clause); + private boolean segmentMatchesContext(Segment segment, LDContext context) { + FeatureFlag flag = booleanFlagWithClauses("flag", clauseMatchingSegment(segment)); Evaluator e = evaluatorBuilder().withStoredSegments(segment).build(); - return e.evaluate(flag, user, expectNoPrerequisiteEvals()).getValue().booleanValue(); + return e.evaluate(flag, context, expectNoPrerequisiteEvals()).getValue().booleanValue(); } } \ No newline at end of file diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTargetTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTargetTest.java new file mode 100644 index 000000000..8d3009668 --- /dev/null +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTargetTest.java @@ -0,0 +1,104 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.ModelBuilders.FlagBuilder; + +import org.junit.Test; + +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; +import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; +import static com.launchdarkly.sdk.server.ModelBuilders.target; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class EvaluatorTargetTest { + private static final int FALLTHROUGH_VAR = 0, MATCH_VAR_1 = 1, MATCH_VAR_2 = 2; + private static final LDValue[] VARIATIONS = new LDValue[] { + LDValue.of("fallthrough"), LDValue.of("match1"), LDValue.of("match2") + }; + private static final ContextKind CAT_KIND = ContextKind.of("cat"); + private static final ContextKind DOG_KIND = ContextKind.of("dog"); + + @Test + public void userTargetsOnly() throws Exception { + FeatureFlag f = baseFlagBuilder() + .targets( + target(MATCH_VAR_1, "c"), + target(MATCH_VAR_2, "b", "a") + ) + .build(); + + expectMatch(f, user("a"), MATCH_VAR_2); + expectMatch(f, user("b"), MATCH_VAR_2); + expectMatch(f, user("c"), MATCH_VAR_1); + expectFallthrough(f, user("z")); + + // in a multi-kind context, these targets match only the key for the user kind + expectMatch(f, LDContext.createMulti(dog("b"), user("a")), MATCH_VAR_2); + expectMatch(f, LDContext.createMulti(dog("a"), user("c")), MATCH_VAR_1); + expectFallthrough(f, LDContext.createMulti(dog("b"), user("z"))); + expectFallthrough(f, LDContext.createMulti(dog("a"), cat("b"))); + } + + @Test + public void userTargetsAndContextTargets() throws Exception { + FeatureFlag f = baseFlagBuilder() + .targets( + target(MATCH_VAR_1, "c"), + target(MATCH_VAR_2, "b", "a") + ) + .contextTargets( + target(DOG_KIND, MATCH_VAR_1, "a", "b"), + target(DOG_KIND, MATCH_VAR_2, "c"), + target(ContextKind.DEFAULT, MATCH_VAR_1), + target(ContextKind.DEFAULT, MATCH_VAR_2) + ) + .build(); + + expectMatch(f, user("a"), MATCH_VAR_2); + expectMatch(f, user("b"), MATCH_VAR_2); + expectMatch(f, user("c"), MATCH_VAR_1); + expectFallthrough(f, user("z")); + + expectMatch(f, LDContext.createMulti(dog("b"), user("a")), MATCH_VAR_1); // the "dog" target takes precedence due to ordering + expectMatch(f, LDContext.createMulti(dog("z"), user("a")), MATCH_VAR_2); // "dog" targets don't match, continue to "user" targets + expectFallthrough(f, LDContext.createMulti(dog("x"), user("z"))); // nothing matches + expectMatch(f, LDContext.createMulti(dog("a"), cat("b")), MATCH_VAR_1); + } + + private static FlagBuilder baseFlagBuilder() { + return flagBuilder("feature").on(true).variations(VARIATIONS) + .fallthroughVariation(FALLTHROUGH_VAR).offVariation(FALLTHROUGH_VAR); + } + + private static void expectMatch(FeatureFlag f, LDContext c, int v) { + EvalResult result = BASE_EVALUATOR.evaluate(f, c, expectNoPrerequisiteEvals()); + assertThat(result.getVariationIndex(), equalTo(v)); + assertThat(result.getValue(), equalTo(VARIATIONS[v])); + assertThat(result.getReason(), equalTo(EvaluationReason.targetMatch())); + } + + private static void expectFallthrough(FeatureFlag f, LDContext c) { + EvalResult result = BASE_EVALUATOR.evaluate(f, c, expectNoPrerequisiteEvals()); + assertThat(result.getVariationIndex(), equalTo(FALLTHROUGH_VAR)); + assertThat(result.getValue(), equalTo(VARIATIONS[FALLTHROUGH_VAR])); + assertThat(result.getReason(), equalTo(EvaluationReason.fallthrough())); + } + + private static LDContext user(String key) { + return LDContext.create(key); + } + + private static LDContext cat(String key) { + return LDContext.create(CAT_KIND, key); + } + + private static LDContext dog(String key) { + return LDContext.create(DOG_KIND, key); + } +} diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java index bee6e1f5f..2cd857c9a 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java @@ -1,31 +1,43 @@ package com.launchdarkly.sdk.server; import com.google.common.collect.Iterables; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.Rollout; import com.launchdarkly.sdk.server.DataModel.RolloutKind; import com.launchdarkly.sdk.server.DataModel.VariationOrRollout; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqEval; import com.launchdarkly.sdk.server.EvaluatorTestUtil.PrereqRecorder; -import com.launchdarkly.sdk.server.ModelBuilders.FlagBuilder; import org.junit.Test; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.FALLTHROUGH_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.FALLTHROUGH_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.GREEN_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.GREEN_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.MATCH_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.MATCH_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.OFF_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.OFF_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.RED_VALUE; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.RED_VARIATION; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.buildRedGreenFlag; +import static com.launchdarkly.sdk.server.EvaluatorTestUtil.buildThreeWayFlag; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingContext; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.prerequisite; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; -import static com.launchdarkly.sdk.server.ModelBuilders.target; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; @@ -33,69 +45,32 @@ @SuppressWarnings("javadoc") public class EvaluatorTest extends EvaluatorTestBase { - private static final LDUser BASE_USER = new LDUser.Builder("x").build(); - - // These constants and flag builders define two kinds of flag: one with three variations-- allowing us to - // distinguish between match, fallthrough, and off results-- and one with two. - private static final int OFF_VARIATION = 0; - private static final LDValue OFF_VALUE = LDValue.of("off"); - private static final int FALLTHROUGH_VARIATION = 1; - private static final LDValue FALLTHROUGH_VALUE = LDValue.of("fall"); - private static final int MATCH_VARIATION = 2; - private static final LDValue MATCH_VALUE = LDValue.of("match"); - private static final LDValue[] THREE_VARIATIONS = new LDValue[] { OFF_VALUE, FALLTHROUGH_VALUE, MATCH_VALUE }; - - private static final int RED_VARIATION = 0; - private static final LDValue RED_VALUE = LDValue.of("red"); - private static final int GREEN_VARIATION = 1; - private static final LDValue GREEN_VALUE = LDValue.of("green"); - private static final LDValue[] RED_GREEN_VARIATIONS = new LDValue[] { RED_VALUE, GREEN_VALUE }; - - private static FlagBuilder buildThreeWayFlag(String flagKey) { - return flagBuilder(flagKey) - .fallthroughVariation(FALLTHROUGH_VARIATION) - .offVariation(OFF_VARIATION) - .variations(THREE_VARIATIONS) - .version(versionFromKey(flagKey)); - } - - private static FlagBuilder buildRedGreenFlag(String flagKey) { - return flagBuilder(flagKey) - .fallthroughVariation(GREEN_VARIATION) - .offVariation(RED_VARIATION) - .variations(RED_GREEN_VARIATIONS) - .version(versionFromKey(flagKey)); - } + private static final LDContext BASE_USER = LDContext.create("x"); private static Rollout buildRollout(boolean isExperiment, boolean untrackedVariations) { List variations = new ArrayList<>(); variations.add(new WeightedVariation(1, 50000, untrackedVariations)); variations.add(new WeightedVariation(2, 50000, untrackedVariations)); - UserAttribute bucketBy = UserAttribute.KEY; RolloutKind kind = isExperiment ? RolloutKind.experiment : RolloutKind.rollout; Integer seed = 123; - Rollout rollout = new Rollout(variations, bucketBy, kind, seed); + Rollout rollout = new Rollout(null, variations, null, kind, seed); return rollout; } - private static int versionFromKey(String flagKey) { - return Math.abs(flagKey.hashCode()); - } - @Test - public void evaluationReturnsErrorIfUserIsNull() throws Exception { + public void evaluationReturnsErrorIfContextIsNull() throws Exception { DataModel.FeatureFlag f = flagBuilder("feature").build(); EvalResult result = BASE_EVALUATOR.evaluate(f, null, expectNoPrerequisiteEvals()); - assertEquals(EvalResult.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED), result); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.EXCEPTION), result); } @Test - public void evaluationReturnsErrorIfUserKeyIsNull() throws Exception { + public void evaluationReturnsErrorIfContextIsInvalid() throws Exception { DataModel.FeatureFlag f = flagBuilder("feature").build(); - EvalResult result = BASE_EVALUATOR.evaluate(f, new LDUser(null), expectNoPrerequisiteEvals()); + EvalResult result = BASE_EVALUATOR.evaluate(f, LDContext.create(""), expectNoPrerequisiteEvals()); - assertEquals(EvalResult.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED), result); + assertEquals(EvalResult.error(EvaluationReason.ErrorKind.EXCEPTION), result); } @Test @@ -187,8 +162,8 @@ public void flagReturnsNotInExperimentForFallthrougWhenInExperimentVariationButN public void flagReturnsInExperimentForRuleMatchWhenInExperimentVariation() throws Exception { Rollout rollout = buildRollout(true, false); - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of(BASE_USER.getKey())); - DataModel.Rule rule = ruleBuilder().id("ruleid0").clauses(clause).rollout(rollout).build(); + DataModel.Rule rule = ruleBuilder().id("ruleid0").clauses(clauseMatchingContext(BASE_USER)) + .rollout(rollout).build(); DataModel.FeatureFlag f = buildThreeWayFlag("feature") .on(true) @@ -203,8 +178,8 @@ public void flagReturnsInExperimentForRuleMatchWhenInExperimentVariation() throw public void flagReturnsNotInExperimentForRuleMatchWhenNotInExperimentVariation() throws Exception { Rollout rollout = buildRollout(true, true); - DataModel.Clause clause = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); - DataModel.Rule rule = ruleBuilder().id("ruleid0").clauses(clause).rollout(rollout).build(); + DataModel.Rule rule = ruleBuilder().id("ruleid0").clauses(clauseMatchingContext(BASE_USER)) + .rollout(rollout).build(); DataModel.FeatureFlag f = buildThreeWayFlag("feature") .on(true) @@ -215,6 +190,37 @@ public void flagReturnsNotInExperimentForRuleMatchWhenNotInExperimentVariation() assert(!result.getReason().isInExperiment()); } + @Test + public void flagReturnsNotInExperimentWhenContextKindIsNotFound() throws Exception { + Rollout rollout = new Rollout( + ContextKind.of("nonexistent"), + Arrays.asList( + new WeightedVariation(0, 1, false), + new WeightedVariation(1, 99999, false) + ), + null, + RolloutKind.experiment, + null); + + DataModel.Rule rule = ruleBuilder().id("ruleid0").clauses(clauseMatchingContext(BASE_USER)) + .rollout(rollout).build(); + DataModel.FeatureFlag flagWithRule = buildThreeWayFlag("feature") + .on(true) + .rules(rule) + .build(); + EvalResult result1 = BASE_EVALUATOR.evaluate(flagWithRule, BASE_USER, expectNoPrerequisiteEvals()); + assert(!result1.getReason().isInExperiment()); + + DataModel.FeatureFlag flagWithFallthrough = buildThreeWayFlag("feature") + .on(true) + .fallthrough(rollout) + .rules(rule) + .build(); + EvalResult result2 = BASE_EVALUATOR.evaluate(flagWithFallthrough, BASE_USER, expectNoPrerequisiteEvals()); + assert(!result2.getReason().isInExperiment()); + + } + @Test public void flagReturnsFallthroughIfFlagIsOnAndThereAreNoRules() throws Exception { DataModel.FeatureFlag f = buildThreeWayFlag("feature") @@ -444,22 +450,10 @@ public void multipleLevelsOfPrerequisitesProduceMultipleEvents() throws Exceptio assertEquals(GREEN_VALUE, eval1.result.getValue()); } - @Test - public void flagMatchesUserFromTargets() throws Exception { - DataModel.FeatureFlag f = buildThreeWayFlag("feature") - .on(true) - .targets(target(2, "whoever", "userkey")) - .build(); - LDUser user = new LDUser.Builder("userkey").build(); - EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); - - assertEquals(EvalResult.of(MATCH_VALUE, MATCH_VARIATION, EvaluationReason.targetMatch()), result); - } - @Test public void flagMatchesUserFromRules() { - DataModel.Clause clause0 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("wrongkey")); - DataModel.Clause clause1 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); + DataModel.Clause clause0 = clause("key", DataModel.Operator.in, LDValue.of("wrongkey")); + DataModel.Clause clause1 = clause("key", DataModel.Operator.in, LDValue.of("userkey")); DataModel.Rule rule0 = ruleBuilder().id("ruleid0").clauses(clause0).variation(2).build(); DataModel.Rule rule1 = ruleBuilder().id("ruleid1").clauses(clause1).variation(2).build(); @@ -468,7 +462,7 @@ public void flagMatchesUserFromRules() { .rules(rule0, rule1) .build(); - LDUser user = new LDUser.Builder("userkey").build(); + LDContext user = LDContext.create("userkey"); EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); assertEquals(EvalResult.of(MATCH_VALUE, MATCH_VARIATION, EvaluationReason.ruleMatch(1, "ruleid1")), result); @@ -476,8 +470,8 @@ public void flagMatchesUserFromRules() { @Test public void ruleMatchReasonHasTrackReasonTrueIfRuleLevelTrackEventsIsTrue() { - DataModel.Clause clause0 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("wrongkey")); - DataModel.Clause clause1 = clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("userkey")); + DataModel.Clause clause0 = clause("key", DataModel.Operator.in, LDValue.of("wrongkey")); + DataModel.Clause clause1 = clause("key", DataModel.Operator.in, LDValue.of("userkey")); DataModel.Rule rule0 = ruleBuilder().id("ruleid0").clauses(clause0).variation(2).build(); DataModel.Rule rule1 = ruleBuilder().id("ruleid1").clauses(clause1).variation(2) .trackEvents(true).build(); @@ -487,7 +481,7 @@ public void ruleMatchReasonHasTrackReasonTrueIfRuleLevelTrackEventsIsTrue() { .rules(rule0, rule1) .build(); - LDUser user = new LDUser.Builder("userkey").build(); + LDContext user = LDContext.create("userkey"); EvalResult result = BASE_EVALUATOR.evaluate(f, user, expectNoPrerequisiteEvals()); assertEquals( diff --git a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java index 98c354bd2..5f5e3d41c 100644 --- a/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java +++ b/src/test/java/com/launchdarkly/sdk/server/EvaluatorTestUtil.java @@ -2,16 +2,62 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.Logs; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.BigSegmentStoreWrapper.BigSegmentsQueryResult; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.ModelBuilders.FlagBuilder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; + @SuppressWarnings("javadoc") public abstract class EvaluatorTestUtil { + public static final LDContext BASE_USER = LDContext.create("x"); + + // These constants and flag builders define two kinds of flag: one with three variations-- allowing us to + // distinguish between match, fallthrough, and off results-- and one with two. + public static final int OFF_VARIATION = 0; + public static final LDValue OFF_VALUE = LDValue.of("off"); + public static final int FALLTHROUGH_VARIATION = 1; + public static final LDValue FALLTHROUGH_VALUE = LDValue.of("fall"); + public static final int MATCH_VARIATION = 2; + public static final LDValue MATCH_VALUE = LDValue.of("match"); + public static final LDValue[] THREE_VARIATIONS = new LDValue[] { OFF_VALUE, FALLTHROUGH_VALUE, MATCH_VALUE }; + + public static final int RED_VARIATION = 0; + public static final LDValue RED_VALUE = LDValue.of("red"); + public static final int GREEN_VARIATION = 1; + public static final LDValue GREEN_VALUE = LDValue.of("green"); + public static final LDValue[] RED_GREEN_VARIATIONS = new LDValue[] { RED_VALUE, GREEN_VALUE }; + + public static FlagBuilder buildThreeWayFlag(String flagKey) { + return flagBuilder(flagKey) + .fallthroughVariation(FALLTHROUGH_VARIATION) + .offVariation(OFF_VARIATION) + .variations(THREE_VARIATIONS) + .version(versionFromKey(flagKey)); + } + + public static FlagBuilder buildRedGreenFlag(String flagKey) { + return flagBuilder(flagKey) + .fallthroughVariation(GREEN_VARIATION) + .offVariation(RED_VARIATION) + .variations(RED_GREEN_VARIATIONS) + .version(versionFromKey(flagKey)); + } + + public static int versionFromKey(String flagKey) { + return Math.abs(flagKey.hashCode()); + } + + public static EvaluatorBuilder evaluatorBuilder() { + return new EvaluatorBuilder(); + } + public static Evaluator BASE_EVALUATOR = new EvaluatorBuilder().build(); public static class EvaluatorBuilder { @@ -92,13 +138,13 @@ public static Evaluator.PrerequisiteEvaluationSink expectNoPrerequisiteEvals() { public static final class PrereqEval { public final FeatureFlag flag; public final FeatureFlag prereqOfFlag; - public final LDUser user; + public final LDContext context; public final EvalResult result; - public PrereqEval(FeatureFlag flag, FeatureFlag prereqOfFlag, LDUser user, EvalResult result) { + public PrereqEval(FeatureFlag flag, FeatureFlag prereqOfFlag, LDContext context, EvalResult result) { this.flag = flag; this.prereqOfFlag = prereqOfFlag; - this.user = user; + this.context = context; this.result = result; } } @@ -107,9 +153,9 @@ public static final class PrereqRecorder implements Evaluator.PrerequisiteEvalua public final List evals = new ArrayList(); @Override - public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDUser user, + public void recordPrerequisiteEvaluation(FeatureFlag flag, FeatureFlag prereqOfFlag, LDContext context, EvalResult result) { - evals.add(new PrereqEval(flag, prereqOfFlag, user, result)); + evals.add(new PrereqEval(flag, prereqOfFlag, context, result)); } } } diff --git a/src/test/java/com/launchdarkly/sdk/server/EventFactoryTest.java b/src/test/java/com/launchdarkly/sdk/server/EventFactoryTest.java deleted file mode 100644 index 213d710d0..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/EventFactoryTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.interfaces.Event.FeatureRequest; - -import org.junit.Test; - -import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -public class EventFactoryTest { - private static final LDUser BASE_USER = new LDUser.Builder("x").build(); - private static final LDValue SOME_VALUE = LDValue.of("value"); - private static final int SOME_VARIATION = 11; - private static final EvaluationReason SOME_REASON = EvaluationReason.fallthrough(); - private static final EvalResult SOME_RESULT = EvalResult.of(SOME_VALUE, SOME_VARIATION, SOME_REASON); - private static final LDValue DEFAULT_VALUE = LDValue.of("default"); - - @Test - public void flagKeyIsSetInFeatureEvent() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assertEquals(flag.getKey(), fr.getKey()); - } - - @Test - public void flagVersionIsSetInFeatureEvent() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assertEquals(flag.getVersion(), fr.getVersion()); - } - - @Test - public void userIsSetInFeatureEvent() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assertEquals(BASE_USER, fr.getUser()); - } - - @Test - public void valueIsSetInFeatureEvent() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assertEquals(SOME_VALUE, fr.getValue()); - } - - @Test - public void variationIsSetInFeatureEvent() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assertEquals(SOME_VARIATION, fr.getVariation()); - } - - @Test - public void reasonIsNormallyNotIncludedWithDefaultEventFactory() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assertNull(fr.getReason()); - } - - @Test - public void reasonIsIncludedWithEventFactoryThatIsConfiguredToIncludedReasons() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT_WITH_REASONS.newFeatureRequestEvent( - flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assertEquals(SOME_REASON, fr.getReason()); - } - - @Test - public void reasonIsIncludedIfForceReasonTrackingIsTrue() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, - SOME_RESULT.withForceReasonTracking(true), DEFAULT_VALUE); - - assertEquals(SOME_REASON, fr.getReason()); - } - @Test - public void trackEventsIsNormallyFalse() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assert(!fr.isTrackEvents()); - } - - @Test - public void trackEventsIsTrueIfItIsTrueInFlag() { - FeatureFlag flag = flagBuilder("flagkey") - .trackEvents(true) - .build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, SOME_RESULT, DEFAULT_VALUE); - - assert(fr.isTrackEvents()); - } - - @Test - public void trackEventsIsTrueIfForceReasonTrackingIsTrue() { - FeatureFlag flag = flagBuilder("flagkey").build(); - FeatureRequest fr = EventFactory.DEFAULT.newFeatureRequestEvent(flag, BASE_USER, - SOME_RESULT.withForceReasonTracking(true), DEFAULT_VALUE); - - assert(fr.isTrackEvents()); - } - -} diff --git a/src/test/java/com/launchdarkly/sdk/server/EventOutputTest.java b/src/test/java/com/launchdarkly/sdk/server/EventOutputTest.java deleted file mode 100644 index 3d0240595..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/EventOutputTest.java +++ /dev/null @@ -1,543 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableSet; -import com.google.gson.Gson; -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.ObjectBuilder; -import com.launchdarkly.sdk.UserAttribute; -import com.launchdarkly.sdk.server.EventSummarizer.EventSummary; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.Event.AliasEvent; -import com.launchdarkly.sdk.server.interfaces.Event.FeatureRequest; - -import org.junit.Test; - -import java.io.IOException; -import java.io.StringWriter; -import java.util.Set; - -import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION; -import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; -import static com.launchdarkly.sdk.server.TestComponents.defaultEventsConfig; -import static com.launchdarkly.sdk.server.TestComponents.makeEventsConfig; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.junit.Assert.assertEquals; - -@SuppressWarnings("javadoc") -public class EventOutputTest { - private static final Gson gson = new Gson(); - private static final String[] attributesThatCanBePrivate = new String[] { - "avatar", "country", "custom1", "custom2", "email", "firstName", "ip", "lastName", "name", "secondary" - }; - - private LDUser.Builder userBuilderWithAllAttributes = new LDUser.Builder("userkey") - .anonymous(true) - .avatar("http://avatar") - .country("US") - .custom("custom1", "value1") - .custom("custom2", "value2") - .email("test@example.com") - .firstName("first") - .ip("1.2.3.4") - .lastName("last") - .name("me") - .secondary("s"); - private static final LDValue userJsonWithAllAttributes = parseValue("{" + - "\"key\":\"userkey\"," + - "\"anonymous\":true," + - "\"avatar\":\"http://avatar\"," + - "\"country\":\"US\"," + - "\"custom\":{\"custom1\":\"value1\",\"custom2\":\"value2\"}," + - "\"email\":\"test@example.com\"," + - "\"firstName\":\"first\"," + - "\"ip\":\"1.2.3.4\"," + - "\"lastName\":\"last\"," + - "\"name\":\"me\"," + - "\"secondary\":\"s\"" + - "}"); - - @Test - public void allUserAttributesAreSerialized() throws Exception { - testInlineUserSerialization(userBuilderWithAllAttributes.build(), userJsonWithAllAttributes, - defaultEventsConfig()); - } - - @Test - public void unsetUserAttributesAreNotSerialized() throws Exception { - LDUser user = new LDUser("userkey"); - LDValue userJson = parseValue("{\"key\":\"userkey\"}"); - testInlineUserSerialization(user, userJson, defaultEventsConfig()); - } - - @Test - public void userKeyIsSetInsteadOfUserWhenNotInlined() throws Exception { - LDUser user = new LDUser.Builder("userkey").name("me").build(); - LDValue userJson = parseValue("{\"key\":\"userkey\",\"name\":\"me\"}"); - EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); - - Event.FeatureRequest featureEvent = EventFactory.DEFAULT.newFeatureRequestEvent( - flagBuilder("flag").build(), - user, - EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), - LDValue.ofNull()); - LDValue outputEvent = getSingleOutputEvent(f, featureEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("user")); - assertEquals(LDValue.of(user.getKey()), outputEvent.get("userKey")); - - Event.Identify identifyEvent = EventFactory.DEFAULT.newIdentifyEvent(user); - outputEvent = getSingleOutputEvent(f, identifyEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("userKey")); - assertEquals(userJson, outputEvent.get("user")); - - Event.Custom customEvent = EventFactory.DEFAULT.newCustomEvent("custom", user, LDValue.ofNull(), null); - outputEvent = getSingleOutputEvent(f, customEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("user")); - assertEquals(LDValue.of(user.getKey()), outputEvent.get("userKey")); - - Event.Index indexEvent = new Event.Index(0, user); - outputEvent = getSingleOutputEvent(f, indexEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("userKey")); - assertEquals(userJson, outputEvent.get("user")); - } - - @Test - public void allAttributesPrivateMakesAttributesPrivate() throws Exception { - LDUser user = userBuilderWithAllAttributes.build(); - EventsConfiguration config = makeEventsConfig(true, false, null); - testPrivateAttributes(config, user, attributesThatCanBePrivate); - } - - @Test - public void globalPrivateAttributeNamesMakeAttributesPrivate() throws Exception { - LDUser user = userBuilderWithAllAttributes.build(); - for (String attrName: attributesThatCanBePrivate) { - EventsConfiguration config = makeEventsConfig(false, false, ImmutableSet.of(UserAttribute.forName(attrName))); - testPrivateAttributes(config, user, attrName); - } - } - - @Test - public void perUserPrivateAttributesMakeAttributePrivate() throws Exception { - LDUser baseUser = userBuilderWithAllAttributes.build(); - EventsConfiguration config = defaultEventsConfig(); - - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateAvatar("x").build(), "avatar"); - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateCountry("US").build(), "country"); - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateCustom("custom1", "x").build(), "custom1"); - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateEmail("x").build(), "email"); - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateFirstName("x").build(), "firstName"); - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateLastName("x").build(), "lastName"); - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateName("x").build(), "name"); - testPrivateAttributes(config, new LDUser.Builder(baseUser).privateSecondary("x").build(), "secondary"); - } - - private void testPrivateAttributes(EventsConfiguration config, LDUser user, String... privateAttrNames) throws IOException { - EventOutputFormatter f = new EventOutputFormatter(config); - Set privateAttrNamesSet = ImmutableSet.copyOf(privateAttrNames); - Event.Identify identifyEvent = EventFactory.DEFAULT.newIdentifyEvent(user); - LDValue outputEvent = getSingleOutputEvent(f, identifyEvent); - LDValue userJson = outputEvent.get("user"); - - ObjectBuilder o = LDValue.buildObject(); - for (String key: userJsonWithAllAttributes.keys()) { - LDValue value = userJsonWithAllAttributes.get(key); - if (!privateAttrNamesSet.contains(key)) { - if (key.equals("custom")) { - ObjectBuilder co = LDValue.buildObject(); - for (String customKey: value.keys()) { - if (!privateAttrNamesSet.contains(customKey)) { - co.put(customKey, value.get(customKey)); - } - } - LDValue custom = co.build(); - if (custom.size() > 0) { - o.put(key, custom); - } - } else { - o.put(key, value); - } - } - } - o.put("privateAttrs", LDValue.Convert.String.arrayOf(privateAttrNames)); - - assertEquals(o.build(), userJson); - } - - private ObjectBuilder buildFeatureEventProps(String key, String userKey) { - return LDValue.buildObject() - .put("kind", "feature") - .put("key", key) - .put("creationDate", 100000) - .put("userKey", userKey); - } - - private ObjectBuilder buildFeatureEventProps(String key) { - return buildFeatureEventProps(key, "userkey"); - } - - @Test - public void featureEventIsSerialized() throws Exception { - EventFactory factory = eventFactoryWithTimestamp(100000, false); - EventFactory factoryWithReason = eventFactoryWithTimestamp(100000, true); - DataModel.FeatureFlag flag = flagBuilder("flag").version(11).build(); - LDUser user = new LDUser.Builder("userkey").name("me").build(); - LDUser anon = new LDUser.Builder("anonymouskey").anonymous(true).build(); - EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); - - FeatureRequest feWithVariation = factory.newFeatureRequestEvent(flag, user, - EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.off()), - LDValue.of("defaultvalue")); - LDValue feJson1 = buildFeatureEventProps("flag") - .put("version", 11) - .put("variation", 1) - .put("value", "flagvalue") - .put("default", "defaultvalue") - .build(); - assertEquals(feJson1, getSingleOutputEvent(f, feWithVariation)); - - FeatureRequest feWithoutVariationOrDefault = factory.newFeatureRequestEvent(flag, user, - EvalResult.of(LDValue.of("flagvalue"), NO_VARIATION, EvaluationReason.off()), - LDValue.ofNull()); - LDValue feJson2 = buildFeatureEventProps("flag") - .put("version", 11) - .put("value", "flagvalue") - .build(); - assertEquals(feJson2, getSingleOutputEvent(f, feWithoutVariationOrDefault)); - - FeatureRequest feWithReason = factoryWithReason.newFeatureRequestEvent(flag, user, - EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), - LDValue.of("defaultvalue")); - LDValue feJson3 = buildFeatureEventProps("flag") - .put("version", 11) - .put("variation", 1) - .put("value", "flagvalue") - .put("default", "defaultvalue") - .put("reason", LDValue.buildObject().put("kind", "FALLTHROUGH").build()) - .build(); - assertEquals(feJson3, getSingleOutputEvent(f, feWithReason)); - - FeatureRequest feUnknownFlag = factoryWithReason.newUnknownFeatureRequestEvent("flag", user, - LDValue.of("defaultvalue"), EvaluationReason.ErrorKind.FLAG_NOT_FOUND); - LDValue feJson4 = buildFeatureEventProps("flag") - .put("value", "defaultvalue") - .put("default", "defaultvalue") - .put("reason", LDValue.buildObject().put("kind", "ERROR").put("errorKind", "FLAG_NOT_FOUND").build()) - .build(); - assertEquals(feJson4, getSingleOutputEvent(f, feUnknownFlag)); - - Event.FeatureRequest debugEvent = EventFactory.newDebugEvent(feWithVariation); - - LDValue feJson5 = LDValue.buildObject() - .put("kind", "debug") - .put("key", "flag") - .put("creationDate", 100000) - .put("version", 11) - .put("variation", 1) - .put("user", LDValue.buildObject().put("key", "userkey").put("name", "me").build()) - .put("value", "flagvalue") - .put("default", "defaultvalue") - .build(); - assertEquals(feJson5, getSingleOutputEvent(f, debugEvent)); - - DataModel.FeatureFlag parentFlag = flagBuilder("parent").build(); - Event.FeatureRequest prereqEvent = factory.newPrerequisiteFeatureRequestEvent(flag, user, - EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), parentFlag); - LDValue feJson6 = buildFeatureEventProps("flag") - .put("version", 11) - .put("variation", 1) - .put("value", "flagvalue") - .put("prereqOf", "parent") - .build(); - assertEquals(feJson6, getSingleOutputEvent(f, prereqEvent)); - - Event.FeatureRequest prereqWithReason = factoryWithReason.newPrerequisiteFeatureRequestEvent(flag, user, - EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.fallthrough()), parentFlag); - LDValue feJson7 = buildFeatureEventProps("flag") - .put("version", 11) - .put("variation", 1) - .put("value", "flagvalue") - .put("reason", LDValue.buildObject().put("kind", "FALLTHROUGH").build()) - .put("prereqOf", "parent") - .build(); - assertEquals(feJson7, getSingleOutputEvent(f, prereqWithReason)); - - Event.FeatureRequest prereqWithoutResult = factoryWithReason.newPrerequisiteFeatureRequestEvent(flag, user, - null, parentFlag); - LDValue feJson8 = buildFeatureEventProps("flag") - .put("version", 11) - .put("prereqOf", "parent") - .build(); - assertEquals(feJson8, getSingleOutputEvent(f, prereqWithoutResult)); - - FeatureRequest anonFeWithVariation = factory.newFeatureRequestEvent(flag, anon, - EvalResult.of(LDValue.of("flagvalue"), 1, EvaluationReason.off()), - LDValue.of("defaultvalue")); - LDValue anonFeJson1 = buildFeatureEventProps("flag", "anonymouskey") - .put("version", 11) - .put("variation", 1) - .put("value", "flagvalue") - .put("default", "defaultvalue") - .put("contextKind", "anonymousUser") - .build(); - assertEquals(anonFeJson1, getSingleOutputEvent(f, anonFeWithVariation)); - } - - @Test - public void identifyEventIsSerialized() throws IOException { - EventFactory factory = eventFactoryWithTimestamp(100000, false); - LDUser user = new LDUser.Builder("userkey").name("me").build(); - EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); - - Event.Identify ie = factory.newIdentifyEvent(user); - LDValue ieJson = parseValue("{" + - "\"kind\":\"identify\"," + - "\"creationDate\":100000," + - "\"key\":\"userkey\"," + - "\"user\":{\"key\":\"userkey\",\"name\":\"me\"}" + - "}"); - assertEquals(ieJson, getSingleOutputEvent(f, ie)); - } - - @Test - public void customEventIsSerialized() throws IOException { - EventFactory factory = eventFactoryWithTimestamp(100000, false); - LDUser user = new LDUser.Builder("userkey").name("me").build(); - LDUser anon = new LDUser.Builder("userkey").name("me").anonymous(true).build(); - EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); - - Event.Custom ceWithoutData = factory.newCustomEvent("customkey", user, LDValue.ofNull(), null); - LDValue ceJson1 = parseValue("{" + - "\"kind\":\"custom\"," + - "\"creationDate\":100000," + - "\"key\":\"customkey\"," + - "\"userKey\":\"userkey\"" + - "}"); - assertEquals(ceJson1, getSingleOutputEvent(f, ceWithoutData)); - - Event.Custom ceWithData = factory.newCustomEvent("customkey", user, LDValue.of("thing"), null); - LDValue ceJson2 = parseValue("{" + - "\"kind\":\"custom\"," + - "\"creationDate\":100000," + - "\"key\":\"customkey\"," + - "\"userKey\":\"userkey\"," + - "\"data\":\"thing\"" + - "}"); - assertEquals(ceJson2, getSingleOutputEvent(f, ceWithData)); - - Event.Custom ceWithMetric = factory.newCustomEvent("customkey", user, LDValue.ofNull(), 2.5); - LDValue ceJson3 = parseValue("{" + - "\"kind\":\"custom\"," + - "\"creationDate\":100000," + - "\"key\":\"customkey\"," + - "\"userKey\":\"userkey\"," + - "\"metricValue\":2.5" + - "}"); - assertEquals(ceJson3, getSingleOutputEvent(f, ceWithMetric)); - - Event.Custom ceWithDataAndMetric = factory.newCustomEvent("customkey", user, LDValue.of("thing"), 2.5); - LDValue ceJson4 = parseValue("{" + - "\"kind\":\"custom\"," + - "\"creationDate\":100000," + - "\"key\":\"customkey\"," + - "\"userKey\":\"userkey\"," + - "\"data\":\"thing\"," + - "\"metricValue\":2.5" + - "}"); - assertEquals(ceJson4, getSingleOutputEvent(f, ceWithDataAndMetric)); - - Event.Custom ceWithDataAndMetricAnon = factory.newCustomEvent("customkey", anon, LDValue.of("thing"), 2.5); - LDValue ceJson5 = parseValue("{" + - "\"kind\":\"custom\"," + - "\"creationDate\":100000," + - "\"key\":\"customkey\"," + - "\"userKey\":\"userkey\"," + - "\"data\":\"thing\"," + - "\"metricValue\":2.5," + - "\"contextKind\":\"anonymousUser\"" + - "}"); - assertEquals(ceJson5, getSingleOutputEvent(f, ceWithDataAndMetricAnon)); - } - - @Test - public void summaryEventIsSerialized() throws Exception { - EventSummary summary = new EventSummary(); - summary.noteTimestamp(1001); - - // Note use of "new String()" to ensure that these flag keys are not interned, as string literals normally are - - // we found a bug where strings were being compared by reference equality. - - summary.incrementCounter(new String("first"), 1, 11, LDValue.of("value1a"), LDValue.of("default1")); - - summary.incrementCounter(new String("second"), 1, 21, LDValue.of("value2a"), LDValue.of("default2")); - - summary.incrementCounter(new String("first"), 1, 11, LDValue.of("value1a"), LDValue.of("default1")); - summary.incrementCounter(new String("first"), 1, 12, LDValue.of("value1a"), LDValue.of("default1")); - - summary.incrementCounter(new String("second"), 2, 21, LDValue.of("value2b"), LDValue.of("default2")); - summary.incrementCounter(new String("second"), -1, 21, LDValue.of("default2"), LDValue.of("default2")); // flag exists (has version), but eval failed (no variation) - - summary.incrementCounter(new String("third"), -1, -1, LDValue.of("default3"), LDValue.of("default3")); // flag doesn't exist (no version) - - summary.noteTimestamp(1000); - summary.noteTimestamp(1002); - - EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); - StringWriter w = new StringWriter(); - int count = f.writeOutputEvents(new Event[0], summary, w); - assertEquals(1, count); - LDValue outputEvent = parseValue(w.toString()).get(0); - - assertEquals("summary", outputEvent.get("kind").stringValue()); - assertEquals(1000, outputEvent.get("startDate").intValue()); - assertEquals(1002, outputEvent.get("endDate").intValue()); - - LDValue featuresJson = outputEvent.get("features"); - assertEquals(3, featuresJson.size()); - - LDValue firstJson = featuresJson.get("first"); - assertEquals("default1", firstJson.get("default").stringValue()); - assertThat(firstJson.get("counters").values(), containsInAnyOrder( - parseValue("{\"value\":\"value1a\",\"variation\":1,\"version\":11,\"count\":2}"), - parseValue("{\"value\":\"value1a\",\"variation\":1,\"version\":12,\"count\":1}") - )); - - LDValue secondJson = featuresJson.get("second"); - assertEquals("default2", secondJson.get("default").stringValue()); - assertThat(secondJson.get("counters").values(), containsInAnyOrder( - parseValue("{\"value\":\"value2a\",\"variation\":1,\"version\":21,\"count\":1}"), - parseValue("{\"value\":\"value2b\",\"variation\":2,\"version\":21,\"count\":1}"), - parseValue("{\"value\":\"default2\",\"version\":21,\"count\":1}") - )); - - LDValue thirdJson = featuresJson.get("third"); - assertEquals("default3", thirdJson.get("default").stringValue()); - assertThat(thirdJson.get("counters").values(), contains( - parseValue("{\"unknown\":true,\"value\":\"default3\",\"count\":1}") - )); - } - - @Test - public void unknownEventClassIsNotSerialized() throws Exception { - // This shouldn't be able to happen in reality. - Event event = new FakeEventClass(1000, new LDUser("user")); - - EventOutputFormatter f = new EventOutputFormatter(defaultEventsConfig()); - StringWriter w = new StringWriter(); - f.writeOutputEvents(new Event[] { event }, new EventSummary(), w); - - assertEquals("[]", w.toString()); - } - - @Test - public void aliasEventIsSerialized() throws IOException { - EventFactory factory = eventFactoryWithTimestamp(1000, false); - LDUser user1 = new LDUser.Builder("bob-key").build(); - LDUser user2 = new LDUser.Builder("jeff-key").build(); - LDUser anon1 = new LDUser.Builder("bob-key-anon").anonymous(true).build(); - LDUser anon2 = new LDUser.Builder("jeff-key-anon").anonymous(true).build(); - AliasEvent userToUser = factory.newAliasEvent(user1, user2); - AliasEvent userToAnon = factory.newAliasEvent(anon1, user1); - AliasEvent anonToUser = factory.newAliasEvent(user1, anon1); - AliasEvent anonToAnon = factory.newAliasEvent(anon1, anon2); - - EventOutputFormatter fmt = new EventOutputFormatter(defaultEventsConfig()); - - LDValue userToUserExpected = parseValue("{" + - "\"kind\":\"alias\"," + - "\"creationDate\":1000," + - "\"key\":\"bob-key\"," + - "\"contextKind\":\"user\"," + - "\"previousKey\":\"jeff-key\"," + - "\"previousContextKind\":\"user\"" + - "}"); - - assertEquals(userToUserExpected, getSingleOutputEvent(fmt, userToUser)); - - LDValue userToAnonExpected = parseValue("{" + - "\"kind\":\"alias\"," + - "\"creationDate\":1000," + - "\"key\":\"bob-key-anon\"," + - "\"contextKind\":\"anonymousUser\"," + - "\"previousKey\":\"bob-key\"," + - "\"previousContextKind\":\"user\"" + - "}"); - - assertEquals(userToAnonExpected, getSingleOutputEvent(fmt, userToAnon)); - - LDValue anonToUserExpected = parseValue("{" + - "\"kind\":\"alias\"," + - "\"creationDate\":1000," + - "\"key\":\"bob-key\"," + - "\"contextKind\":\"user\"," + - "\"previousKey\":\"bob-key-anon\"," + - "\"previousContextKind\":\"anonymousUser\"" + - "}"); - - assertEquals(anonToUserExpected, getSingleOutputEvent(fmt, anonToUser)); - - LDValue anonToAnonExpected = parseValue("{" + - "\"kind\":\"alias\"," + - "\"creationDate\":1000," + - "\"key\":\"bob-key-anon\"," + - "\"contextKind\":\"anonymousUser\"," + - "\"previousKey\":\"jeff-key-anon\"," + - "\"previousContextKind\":\"anonymousUser\"" + - "}"); - - assertEquals(anonToAnonExpected, getSingleOutputEvent(fmt, anonToAnon)); - } - - private static class FakeEventClass extends Event { - public FakeEventClass(long creationDate, LDUser user) { - super(creationDate, user); - } - } - - private static LDValue parseValue(String json) { - return gson.fromJson(json, LDValue.class); - } - - private EventFactory eventFactoryWithTimestamp(final long timestamp, final boolean includeReasons) { - return new EventFactory.Default(includeReasons, () -> timestamp); - } - - private LDValue getSingleOutputEvent(EventOutputFormatter f, Event event) throws IOException { - StringWriter w = new StringWriter(); - int count = f.writeOutputEvents(new Event[] { event }, new EventSummary(), w); - assertEquals(1, count); - return parseValue(w.toString()).get(0); - } - - private void testInlineUserSerialization(LDUser user, LDValue expectedJsonValue, EventsConfiguration baseConfig) throws IOException { - EventsConfiguration config = makeEventsConfig(baseConfig.allAttributesPrivate, true, baseConfig.privateAttributes); - EventOutputFormatter f = new EventOutputFormatter(config); - - Event.FeatureRequest featureEvent = EventFactory.DEFAULT.newFeatureRequestEvent( - flagBuilder("flag").build(), - user, - EvalResult.of(LDValue.ofNull(), NO_VARIATION, EvaluationReason.off()), - LDValue.ofNull()); - LDValue outputEvent = getSingleOutputEvent(f, featureEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("userKey")); - assertEquals(expectedJsonValue, outputEvent.get("user")); - - Event.Identify identifyEvent = EventFactory.DEFAULT.newIdentifyEvent(user); - outputEvent = getSingleOutputEvent(f, identifyEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("userKey")); - assertEquals(expectedJsonValue, outputEvent.get("user")); - - Event.Custom customEvent = EventFactory.DEFAULT.newCustomEvent("custom", user, LDValue.ofNull(), null); - outputEvent = getSingleOutputEvent(f, customEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("userKey")); - assertEquals(expectedJsonValue, outputEvent.get("user")); - - Event.Index indexEvent = new Event.Index(0, user); - outputEvent = getSingleOutputEvent(f, indexEvent); - assertEquals(LDValue.ofNull(), outputEvent.get("userKey")); - assertEquals(expectedJsonValue, outputEvent.get("user")); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/EventSummarizerTest.java b/src/test/java/com/launchdarkly/sdk/server/EventSummarizerTest.java deleted file mode 100644 index f3d2af2e6..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/EventSummarizerTest.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableMap; -import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.EventSummarizer.CounterValue; -import com.launchdarkly.sdk.server.EventSummarizer.EventSummary; -import com.launchdarkly.sdk.server.EventSummarizer.FlagInfo; -import com.launchdarkly.sdk.server.EventSummarizer.SimpleIntKeyedMap; -import com.launchdarkly.sdk.server.interfaces.Event; - -import org.junit.Test; - -import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; -import static com.launchdarkly.sdk.server.TestUtil.simpleEvaluation; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -@SuppressWarnings("javadoc") -public class EventSummarizerTest { - private static final LDUser user = new LDUser.Builder("key").build(); - - private long eventTimestamp; - private EventFactory eventFactory = new EventFactory.Default(false, () -> eventTimestamp); - - @Test - public void summarizerCanBeCleared() { - EventSummarizer es = new EventSummarizer(); - assertTrue(es.isEmpty()); - - DataModel.FeatureFlag flag = flagBuilder("key").build(); - Event event = eventFactory.newFeatureRequestEvent(flag, user, null, null); - es.summarizeEvent(event); - - assertFalse(es.isEmpty()); - - es.clear(); - - assertTrue(es.isEmpty()); - } - - @Test - public void summarizeEventDoesNothingForIdentifyEvent() { - EventSummarizer es = new EventSummarizer(); - es.summarizeEvent(eventFactory.newIdentifyEvent(user)); - assertTrue(es.isEmpty()); - } - - @Test - public void summarizeEventDoesNothingForCustomEvent() { - EventSummarizer es = new EventSummarizer(); - es.summarizeEvent(eventFactory.newCustomEvent("whatever", user, null, null)); - assertTrue(es.isEmpty()); - } - - @Test - public void summarizeEventSetsStartAndEndDates() { - EventSummarizer es = new EventSummarizer(); - DataModel.FeatureFlag flag = flagBuilder("key").build(); - eventTimestamp = 2000; - Event event1 = eventFactory.newFeatureRequestEvent(flag, user, null, null); - eventTimestamp = 1000; - Event event2 = eventFactory.newFeatureRequestEvent(flag, user, null, null); - eventTimestamp = 1500; - Event event3 = eventFactory.newFeatureRequestEvent(flag, user, null, null); - es.summarizeEvent(event1); - es.summarizeEvent(event2); - es.summarizeEvent(event3); - EventSummarizer.EventSummary data = es.getSummaryAndReset(); - - assertEquals(1000, data.startDate); - assertEquals(2000, data.endDate); - } - - @Test - public void summarizeEventIncrementsCounters() { - EventSummarizer es = new EventSummarizer(); - DataModel.FeatureFlag flag1 = flagBuilder("key1").version(11).build(); - DataModel.FeatureFlag flag2 = flagBuilder("key2").version(22).build(); - String unknownFlagKey = "badkey"; - LDValue value1 = LDValue.of("value1"), value2 = LDValue.of("value2"), value99 = LDValue.of("value99"), - default1 = LDValue.of("default1"), default2 = LDValue.of("default2"), default3 = LDValue.of("default3"); - Event event1 = eventFactory.newFeatureRequestEvent(flag1, user, - simpleEvaluation(1, value1), default1); - Event event2 = eventFactory.newFeatureRequestEvent(flag1, user, - simpleEvaluation(2, value2), default1); - Event event3 = eventFactory.newFeatureRequestEvent(flag2, user, - simpleEvaluation(1, value99), default2); - Event event4 = eventFactory.newFeatureRequestEvent(flag1, user, - simpleEvaluation(1, value1), default1); - Event event5 = eventFactory.newUnknownFeatureRequestEvent(unknownFlagKey, user, default3, EvaluationReason.ErrorKind.FLAG_NOT_FOUND); - es.summarizeEvent(event1); - es.summarizeEvent(event2); - es.summarizeEvent(event3); - es.summarizeEvent(event4); - es.summarizeEvent(event5); - EventSummarizer.EventSummary data = es.getSummaryAndReset(); - - assertThat(data.counters, equalTo(ImmutableMap.builder() - .put(flag1.getKey(), new FlagInfo(default1, - new SimpleIntKeyedMap>() - .put(flag1.getVersion(), new SimpleIntKeyedMap() - .put(1, new CounterValue(2, value1)) - .put(2, new CounterValue(1, value2)) - ))) - .put(flag2.getKey(), new FlagInfo(default2, - new SimpleIntKeyedMap>() - .put(flag2.getVersion(), new SimpleIntKeyedMap() - .put(1, new CounterValue(1, value99)) - ))) - .put(unknownFlagKey, new FlagInfo(default3, - new SimpleIntKeyedMap>() - .put(-1, new SimpleIntKeyedMap() - .put(-1, new CounterValue(1, default3)) - ))) - .build())); - } - - // The following implementations are used only in debug/test code, but may as well test them - - @Test - public void counterValueEquality() { - CounterValue value1 = new CounterValue(1, LDValue.of("a")); - CounterValue value2 = new CounterValue(1, LDValue.of("a")); - assertEquals(value1, value2); - assertEquals(value2, value1); - - for (CounterValue notEqualValue: new CounterValue[] { - new CounterValue(2, LDValue.of("a")), - new CounterValue(1, LDValue.of("b")) - }) { - assertNotEquals(value1, notEqualValue); - assertNotEquals(notEqualValue, value1); - - assertNotEquals(value1, null); - assertNotEquals(value1, "x"); - } - } - - @Test - public void counterValueToString() { - assertEquals("(1,\"a\")", new CounterValue(1, LDValue.of("a")).toString()); - } - - @Test - public void eventSummaryEquality() { - String key1 = "key1", key2 = "key2"; - int variation1 = 0, variation2 = 1, variation3 = 2, version1 = 10, version2 = 20; - LDValue value1 = LDValue.of(1), value2 = LDValue.of(2), value3 = LDValue.of(3), - default1 = LDValue.of(-1), default2 = LDValue.of(-2); - EventSummary es1 = new EventSummary(); - es1.noteTimestamp(1000); - es1.incrementCounter(key1, variation1, version1, value1, default1); - es1.incrementCounter(key1, variation1, version1, value1, default1); - es1.incrementCounter(key1, variation2, version2, value2, default1); - es1.incrementCounter(key2, variation3, version2, value3, default2); - es1.noteTimestamp(2000); - - EventSummary es2 = new EventSummary(); // same operations in different order - es2.noteTimestamp(1000); - es2.incrementCounter(key2, variation3, version2, value3, default2); - es2.incrementCounter(key1, variation1, version1, value1, default1); - es2.incrementCounter(key1, variation2, version2, value2, default1); - es2.incrementCounter(key1, variation1, version1, value1, default1); - es2.noteTimestamp(2000); - - EventSummary es3 = new EventSummary(); // same operations with different start time - es3.noteTimestamp(1100); - es3.incrementCounter(key2, variation3, version2, value3, default2); - es3.incrementCounter(key1, variation1, version1, value1, default1); - es3.incrementCounter(key1, variation2, version2, value2, default1); - es3.incrementCounter(key1, variation1, version1, value1, default1); - es3.noteTimestamp(2000); - - EventSummary es4 = new EventSummary(); // same operations with different end time - es4.noteTimestamp(1000); - es4.incrementCounter(key2, variation3, version2, value3, default2); - es4.incrementCounter(key1, variation1, version1, value1, default1); - es4.incrementCounter(key1, variation2, version2, value2, default1); - es4.incrementCounter(key1, variation1, version1, value1, default1); - es4.noteTimestamp(1900); - - assertEquals(es1, es2); - assertEquals(es2, es1); - - assertEquals(0, es1.hashCode()); // see comment on hashCode - - assertNotEquals(es1, es3); - assertNotEquals(es1, es4); - - assertNotEquals(es1, null); - assertNotEquals(es1, "x"); - } - - @Test - public void simpleIntKeyedMapBehavior() { - // Tests the behavior of the inner class that we use instead of a Map. - SimpleIntKeyedMap m = new SimpleIntKeyedMap<>(); - int initialCapacity = m.capacity(); - - assertEquals(0, m.size()); - assertNotEquals(0, initialCapacity); - assertNull(m.get(1)); - - for (int i = 0; i < initialCapacity; i++) { - m.put(i * 100, "value" + i); - } - - assertEquals(initialCapacity, m.size()); - assertEquals(initialCapacity, m.capacity()); - - for (int i = 0; i < initialCapacity; i++) { - assertEquals("value" + i, m.get(i * 100)); - } - assertNull(m.get(33)); - - m.put(33, "other"); - assertNotEquals(initialCapacity, m.capacity()); - assertEquals(initialCapacity + 1, m.size()); - assertEquals("other", m.get(33)); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/EventUserSerializationTest.java b/src/test/java/com/launchdarkly/sdk/server/EventUserSerializationTest.java deleted file mode 100644 index bb83f97da..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/EventUserSerializationTest.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.gson.JsonObject; -import com.google.gson.reflect.TypeToken; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.UserAttribute; - -import org.junit.Test; - -import java.lang.reflect.Type; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstanceForEventsSerialization; -import static com.launchdarkly.sdk.server.TestComponents.defaultEventsConfig; -import static com.launchdarkly.sdk.server.TestComponents.makeEventsConfig; -import static com.launchdarkly.sdk.server.TestUtil.TEST_GSON_INSTANCE; -import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; - -@SuppressWarnings("javadoc") -public class EventUserSerializationTest { - - @Test - public void testAllPropertiesInPrivateAttributeEncoding() { - for (Map.Entry e: getUserPropertiesJsonMap().entrySet()) { - String expected = e.getValue(); - String actual = TEST_GSON_INSTANCE.toJson(e.getKey()); - assertJsonEquals(expected, actual); - } - } - - private Map getUserPropertiesJsonMap() { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put(new LDUser.Builder("userkey").build(), "{\"key\":\"userkey\"}"); - builder.put(new LDUser.Builder("userkey").secondary("value").build(), - "{\"key\":\"userkey\",\"secondary\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").ip("value").build(), - "{\"key\":\"userkey\",\"ip\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").email("value").build(), - "{\"key\":\"userkey\",\"email\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").name("value").build(), - "{\"key\":\"userkey\",\"name\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").avatar("value").build(), - "{\"key\":\"userkey\",\"avatar\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").firstName("value").build(), - "{\"key\":\"userkey\",\"firstName\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").lastName("value").build(), - "{\"key\":\"userkey\",\"lastName\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").anonymous(true).build(), - "{\"key\":\"userkey\",\"anonymous\":true}"); - builder.put(new LDUser.Builder("userkey").country("value").build(), - "{\"key\":\"userkey\",\"country\":\"value\"}"); - builder.put(new LDUser.Builder("userkey").custom("thing", "value").build(), - "{\"key\":\"userkey\",\"custom\":{\"thing\":\"value\"}}"); - return builder.build(); - } - - @Test - public void defaultJsonEncodingHasPrivateAttributeNames() { - LDUser user = new LDUser.Builder("userkey").privateName("x").build(); - String expected = "{\"key\":\"userkey\",\"name\":\"x\",\"privateAttributeNames\":[\"name\"]}"; - assertJsonEquals(expected, TEST_GSON_INSTANCE.toJson(user)); - } - - @Test - public void privateAttributeEncodingRedactsAllPrivateAttributes() { - EventsConfiguration config = makeEventsConfig(true, false, null); - LDUser user = new LDUser.Builder("userkey") - .secondary("s") - .ip("i") - .email("e") - .name("n") - .avatar("a") - .firstName("f") - .lastName("l") - .anonymous(true) - .country("USA") - .custom("thing", "value") - .build(); - Set redacted = ImmutableSet.of("secondary", "ip", "email", "name", "avatar", "firstName", "lastName", "country", "thing"); - - JsonObject o = gsonInstanceForEventsSerialization(config).toJsonTree(user).getAsJsonObject(); - assertEquals("userkey", o.get("key").getAsString()); - assertEquals(true, o.get("anonymous").getAsBoolean()); - for (String attr: redacted) { - assertNull(o.get(attr)); - } - assertNull(o.get("custom")); - assertEquals(redacted, getPrivateAttrs(o)); - } - - @Test - public void privateAttributeEncodingRedactsSpecificPerUserPrivateAttributes() { - LDUser user = new LDUser.Builder("userkey") - .email("e") - .privateName("n") - .custom("bar", 43) - .privateCustom("foo", 42) - .build(); - - JsonObject o = gsonInstanceForEventsSerialization(defaultEventsConfig()).toJsonTree(user).getAsJsonObject(); - assertEquals("e", o.get("email").getAsString()); - assertNull(o.get("name")); - assertEquals(43, o.get("custom").getAsJsonObject().get("bar").getAsInt()); - assertNull(o.get("custom").getAsJsonObject().get("foo")); - assertEquals(ImmutableSet.of("name", "foo"), getPrivateAttrs(o)); - } - - @Test - public void privateAttributeEncodingRedactsSpecificGlobalPrivateAttributes() { - EventsConfiguration config = makeEventsConfig(false, false, - ImmutableSet.of(UserAttribute.NAME, UserAttribute.forName("foo"))); - LDUser user = new LDUser.Builder("userkey") - .email("e") - .name("n") - .custom("bar", 43) - .custom("foo", 42) - .build(); - - JsonObject o = gsonInstanceForEventsSerialization(config).toJsonTree(user).getAsJsonObject(); - assertEquals("e", o.get("email").getAsString()); - assertNull(o.get("name")); - assertEquals(43, o.get("custom").getAsJsonObject().get("bar").getAsInt()); - assertNull(o.get("custom").getAsJsonObject().get("foo")); - assertEquals(ImmutableSet.of("name", "foo"), getPrivateAttrs(o)); - } - - @Test - public void privateAttributeEncodingWorksForMinimalUser() { - EventsConfiguration config = makeEventsConfig(true, false, null); - LDUser user = new LDUser("userkey"); - - JsonObject o = gsonInstanceForEventsSerialization(config).toJsonTree(user).getAsJsonObject(); - JsonObject expected = new JsonObject(); - expected.addProperty("key", "userkey"); - assertEquals(expected, o); - } - - @Test - public void cannotDeserializeEventUser() { - String json = "{}"; - LDUser user = gsonInstanceForEventsSerialization(defaultEventsConfig()).fromJson(json, LDUser.class); - assertNull(user); - } - - private Set getPrivateAttrs(JsonObject o) { - Type type = new TypeToken>(){}.getType(); - return TEST_GSON_INSTANCE.>fromJson(o.get("privateAttrs"), type); - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java b/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java index 24e437483..c69df413e 100644 --- a/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/FlagModelDeserializationTest.java @@ -2,7 +2,6 @@ import com.google.gson.Gson; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Operator; @@ -33,7 +32,7 @@ public void preprocessingIsDoneOnDeserialization() { .prerequisites(new Prerequisite("abc", 0)) .targets(target(0, "x")) .rules(ruleBuilder().clauses( - clause(UserAttribute.KEY, Operator.in, LDValue.of("x"), LDValue.of("y")) + clause("key", Operator.in, LDValue.of("x"), LDValue.of("y")) ).build()) .build(); String flagJson = JsonHelpers.serialize(originalFlag); diff --git a/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java b/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java index 05a4f36e4..410e68b48 100644 --- a/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/FlagTrackerImplTest.java @@ -1,5 +1,6 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; @@ -63,11 +64,12 @@ public void flagChangeListeners() throws Exception { @Test public void flagValueChangeListener() throws Exception { String flagKey = "important-flag"; - LDUser user = new LDUser("important-user"); - LDUser otherUser = new LDUser("unimportant-user"); + LDContext user = LDContext.create("important-user"); + LDUser userAsOldUserType = new LDUser(user.getKey()); + LDContext otherUser = LDContext.create("unimportant-user"); EventBroadcasterImpl broadcaster = EventBroadcasterImpl.forFlagChangeEvents(TestComponents.sharedExecutor, testLogger); - Map, LDValue> resultMap = new HashMap<>(); + Map, LDValue> resultMap = new HashMap<>(); FlagTrackerImpl tracker = new FlagTrackerImpl(broadcaster, (k, u) -> LDValue.normalize(resultMap.get(new AbstractMap.SimpleEntry<>(k, u)))); @@ -76,9 +78,11 @@ public void flagValueChangeListener() throws Exception { resultMap.put(new AbstractMap.SimpleEntry<>(flagKey, otherUser), LDValue.of(false)); BlockingQueue eventSink1 = new LinkedBlockingQueue<>(); + BlockingQueue eventSink1WithOldUser = new LinkedBlockingQueue<>(); BlockingQueue eventSink2 = new LinkedBlockingQueue<>(); BlockingQueue eventSink3 = new LinkedBlockingQueue<>(); tracker.addFlagValueChangeListener(flagKey, user, eventSink1::add); + tracker.addFlagValueChangeListener(flagKey, userAsOldUserType, eventSink1WithOldUser::add); FlagChangeListener listener2 = tracker.addFlagValueChangeListener(flagKey, user, eventSink2::add); tracker.removeFlagChangeListener(listener2); // just verifying that the remove method works tracker.addFlagValueChangeListener(flagKey, otherUser, eventSink3::add); @@ -98,6 +102,12 @@ public void flagValueChangeListener() throws Exception { assertThat(event1.getNewValue(), equalTo(LDValue.of(true))); assertNoMoreValues(eventSink1, 100, TimeUnit.MILLISECONDS); + // and the listener for the same user represented as the LDUser type also gets the event + FlagValueChangeEvent event1a = awaitValue(eventSink1WithOldUser, 1, TimeUnit.SECONDS); + assertThat(event1a.getKey(), equalTo(flagKey)); + assertThat(event1a.getOldValue(), equalTo(LDValue.of(false))); + assertThat(event1a.getNewValue(), equalTo(LDValue.of(true))); + // eventSink2 doesn't receive one, because it was unregistered assertNoMoreValues(eventSink2, 100, TimeUnit.MILLISECONDS); diff --git a/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java b/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java index 13400cf1d..e507dfb6d 100644 --- a/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/InMemoryDataStoreTest.java @@ -1,6 +1,6 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.server.interfaces.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStore; import org.junit.Test; diff --git a/src/test/java/com/launchdarkly/sdk/server/JsonHelpersTest.java b/src/test/java/com/launchdarkly/sdk/server/JsonHelpersTest.java index 25ce7f133..2ca157cd2 100644 --- a/src/test/java/com/launchdarkly/sdk/server/JsonHelpersTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/JsonHelpersTest.java @@ -1,7 +1,7 @@ package com.launchdarkly.sdk.server; import com.google.gson.annotations.JsonAdapter; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import org.junit.Test; diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientBigSegmentsTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientBigSegmentsTest.java index 743c850e0..11c8933a2 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientBigSegmentsTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientBigSegmentsTest.java @@ -1,50 +1,51 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Segment; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.Membership; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.StoreMetadata; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataStore; + +import org.easymock.EasyMockSupport; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; + import static com.launchdarkly.sdk.server.BigSegmentStoreWrapper.hashForUserKey; import static com.launchdarkly.sdk.server.Evaluator.makeBigSegmentRef; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; import static com.launchdarkly.sdk.server.TestComponents.initedDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static com.launchdarkly.sdk.server.TestUtil.upsertSegment; -import static com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.createMembershipFromSegmentRefs; +import static com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.createMembershipFromSegmentRefs; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.isA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import com.launchdarkly.sdk.EvaluationDetail; -import com.launchdarkly.sdk.EvaluationReason.BigSegmentsStatus; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.server.DataModel.FeatureFlag; -import com.launchdarkly.sdk.server.DataModel.Segment; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.Membership; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.StoreMetadata; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataStore; - -import org.easymock.EasyMockSupport; -import org.junit.Before; -import org.junit.Test; - -import java.util.Collections; - @SuppressWarnings("javadoc") public class LDClientBigSegmentsTest extends BaseTest { - private final LDUser user = new LDUser("userkey"); + private final LDContext user = LDContext.create("userkey"); private final Segment bigSegment = segmentBuilder("segmentkey").unbounded(true).generation(1).build(); private final FeatureFlag flag = booleanFlagWithClauses("flagkey", clauseMatchingSegment(bigSegment)); private LDConfig.Builder configBuilder; private BigSegmentStore storeMock; - private BigSegmentStoreFactory storeFactoryMock; + private ComponentConfigurer storeFactoryMock; private final EasyMockSupport mocks = new EasyMockSupport(); + @SuppressWarnings("unchecked") @Before public void setup() { DataStore dataStore = initedDataStore(); @@ -52,10 +53,10 @@ public void setup() { upsertSegment(dataStore, bigSegment); storeMock = mocks.niceMock(BigSegmentStore.class); - storeFactoryMock = mocks.strictMock(BigSegmentStoreFactory.class); - expect(storeFactoryMock.createBigSegmentStore(isA(ClientContext.class))).andReturn(storeMock); + storeFactoryMock = mocks.strictMock(ComponentConfigurer.class); + expect(storeFactoryMock.build(isA(ClientContext.class))).andReturn(storeMock); - configBuilder = baseConfig().dataStore(specificDataStore(dataStore)); + configBuilder = baseConfig().dataStore(specificComponent(dataStore)); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientEndToEndTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientEndToEndTest.java index 6045ea069..36d86754e 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientEndToEndTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientEndToEndTest.java @@ -2,17 +2,20 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.HttpConfigurationFactory; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; import com.launchdarkly.testhelpers.httptest.Handler; import com.launchdarkly.testhelpers.httptest.Handlers; import com.launchdarkly.testhelpers.httptest.HttpServer; import com.launchdarkly.testhelpers.httptest.RequestInfo; +import com.launchdarkly.testhelpers.httptest.SpecialHttpConfigurations; import org.junit.Test; +import java.io.IOException; import java.net.URI; import java.time.Duration; import java.util.concurrent.BlockingQueue; @@ -37,7 +40,7 @@ public class LDClientEndToEndTest extends BaseTest { private static final DataModel.FeatureFlag flag = flagBuilder(flagKey) .offVariation(0).variations(LDValue.of(true)) .build(); - private static final LDUser user = new LDUser("user-key"); + private static final LDContext user = LDContext.create("user-key"); private static Handler makePollingSuccessResponse() { return bodyJson(makeAllDataJson()); @@ -62,7 +65,8 @@ private static Handler makeServiceUnavailableResponse() { public void clientStartsInPollingMode() throws Exception { try (HttpServer server = HttpServer.start(makePollingSuccessResponse())) { LDConfig config = baseConfig() - .dataSource(Components.pollingDataSource().baseURI(server.getUri())) + .serviceEndpoints(Components.serviceEndpoints().polling(server.getUri())) + .dataSource(Components.pollingDataSource()) .events(noEvents()) .build(); @@ -82,9 +86,9 @@ public void clientStartsInPollingModeAfterRecoverableError() throws Exception { try (HttpServer server = HttpServer.start(errorThenSuccess)) { LDConfig config = baseConfig() + .serviceEndpoints(Components.serviceEndpoints().polling(server.getUri())) .dataSource(Components.pollingDataSourceInternal() - .pollIntervalWithNoMinimum(Duration.ofMillis(5)) // use small interval because we expect it to retry - .baseURI(server.getUri())) + .pollIntervalWithNoMinimum(Duration.ofMillis(5))) // use small interval because we expect it to retry .events(noEvents()) .build(); @@ -99,9 +103,9 @@ public void clientStartsInPollingModeAfterRecoverableError() throws Exception { public void clientFailsInPollingModeWith401Error() throws Exception { try (HttpServer server = HttpServer.start(makeInvalidSdkKeyResponse())) { LDConfig config = baseConfig() + .serviceEndpoints(Components.serviceEndpoints().polling(server.getUri())) .dataSource(Components.pollingDataSourceInternal() - .pollIntervalWithNoMinimum(Duration.ofMillis(5)) // use small interval so we'll know if it does not stop permanently - .baseURI(server.getUri())) + .pollIntervalWithNoMinimum(Duration.ofMillis(5))) // use small interval so we'll know if it does not stop permanently .events(noEvents()) .build(); @@ -121,7 +125,8 @@ public void testPollingModeSpecialHttpConfigurations() throws Exception { makePollingSuccessResponse(), (serverUri, httpConfig) -> baseConfig() - .dataSource(Components.pollingDataSource().baseURI(serverUri)) + .serviceEndpoints(Components.serviceEndpoints().polling(serverUri)) + .dataSource(Components.pollingDataSource()) .events(noEvents()) .http(httpConfig)); } @@ -130,7 +135,8 @@ public void testPollingModeSpecialHttpConfigurations() throws Exception { public void clientStartsInStreamingMode() throws Exception { try (HttpServer server = HttpServer.start(makeStreamingSuccessResponse())) { LDConfig config = baseConfig() - .dataSource(Components.streamingDataSource().baseURI(server.getUri())) + .dataSource(Components.streamingDataSource()) + .serviceEndpoints(Components.serviceEndpoints().streaming(server.getUri())) .events(noEvents()) .build(); @@ -150,7 +156,8 @@ public void clientStartsInStreamingModeAfterRecoverableError() throws Exception try (HttpServer server = HttpServer.start(errorThenStream)) { LDConfig config = baseConfig() - .dataSource(Components.streamingDataSource().baseURI(server.getUri()).initialReconnectDelay(Duration.ZERO)) + .serviceEndpoints(Components.serviceEndpoints().streaming(server.getUri())) + .dataSource(Components.streamingDataSource().initialReconnectDelay(Duration.ZERO)) // use zero reconnect delay so we'll know if it does not stop permanently .events(noEvents()) .build(); @@ -170,7 +177,8 @@ public void clientStartsInStreamingModeAfterRecoverableError() throws Exception public void clientFailsInStreamingModeWith401Error() throws Exception { try (HttpServer server = HttpServer.start(makeInvalidSdkKeyResponse())) { LDConfig config = baseConfig() - .dataSource(Components.streamingDataSource().baseURI(server.getUri()).initialReconnectDelay(Duration.ZERO)) + .serviceEndpoints(Components.serviceEndpoints().streaming(server.getUri())) + .dataSource(Components.streamingDataSource().initialReconnectDelay(Duration.ZERO)) // use zero reconnect delay so we'll know if it does not stop permanently .events(noEvents()) .build(); @@ -204,7 +212,8 @@ public void testStreamingModeSpecialHttpConfigurations() throws Exception { makeStreamingSuccessResponse(), (serverUri, httpConfig) -> baseConfig() - .dataSource(Components.streamingDataSource().baseURI(serverUri)) + .serviceEndpoints(Components.serviceEndpoints().streaming(serverUri)) + .dataSource(Components.streamingDataSource()) .events(noEvents()) .http(httpConfig)); } @@ -215,14 +224,15 @@ public void clientSendsAnalyticsEvent() throws Exception { try (HttpServer server = HttpServer.start(resp)) { LDConfig config = baseConfig() + .serviceEndpoints(Components.serviceEndpoints().events(server.getUri())) .dataSource(externalUpdatesOnly()) - .events(Components.sendEvents().baseURI(server.getUri())) .diagnosticOptOut(true) + .events(Components.sendEvents()) .build(); try (LDClient client = new LDClient(sdkKey, config)) { assertTrue(client.isInitialized()); - client.identify(new LDUser("userkey")); + client.identify(user); } RequestInfo req = server.getRecorder().requireRequest(); @@ -236,8 +246,9 @@ public void clientSendsDiagnosticEvent() throws Exception { try (HttpServer server = HttpServer.start(resp)) { LDConfig config = baseConfig() + .serviceEndpoints(Components.serviceEndpoints().events(server.getUri())) .dataSource(externalUpdatesOnly()) - .events(Components.sendEvents().baseURI(server.getUri())) + .events(Components.sendEvents()) .build(); try (LDClient client = new LDClient(sdkKey, config)) { @@ -250,24 +261,21 @@ public void clientSendsDiagnosticEvent() throws Exception { } private static void testWithSpecialHttpConfigurations(Handler handler, - BiFunction makeConfig) throws Exception { - TestHttpUtil.testWithSpecialHttpConfigurations(handler, - (serverUri, httpConfig) -> { - LDConfig config = makeConfig.apply(serverUri, httpConfig) + BiFunction, LDConfig.Builder> makeConfig) throws Exception { + SpecialHttpConfigurations.testAll(handler, + (URI serverUri, SpecialHttpConfigurations.Params params) -> { + LDConfig config = makeConfig.apply(serverUri, TestUtil.makeHttpConfigurationFromTestParams(params)) .startWait(Duration.ofSeconds(10)) // allow extra time to be sure it can connect .build(); try (LDClient client = new LDClient(sdkKey, config)) { - assertTrue(client.isInitialized()); - assertTrue(client.boolVariation(flagKey, user, false)); - } - }, - (serverUri, httpConfig) -> { - LDConfig config = makeConfig.apply(serverUri, httpConfig) - .startWait(Duration.ofMillis(100)) // don't wait terribly long when we don't expect it to succeed - .build(); - try (LDClient client = new LDClient(sdkKey, config)) { - assertFalse(client.isInitialized()); + if (!client.isInitialized()) { + throw new IOException("client did not initialize successfully"); + } + if (!client.boolVariation(flagKey, user, false)) { + throw new IOException("client said it initialized, but did not have correct flag data"); + } } + return true; } ); } diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java index a3c732a90..6b03c49cc 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientEvaluationTest.java @@ -4,11 +4,13 @@ import com.google.gson.Gson; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.integrations.TestData; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import org.junit.Test; @@ -21,7 +23,7 @@ import static com.launchdarkly.sdk.server.Evaluator.EXPECTED_EXCEPTION_FROM_INVALID_FLAG; import static com.launchdarkly.sdk.server.Evaluator.INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION; import static com.launchdarkly.sdk.server.ModelBuilders.booleanFlagWithClauses; -import static com.launchdarkly.sdk.server.ModelBuilders.clause; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingSegment; import static com.launchdarkly.sdk.server.ModelBuilders.fallthroughVariation; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.flagWithValue; @@ -29,8 +31,7 @@ import static com.launchdarkly.sdk.server.TestComponents.dataStoreThatThrowsException; import static com.launchdarkly.sdk.server.TestComponents.failedDataSource; import static com.launchdarkly.sdk.server.TestComponents.initedDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificDataSource; -import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static com.launchdarkly.sdk.server.TestUtil.upsertSegment; import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonEquals; @@ -41,58 +42,156 @@ @SuppressWarnings("javadoc") public class LDClientEvaluationTest extends BaseTest { - private static final LDUser user = new LDUser("userkey"); - private static final LDUser userWithNullKey = new LDUser.Builder((String)null).build(); + private static final LDContext context = LDContext.create("userkey"); + private static final LDUser contextAsUser = new LDUser(context.getKey()); + private static final LDContext invalidContext = LDContext.create(null); + private static final LDUser invalidUser = new LDUser(null); private static final Gson gson = new Gson(); private DataStore dataStore = initedDataStore(); private LDConfig config = baseConfig() - .dataStore(specificDataStore(dataStore)) - .events(Components.noEvents()) - .dataSource(Components.externalUpdatesOnly()) + .dataStore(specificComponent(dataStore)) .build(); private LDClientInterface client = new LDClient("SDK_KEY", config); - - @Test - public void boolVariationReturnsFlagValue() throws Exception { - upsertFlag(dataStore, flagWithValue("key", LDValue.of(true))); - - assertTrue(client.boolVariation("key", user, false)); - } - @Test - public void boolVariationReturnsDefaultValueForUnknownFlag() throws Exception { - assertFalse(client.boolVariation("key", user, false)); - - assertEquals(EvaluationDetail.fromValue(false, NO_VARIATION, - EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)), - client.boolVariationDetail("key", user, false)); - } - - @Test - public void boolVariationReturnsDefaultValueForWrongType() throws Exception { - upsertFlag(dataStore, flagWithValue("key", LDValue.of("wrong"))); - - assertFalse(client.boolVariation("key", user, false)); - - assertEquals(EvaluationDetail.fromValue(false, NO_VARIATION, - EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)), - client.boolVariationDetail("key", user, false)); + public interface EvalMethod { + public ValueT apply(LDClientInterface client, String flagKey, ContextT contextOrUser, ValueT defaultVal); + } + + public interface EvalDetailMethod { + public EvaluationDetail apply(LDClientInterface client, String flagKey, ContextT contextOrUser, ValueT defaultVal); + } + + private void doTypedVariationTests( + EvalMethod variationMethod, + EvalMethod variationForUserMethod, + EvalDetailMethod variationDetailMethod, + EvalDetailMethod variationDetailForUserMethod, + T expectedValue, + LDValue expectedLdValue, + T defaultValue, + LDValue wrongTypeLdValue + ) + { + String flagKey = "flagkey", + wrongTypeFlagKey = "wrongtypekey", + nullValueFlagKey = "nullvaluekey", + unknownKey = "unknownkey"; + + TestData testData = TestData.dataSource(); + testData.update(testData.flag(flagKey).on(true).variations(LDValue.ofNull(), expectedLdValue) + .variationForUser(context.getKey(), 1)); + testData.update(testData.flag(nullValueFlagKey).on(true).variations(LDValue.ofNull()) + .variationForUser(context.getKey(), 0)); + testData.update(testData.flag(wrongTypeFlagKey).on(true).variations(LDValue.ofNull(), wrongTypeLdValue) + .variationForUser(context.getKey(), 1)); + + LDClientInterface client = new LDClient("SDK_KEY", baseConfig().dataSource(testData).build()); + + assertEquals(expectedValue, variationMethod.apply(client, flagKey, context, defaultValue)); + assertEquals(expectedValue, variationForUserMethod.apply(client, flagKey, contextAsUser, defaultValue)); + + assertEquals(EvaluationDetail.fromValue(expectedValue, 1, EvaluationReason.targetMatch()), + variationDetailMethod.apply(client, flagKey, context, defaultValue)); + assertEquals(EvaluationDetail.fromValue(expectedValue, 1, EvaluationReason.targetMatch()), + variationDetailForUserMethod.apply(client, flagKey, contextAsUser, defaultValue)); + + // unknown flag + assertEquals(defaultValue, variationMethod.apply(client, unknownKey, context, defaultValue)); + assertEquals(defaultValue, variationForUserMethod.apply(client, unknownKey, contextAsUser, defaultValue)); + assertEquals(EvaluationDetail.fromValue(defaultValue, -1, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)), + variationDetailMethod.apply(client, unknownKey, context, defaultValue)); + assertEquals(EvaluationDetail.fromValue(defaultValue, -1, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)), + variationDetailForUserMethod.apply(client, unknownKey, contextAsUser, defaultValue)); + + // invalid/null context/user + assertEquals(defaultValue, variationMethod.apply(client, flagKey, invalidContext, defaultValue)); + assertEquals(defaultValue, variationMethod.apply(client, flagKey, null, defaultValue)); + assertEquals(defaultValue, variationForUserMethod.apply(client, flagKey, invalidUser, defaultValue)); + assertEquals(defaultValue, variationForUserMethod.apply(client, flagKey, null, defaultValue)); + assertEquals(EvaluationDetail.fromValue(defaultValue, -1, EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)), + variationDetailMethod.apply(client, flagKey, invalidContext, defaultValue)); + assertEquals(EvaluationDetail.fromValue(defaultValue, -1, EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)), + variationDetailMethod.apply(client, flagKey, null, defaultValue)); + assertEquals(EvaluationDetail.fromValue(defaultValue, -1, EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)), + variationDetailForUserMethod.apply(client, flagKey, invalidUser, defaultValue)); + assertEquals(EvaluationDetail.fromValue(defaultValue, -1, EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)), + variationDetailForUserMethod.apply(client, flagKey, null, defaultValue)); + + // wrong type + if (wrongTypeLdValue != null) + { + assertEquals(defaultValue, variationMethod.apply(client, wrongTypeFlagKey, context, defaultValue)); + assertEquals(defaultValue, variationForUserMethod.apply(client, wrongTypeFlagKey, contextAsUser, defaultValue)); + assertEquals(EvaluationDetail.fromValue(defaultValue, -1, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)), + variationDetailMethod.apply(client, wrongTypeFlagKey, context, defaultValue)); + } + } + + @Test + public void boolEvaluations() { + doTypedVariationTests( + (LDClientInterface c, String f, LDContext ctx, Boolean d) -> c.boolVariation(f, ctx, d), + (LDClientInterface c, String f, LDUser u, Boolean d) -> c.boolVariation(f, u, d), + (LDClientInterface c, String f, LDContext ctx, Boolean d) -> c.boolVariationDetail(f, ctx, d), + (LDClientInterface c, String f, LDUser u, Boolean d) -> c.boolVariationDetail(f, u, d), + true, + LDValue.of(true), + false, + LDValue.of("wrongtype") + ); + } + + @Test + public void intEvaluations() { + doTypedVariationTests( + (LDClientInterface c, String f, LDContext ctx, Integer d) -> c.intVariation(f, ctx, d), + (LDClientInterface c, String f, LDUser u, Integer d) -> c.intVariation(f, u, d), + (LDClientInterface c, String f, LDContext ctx, Integer d) -> c.intVariationDetail(f, ctx, d), + (LDClientInterface c, String f, LDUser u, Integer d) -> c.intVariationDetail(f, u, d), + 2, + LDValue.of(2), + 1, + LDValue.of("wrongtype") + ); + } + + @Test + public void doubleEvaluations() { + doTypedVariationTests( + (LDClientInterface c, String f, LDContext ctx, Double d) -> c.doubleVariation(f, ctx, d), + (LDClientInterface c, String f, LDUser u, Double d) -> c.doubleVariation(f, u, d), + (LDClientInterface c, String f, LDContext ctx, Double d) -> c.doubleVariationDetail(f, ctx, d), + (LDClientInterface c, String f, LDUser u, Double d) -> c.doubleVariationDetail(f, u, d), + 2.5d, + LDValue.of(2.5d), + 1.5d, + LDValue.of("wrongtype") + ); + } + + @Test + public void jsonEvaluations() { + LDValue data = LDValue.buildObject().put("thing", "stuff").build(); + LDValue defaultValue = LDValue.of("default"); + doTypedVariationTests( + (LDClientInterface c, String f, LDContext ctx, LDValue d) -> c.jsonValueVariation(f, ctx, d), + (LDClientInterface c, String f, LDUser u, LDValue d) -> c.jsonValueVariation(f, u, d), + (LDClientInterface c, String f, LDContext ctx, LDValue d) -> c.jsonValueVariationDetail(f, ctx, d), + (LDClientInterface c, String f, LDUser u, LDValue d) -> c.jsonValueVariationDetail(f, u, d), + data, + data, + defaultValue, + null + ); } - @Test - public void intVariationReturnsFlagValue() throws Exception { - upsertFlag(dataStore, flagWithValue("key", LDValue.of(2))); - - assertEquals(2, client.intVariation("key", user, 1)); - } - @Test public void intVariationReturnsFlagValueEvenIfEncodedAsDouble() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of(2.0))); - assertEquals(2, client.intVariation("key", user, 1)); + assertEquals(2, client.intVariation("key", context, 1)); } @Test @@ -102,128 +201,75 @@ public void intVariationFromDoubleRoundsTowardZero() throws Exception { upsertFlag(dataStore, flagWithValue("flag3", LDValue.of(-2.25))); upsertFlag(dataStore, flagWithValue("flag4", LDValue.of(-2.75))); - assertEquals(2, client.intVariation("flag1", user, 1)); - assertEquals(2, client.intVariation("flag2", user, 1)); - assertEquals(-2, client.intVariation("flag3", user, 1)); - assertEquals(-2, client.intVariation("flag4", user, 1)); - } - - @Test - public void intVariationReturnsDefaultValueForUnknownFlag() throws Exception { - assertEquals(1, client.intVariation("key", user, 1)); - - assertEquals(EvaluationDetail.fromValue(1, NO_VARIATION, - EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)), - client.intVariationDetail("key", user, 1)); - } - - @Test - public void intVariationReturnsDefaultValueForWrongType() throws Exception { - upsertFlag(dataStore, flagWithValue("key", LDValue.of("wrong"))); - - assertEquals(1, client.intVariation("key", user, 1)); - - assertEquals(EvaluationDetail.fromValue(1, NO_VARIATION, - EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)), - client.intVariationDetail("key", user, 1)); + assertEquals(2, client.intVariation("flag1", context, 1)); + assertEquals(2, client.intVariation("flag2", context, 1)); + assertEquals(-2, client.intVariation("flag3", context, 1)); + assertEquals(-2, client.intVariation("flag4", context, 1)); } - @Test - public void doubleVariationReturnsFlagValue() throws Exception { - upsertFlag(dataStore, flagWithValue("key", LDValue.of(2.5d))); - - assertEquals(2.5d, client.doubleVariation("key", user, 1.0d), 0d); - } - @Test public void doubleVariationReturnsFlagValueEvenIfEncodedAsInt() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of(2))); - assertEquals(2.0d, client.doubleVariation("key", user, 1.0d), 0d); + assertEquals(2.0d, client.doubleVariation("key", context, 1.0d), 0d); } @Test public void doubleVariationReturnsDefaultValueForUnknownFlag() throws Exception { - assertEquals(1.0d, client.doubleVariation("key", user, 1.0d), 0d); + assertEquals(1.0d, client.doubleVariation("key", context, 1.0d), 0d); assertEquals(EvaluationDetail.fromValue(1.0d, NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)), - client.doubleVariationDetail("key", user, 1.0d)); + client.doubleVariationDetail("key", context, 1.0d)); } @Test public void doubleVariationReturnsDefaultValueForWrongType() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of("wrong"))); - assertEquals(1.0d, client.doubleVariation("key", user, 1.0d), 0d); + assertEquals(1.0d, client.doubleVariation("key", context, 1.0d), 0d); assertEquals(EvaluationDetail.fromValue(1.0d, NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)), - client.doubleVariationDetail("key", user, 1.0d)); + client.doubleVariationDetail("key", context, 1.0d)); } - @Test - public void stringVariationReturnsFlagValue() throws Exception { - upsertFlag(dataStore, flagWithValue("key", LDValue.of("b"))); - - assertEquals("b", client.stringVariation("key", user, "a")); - } - @Test public void stringVariationWithNullDefaultReturnsFlagValue() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of("b"))); - assertEquals("b", client.stringVariation("key", user, null)); - } - - @Test - public void stringVariationReturnsDefaultValueForUnknownFlag() throws Exception { - assertEquals("a", client.stringVariation("key", user, "a")); + assertEquals("b", client.stringVariation("key", context, null)); } @Test public void stringVariationWithNullDefaultReturnsDefaultValueForUnknownFlag() throws Exception { - assertNull(client.stringVariation("key", user, null)); + assertNull(client.stringVariation("key", context, null)); assertEquals(EvaluationDetail.fromValue((String)null, NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)), - client.stringVariationDetail("key", user, null)); + client.stringVariationDetail("key", context, null)); } @Test public void stringVariationReturnsDefaultValueForWrongType() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of(true))); - assertEquals("a", client.stringVariation("key", user, "a")); + assertEquals("a", client.stringVariation("key", context, "a")); assertEquals(EvaluationDetail.fromValue("a", NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)), - client.stringVariationDetail("key", user, "a")); + client.stringVariationDetail("key", context, "a")); } @Test public void stringVariationWithNullDefaultReturnsDefaultValueForWrongType() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of(true))); - assertNull(client.stringVariation("key", user, null)); + assertNull(client.stringVariation("key", context, null)); assertEquals(EvaluationDetail.fromValue((String)null, NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)), - client.stringVariationDetail("key", user, null)); - } - - @Test - public void jsonValueVariationReturnsFlagValue() throws Exception { - LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); - upsertFlag(dataStore, flagWithValue("key", data)); - - assertEquals(data, client.jsonValueVariation("key", user, LDValue.of(42))); - } - - @Test - public void jsonValueVariationReturnsDefaultValueForUnknownFlag() throws Exception { - LDValue defaultVal = LDValue.of(42); - assertEquals(defaultVal, client.jsonValueVariation("key", user, defaultVal)); + client.stringVariationDetail("key", context, null)); } @Test @@ -232,15 +278,15 @@ public void canMatchUserBySegment() throws Exception { // the client is forwarding the Evaluator's segment queries to the data store DataModel.Segment segment = segmentBuilder("segment1") .version(1) - .included(user.getKey()) + .included(context.getKey()) .build(); upsertSegment(dataStore, segment); - DataModel.Clause clause = clause(null, DataModel.Operator.segmentMatch, LDValue.of("segment1")); + DataModel.Clause clause = clauseMatchingSegment("segment1"); DataModel.FeatureFlag feature = booleanFlagWithClauses("feature", clause); upsertFlag(dataStore, feature); - assertTrue(client.boolVariation("feature", user, false)); + assertTrue(client.boolVariation("feature", context, false)); } @Test @@ -248,11 +294,11 @@ public void canTryToMatchUserBySegmentWhenSegmentIsNotFound() throws Exception { // This is similar to EvaluatorSegmentMatchTest, but more end-to-end - we're verifying that // the client is forwarding the Evaluator's segment queries to the data store, and that we // don't blow up if the segment is missing. - DataModel.Clause clause = clause(null, DataModel.Operator.segmentMatch, LDValue.of("segment1")); + DataModel.Clause clause = clauseMatchingSegment("segment1"); DataModel.FeatureFlag feature = booleanFlagWithClauses("feature", clause); upsertFlag(dataStore, feature); - assertFalse(client.boolVariation("feature", user, false)); + assertFalse(client.boolVariation("feature", context, false)); } @Test @@ -261,7 +307,7 @@ public void canGetDetailsForSuccessfulEvaluation() throws Exception { EvaluationDetail expectedResult = EvaluationDetail.fromValue(true, 0, EvaluationReason.off()); - assertEquals(expectedResult, client.boolVariationDetail("key", user, false)); + assertEquals(expectedResult, client.boolVariationDetail("key", context, false)); } @Test @@ -269,7 +315,7 @@ public void jsonVariationReturnsNullIfFlagEvaluatesToNull() { DataModel.FeatureFlag flag = flagBuilder("key").on(false).offVariation(0).variations(LDValue.ofNull()).build(); upsertFlag(dataStore, flag); - assertEquals(LDValue.ofNull(), client.jsonValueVariation("key", user, LDValue.buildObject().build())); + assertEquals(LDValue.ofNull(), client.jsonValueVariation("key", context, LDValue.buildObject().build())); } @Test @@ -277,9 +323,9 @@ public void typedVariationReturnsZeroValueForTypeIfFlagEvaluatesToNull() { DataModel.FeatureFlag flag = flagBuilder("key").on(false).offVariation(0).variations(LDValue.ofNull()).build(); upsertFlag(dataStore, flag); - assertEquals(false, client.boolVariation("key", user, true)); - assertEquals(0, client.intVariation("key", user, 1)); - assertEquals(0d, client.doubleVariation("key", user, 1.0d), 0d); + assertEquals(false, client.boolVariation("key", context, true)); + assertEquals(0, client.intVariation("key", context, 1)); + assertEquals(0d, client.doubleVariation("key", context, 1.0d), 0d); } @Test @@ -289,7 +335,7 @@ public void variationDetailReturnsDefaultIfFlagEvaluatesToNull() { EvaluationDetail expected = EvaluationDetail.fromValue("default", NO_VARIATION, EvaluationReason.off()); - EvaluationDetail actual = client.stringVariationDetail("key", user, "default"); + EvaluationDetail actual = client.stringVariationDetail("key", context, "default"); assertEquals(expected, actual); assertTrue(actual.isDefaultValue()); } @@ -300,21 +346,21 @@ public void deletedFlagPlaceholderIsTreatedAsUnknownFlag() { upsertFlag(dataStore, flag); dataStore.upsert(DataModel.FEATURES, flag.getKey(), ItemDescriptor.deletedItem(flag.getVersion() + 1)); - assertEquals("default", client.stringVariation(flag.getKey(), user, "default")); + assertEquals("default", client.stringVariation(flag.getKey(), context, "default")); } @Test public void appropriateErrorIfClientNotInitialized() throws Exception { DataStore badDataStore = new InMemoryDataStore(); LDConfig badConfig = baseConfig() - .dataStore(specificDataStore(badDataStore)) - .dataSource(specificDataSource(failedDataSource())) + .dataStore(specificComponent(badDataStore)) + .dataSource(specificComponent(failedDataSource())) .startWait(Duration.ZERO) .build(); try (LDClientInterface badClient = new LDClient("SDK_KEY", badConfig)) { EvaluationDetail expectedResult = EvaluationDetail.fromValue(false, NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.CLIENT_NOT_READY)); - assertEquals(expectedResult, badClient.boolVariationDetail("key", user, false)); + assertEquals(expectedResult, badClient.boolVariationDetail("key", context, false)); } } @@ -322,26 +368,16 @@ public void appropriateErrorIfClientNotInitialized() throws Exception { public void appropriateErrorIfFlagDoesNotExist() throws Exception { EvaluationDetail expectedResult = EvaluationDetail.fromValue("default", NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)); - assertEquals(expectedResult, client.stringVariationDetail("key", user, "default")); + assertEquals(expectedResult, client.stringVariationDetail("key", context, "default")); } @Test - public void appropriateErrorIfUserNotSpecified() throws Exception { - upsertFlag(dataStore, flagWithValue("key", LDValue.of(true))); - - EvaluationDetail expectedResult = EvaluationDetail.fromValue("default", NO_VARIATION, - EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)); - assertEquals(expectedResult, client.stringVariationDetail("key", null, "default")); - } - - @Test - public void appropriateErrorIfUserHasNullKey() throws Exception { - LDUser userWithNullKey = new LDUser(null); + public void appropriateErrorIfContextIsInvalid() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of(true))); EvaluationDetail expectedResult = EvaluationDetail.fromValue("default", NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.USER_NOT_SPECIFIED)); - assertEquals(expectedResult, client.stringVariationDetail("key", userWithNullKey, "default")); + assertEquals(expectedResult, client.stringVariationDetail("key", invalidContext, "default")); } @Test @@ -350,7 +386,7 @@ public void appropriateErrorIfValueWrongType() throws Exception { EvaluationDetail expectedResult = EvaluationDetail.fromValue(3, NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)); - assertEquals(expectedResult, client.intVariationDetail("key", user, 3)); + assertEquals(expectedResult, client.intVariationDetail("key", context, 3)); } @Test @@ -358,12 +394,12 @@ public void appropriateErrorForUnexpectedExceptionFromDataStore() throws Excepti RuntimeException exception = new RuntimeException("sorry"); DataStore badDataStore = dataStoreThatThrowsException(exception); LDConfig badConfig = baseConfig() - .dataStore(specificDataStore(badDataStore)) + .dataStore(specificComponent(badDataStore)) .build(); try (LDClientInterface badClient = new LDClient("SDK_KEY", badConfig)) { EvaluationDetail expectedResult = EvaluationDetail.fromValue(false, NO_VARIATION, EvaluationReason.exception(exception)); - assertEquals(expectedResult, badClient.boolVariationDetail("key", user, false)); + assertEquals(expectedResult, badClient.boolVariationDetail("key", context, false)); } } @@ -373,30 +409,22 @@ public void appropriateErrorForUnexpectedExceptionFromFlagEvaluation() throws Ex EvaluationDetail expectedResult = EvaluationDetail.fromValue(false, NO_VARIATION, EvaluationReason.exception(EXPECTED_EXCEPTION_FROM_INVALID_FLAG)); - assertEquals(expectedResult, client.boolVariationDetail(INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION, user, false)); + assertEquals(expectedResult, client.boolVariationDetail(INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION, context, false)); } - @Test - public void canEvaluateWithNonNullButEmptyUserKey() throws Exception { - LDUser userWithEmptyKey = new LDUser(""); - upsertFlag(dataStore, flagWithValue("key", LDValue.of(true))); - - assertEquals(true, client.boolVariation("key", userWithEmptyKey, false)); - } - @Test public void evaluationUsesStoreIfStoreIsInitializedButClientIsNot() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of("value"))); LDConfig customConfig = baseConfig() - .dataStore(specificDataStore(dataStore)) - .dataSource(specificDataSource(failedDataSource())) + .dataStore(specificComponent(dataStore)) + .dataSource(specificComponent(failedDataSource())) .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient("SDK_KEY", customConfig)) { assertFalse(client.isInitialized()); - assertEquals("value", client.stringVariation("key", user, "")); + assertEquals("value", client.stringVariation("key", context, "")); } } @@ -429,7 +457,7 @@ public void allFlagsStateReturnsState() throws Exception { upsertFlag(dataStore, flag2); upsertFlag(dataStore, flag3); - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertTrue(state.isValid()); String json = "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"," + @@ -445,6 +473,9 @@ public void allFlagsStateReturnsState() throws Exception { "\"$valid\":true" + "}"; assertJsonEquals(json, gson.toJson(state)); + + LDUser userWithOldUserType = new LDUser(context.getKey()); + assertJsonEquals(gson.toJson(state), gson.toJson(client.allFlagsState(userWithOldUserType))); } @Test @@ -460,7 +491,7 @@ public void allFlagsStateCanFilterForOnlyClientSideFlags() { upsertFlag(dataStore, flag3); upsertFlag(dataStore, flag4); - FeatureFlagsState state = client.allFlagsState(user, FlagsStateOption.CLIENT_SIDE_ONLY); + FeatureFlagsState state = client.allFlagsState(context, FlagsStateOption.CLIENT_SIDE_ONLY); assertTrue(state.isValid()); Map allValues = state.toValuesMap(); @@ -487,7 +518,7 @@ public void allFlagsStateReturnsStateWithReasons() { upsertFlag(dataStore, flag1); upsertFlag(dataStore, flag2); - FeatureFlagsState state = client.allFlagsState(user, FlagsStateOption.WITH_REASONS); + FeatureFlagsState state = client.allFlagsState(context, FlagsStateOption.WITH_REASONS); assertTrue(state.isValid()); String json = "{\"key1\":\"value1\",\"key2\":\"value2\"," + @@ -532,7 +563,7 @@ public void allFlagsStateCanOmitDetailsForUntrackedFlags() { upsertFlag(dataStore, flag2); upsertFlag(dataStore, flag3); - FeatureFlagsState state = client.allFlagsState(user, FlagsStateOption.WITH_REASONS, FlagsStateOption.DETAILS_ONLY_FOR_TRACKED_FLAGS); + FeatureFlagsState state = client.allFlagsState(context, FlagsStateOption.WITH_REASONS, FlagsStateOption.DETAILS_ONLY_FOR_TRACKED_FLAGS); assertTrue(state.isValid()); String json = "{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"," + @@ -558,7 +589,7 @@ public void allFlagsStateFiltersOutDeletedFlags() throws Exception { upsertFlag(dataStore, flag2); dataStore.upsert(FEATURES, flag2.getKey(), ItemDescriptor.deletedItem(flag2.getVersion() + 1)); - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertTrue(state.isValid()); Map valuesMap = state.toValuesMap(); @@ -567,19 +598,19 @@ public void allFlagsStateFiltersOutDeletedFlags() throws Exception { } @Test - public void allFlagsStateReturnsEmptyStateForNullUser() throws Exception { + public void allFlagsStateReturnsEmptyStateForNullContext() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of("value"))); - FeatureFlagsState state = client.allFlagsState(null); + FeatureFlagsState state = client.allFlagsState((LDContext)null); assertFalse(state.isValid()); assertEquals(0, state.toValuesMap().size()); } @Test - public void allFlagsStateReturnsEmptyStateForNullUserKey() throws Exception { + public void allFlagsStateReturnsEmptyStateForInvalidContext() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of("value"))); - FeatureFlagsState state = client.allFlagsState(userWithNullKey); + FeatureFlagsState state = client.allFlagsState(invalidContext); assertFalse(state.isValid()); assertEquals(0, state.toValuesMap().size()); } @@ -587,12 +618,12 @@ public void allFlagsStateReturnsEmptyStateForNullUserKey() throws Exception { @Test public void allFlagsStateReturnsEmptyStateIfDataStoreThrowsException() throws Exception { LDConfig customConfig = baseConfig() - .dataStore(specificDataStore(TestComponents.dataStoreThatThrowsException(new RuntimeException("sorry")))) + .dataStore(specificComponent(TestComponents.dataStoreThatThrowsException(new RuntimeException("sorry")))) .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient("SDK_KEY", customConfig)) { - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertFalse(state.isValid()); assertEquals(0, state.toValuesMap().size()); } @@ -603,7 +634,7 @@ public void allFlagsStateUsesNullValueForFlagIfEvaluationThrowsException() throw upsertFlag(dataStore, flagWithValue("goodkey", LDValue.of("value"))); upsertFlag(dataStore, flagWithValue(INVALID_FLAG_KEY_THAT_THROWS_EXCEPTION, LDValue.of("nope"))); - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertTrue(state.isValid()); assertEquals(2, state.toValuesMap().size()); assertEquals(LDValue.of("value"), state.getFlagValue("goodkey")); @@ -614,15 +645,15 @@ public void allFlagsStateUsesNullValueForFlagIfEvaluationThrowsException() throw public void allFlagsStateUsesStoreDataIfStoreIsInitializedButClientIsNot() throws Exception { upsertFlag(dataStore, flagWithValue("key", LDValue.of("value"))); LDConfig customConfig = baseConfig() - .dataStore(specificDataStore(dataStore)) - .dataSource(specificDataSource(failedDataSource())) + .dataStore(specificComponent(dataStore)) + .dataSource(specificComponent(failedDataSource())) .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient("SDK_KEY", customConfig)) { assertFalse(client.isInitialized()); - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertTrue(state.isValid()); assertEquals(LDValue.of("value"), state.getFlagValue("key")); } @@ -631,14 +662,14 @@ public void allFlagsStateUsesStoreDataIfStoreIsInitializedButClientIsNot() throw @Test public void allFlagsStateReturnsEmptyStateIfClientAndStoreAreNotInitialized() throws Exception { LDConfig customConfig = baseConfig() - .dataSource(specificDataSource(failedDataSource())) + .dataSource(specificComponent(failedDataSource())) .startWait(Duration.ZERO) .build(); try (LDClient client = new LDClient("SDK_KEY", customConfig)) { assertFalse(client.isInitialized()); - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertFalse(state.isValid()); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java index 8f50f9c47..28859e61c 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientEventTest.java @@ -2,25 +2,28 @@ import com.launchdarkly.sdk.EvaluationReason; import com.launchdarkly.sdk.EvaluationReason.ErrorKind; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.internal.events.Event; +import com.launchdarkly.sdk.server.DataModel.Clause; +import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Prerequisite; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.Event; +import com.launchdarkly.sdk.server.DataModel.Rule; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import com.launchdarkly.sdk.server.subsystems.DataStore; import org.junit.Test; -import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingUser; -import static com.launchdarkly.sdk.server.ModelBuilders.clauseNotMatchingUser; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseMatchingContext; +import static com.launchdarkly.sdk.server.ModelBuilders.clauseNotMatchingContext; import static com.launchdarkly.sdk.server.ModelBuilders.fallthroughVariation; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.flagWithValue; import static com.launchdarkly.sdk.server.ModelBuilders.prerequisite; import static com.launchdarkly.sdk.server.ModelBuilders.ruleBuilder; import static com.launchdarkly.sdk.server.TestComponents.initedDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificEventProcessor; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.allOf; @@ -33,57 +36,77 @@ @SuppressWarnings("javadoc") public class LDClientEventTest extends BaseTest { - private static final LDUser user = new LDUser("userkey"); - private static final LDUser userWithNullKey = new LDUser.Builder((String)null).build(); - private static final LDUser userWithEmptyKey = new LDUser.Builder("").build(); + private static final LDContext context = LDContext.create("userkey"); + private static final LDUser contextAsUser = new LDUser(context.getKey()); + private static final LDContext invalidContext = LDContext.create(null); private DataStore dataStore = initedDataStore(); private TestComponents.TestEventProcessor eventSink = new TestComponents.TestEventProcessor(); private LDConfig config = baseConfig() - .dataStore(specificDataStore(dataStore)) - .events(specificEventProcessor(eventSink)) - .dataSource(Components.externalUpdatesOnly()) + .dataStore(specificComponent(dataStore)) + .events(specificComponent(eventSink)) .build(); private LDClientInterface client = new LDClient("SDK_KEY", config); @Test public void identifySendsEvent() throws Exception { - client.identify(user); + client.identify(context); assertEquals(1, eventSink.events.size()); Event e = eventSink.events.get(0); assertEquals(Event.Identify.class, e.getClass()); Event.Identify ie = (Event.Identify)e; - assertEquals(user.getKey(), ie.getUser().getKey()); + assertEquals(context.getKey(), ie.getContext().getKey()); } @Test - public void identifyWithNullUserDoesNotSendEvent() { - client.identify(null); - assertEquals(0, eventSink.events.size()); + public void identifySendsEventForOldUser() throws Exception { + client.identify(contextAsUser); + + assertEquals(1, eventSink.events.size()); + Event e = eventSink.events.get(0); + assertEquals(Event.Identify.class, e.getClass()); + Event.Identify ie = (Event.Identify)e; + assertEquals(contextAsUser.getKey(), ie.getContext().getKey()); } @Test - public void identifyWithUserWithNoKeyDoesNotSendEvent() { - client.identify(userWithNullKey); + public void identifyWithNullContextOrUserDoesNotSendEvent() { + client.identify((LDContext)null); + assertEquals(0, eventSink.events.size()); + + client.identify((LDUser)null); assertEquals(0, eventSink.events.size()); } @Test - public void identifyWithUserWithEmptyKeyDoesNotSendEvent() { - client.identify(userWithEmptyKey); + public void identifyWithInvalidContextDoesNotSendEvent() { + client.identify(invalidContext); assertEquals(0, eventSink.events.size()); } @Test public void trackSendsEventWithoutData() throws Exception { - client.track("eventkey", user); + client.track("eventkey", context); + + assertEquals(1, eventSink.events.size()); + Event e = eventSink.events.get(0); + assertEquals(Event.Custom.class, e.getClass()); + Event.Custom ce = (Event.Custom)e; + assertEquals(context.getKey(), ce.getContext().getKey()); + assertEquals("eventkey", ce.getKey()); + assertEquals(LDValue.ofNull(), ce.getData()); + } + + @Test + public void trackSendsEventForOldUser() throws Exception { + client.track("eventkey", contextAsUser); assertEquals(1, eventSink.events.size()); Event e = eventSink.events.get(0); assertEquals(Event.Custom.class, e.getClass()); Event.Custom ce = (Event.Custom)e; - assertEquals(user.getKey(), ce.getUser().getKey()); + assertEquals(contextAsUser.getKey(), ce.getContext().getKey()); assertEquals("eventkey", ce.getKey()); assertEquals(LDValue.ofNull(), ce.getData()); } @@ -91,13 +114,27 @@ public void trackSendsEventWithoutData() throws Exception { @Test public void trackSendsEventWithData() throws Exception { LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); - client.trackData("eventkey", user, data); + client.trackData("eventkey", context, data); assertEquals(1, eventSink.events.size()); Event e = eventSink.events.get(0); assertEquals(Event.Custom.class, e.getClass()); Event.Custom ce = (Event.Custom)e; - assertEquals(user.getKey(), ce.getUser().getKey()); + assertEquals(context.getKey(), ce.getContext().getKey()); + assertEquals("eventkey", ce.getKey()); + assertEquals(data, ce.getData()); + } + + @Test + public void trackSendsEventWithDataForOldUser() throws Exception { + LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); + client.trackData("eventkey", contextAsUser, data); + + assertEquals(1, eventSink.events.size()); + Event e = eventSink.events.get(0); + assertEquals(Event.Custom.class, e.getClass()); + Event.Custom ce = (Event.Custom)e; + assertEquals(contextAsUser.getKey(), ce.getContext().getKey()); assertEquals("eventkey", ce.getKey()); assertEquals(data, ce.getData()); } @@ -106,51 +143,58 @@ public void trackSendsEventWithData() throws Exception { public void trackSendsEventWithDataAndMetricValue() throws Exception { LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); double metricValue = 1.5; - client.trackMetric("eventkey", user, data, metricValue); + client.trackMetric("eventkey", context, data, metricValue); assertEquals(1, eventSink.events.size()); Event e = eventSink.events.get(0); assertEquals(Event.Custom.class, e.getClass()); Event.Custom ce = (Event.Custom)e; - assertEquals(user.getKey(), ce.getUser().getKey()); + assertEquals(context.getKey(), ce.getContext().getKey()); assertEquals("eventkey", ce.getKey()); assertEquals(data, ce.getData()); assertEquals(Double.valueOf(metricValue), ce.getMetricValue()); } @Test - public void trackWithNullUserDoesNotSendEvent() { - client.track("eventkey", null); - assertEquals(0, eventSink.events.size()); + public void trackSendsEventWithDataAndMetricValueForOldUser() throws Exception { + LDValue data = LDValue.buildObject().put("thing", LDValue.of("stuff")).build(); + double metricValue = 1.5; + client.trackMetric("eventkey", contextAsUser, data, metricValue); - client.trackData("eventkey", null, LDValue.of(1)); - assertEquals(0, eventSink.events.size()); - - client.trackMetric("eventkey", null, LDValue.of(1), 1.5); - assertEquals(0, eventSink.events.size()); + assertEquals(1, eventSink.events.size()); + Event e = eventSink.events.get(0); + assertEquals(Event.Custom.class, e.getClass()); + Event.Custom ce = (Event.Custom)e; + assertEquals(contextAsUser.getKey(), ce.getContext().getKey()); + assertEquals("eventkey", ce.getKey()); + assertEquals(data, ce.getData()); + assertEquals(Double.valueOf(metricValue), ce.getMetricValue()); } @Test - public void trackWithUserWithNoKeyDoesNotSendEvent() { - client.track("eventkey", userWithNullKey); + public void trackWithNullContextOrUserDoesNotSendEvent() { + client.track("eventkey", (LDContext)null); assertEquals(0, eventSink.events.size()); - - client.trackData("eventkey", userWithNullKey, LDValue.of(1)); + + client.track("eventkey", (LDUser)null); + assertEquals(0, eventSink.events.size()); + + client.trackData("eventkey", (LDContext)null, LDValue.of(1)); assertEquals(0, eventSink.events.size()); - client.trackMetric("eventkey", userWithNullKey, LDValue.of(1), 1.5); + client.trackMetric("eventkey", (LDContext)null, LDValue.of(1), 1.5); assertEquals(0, eventSink.events.size()); } @Test - public void trackWithUserWithEmptyKeyDoesNotSendEvent() { - client.track("eventkey", userWithEmptyKey); + public void trackWithInvalidContextDoesNotSendEvent() { + client.track("eventkey", invalidContext); assertEquals(0, eventSink.events.size()); - client.trackData("eventkey", userWithEmptyKey, LDValue.of(1)); + client.trackData("eventkey", invalidContext, LDValue.of(1)); assertEquals(0, eventSink.events.size()); - client.trackMetric("eventkey", userWithEmptyKey, LDValue.of(1), 1.5); + client.trackMetric("eventkey", invalidContext, LDValue.of(1), 1.5); assertEquals(0, eventSink.events.size()); } @@ -159,14 +203,14 @@ public void boolVariationSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of(true)); upsertFlag(dataStore, flag); - client.boolVariation("key", user, false); + client.boolVariation("key", context, false); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of(true), LDValue.of(false), null, null); } @Test public void boolVariationSendsEventForUnknownFlag() throws Exception { - client.boolVariation("key", user, false); + client.boolVariation("key", context, false); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of(false), null, null); } @@ -176,14 +220,14 @@ public void boolVariationDetailSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of(true)); upsertFlag(dataStore, flag); - client.boolVariationDetail("key", user, false); + client.boolVariationDetail("key", context, false); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of(true), LDValue.of(false), null, EvaluationReason.off()); } @Test public void boolVariationDetailSendsEventForUnknownFlag() throws Exception { - client.boolVariationDetail("key", user, false); + client.boolVariationDetail("key", context, false); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of(false), null, EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND)); @@ -194,14 +238,14 @@ public void intVariationSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of(2)); upsertFlag(dataStore, flag); - client.intVariation("key", user, 1); + client.intVariation("key", context, 1); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of(2), LDValue.of(1), null, null); } @Test public void intVariationSendsEventForUnknownFlag() throws Exception { - client.intVariation("key", user, 1); + client.intVariation("key", context, 1); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of(1), null, null); } @@ -211,14 +255,14 @@ public void intVariationDetailSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of(2)); upsertFlag(dataStore, flag); - client.intVariationDetail("key", user, 1); + client.intVariationDetail("key", context, 1); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of(2), LDValue.of(1), null, EvaluationReason.off()); } @Test public void intVariationDetailSendsEventForUnknownFlag() throws Exception { - client.intVariationDetail("key", user, 1); + client.intVariationDetail("key", context, 1); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of(1), null, EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND)); @@ -229,14 +273,14 @@ public void doubleVariationSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of(2.5d)); upsertFlag(dataStore, flag); - client.doubleVariation("key", user, 1.0d); + client.doubleVariation("key", context, 1.0d); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of(2.5d), LDValue.of(1.0d), null, null); } @Test public void doubleVariationSendsEventForUnknownFlag() throws Exception { - client.doubleVariation("key", user, 1.0d); + client.doubleVariation("key", context, 1.0d); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of(1.0), null, null); } @@ -246,14 +290,14 @@ public void doubleVariationDetailSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of(2.5d)); upsertFlag(dataStore, flag); - client.doubleVariationDetail("key", user, 1.0d); + client.doubleVariationDetail("key", context, 1.0d); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of(2.5d), LDValue.of(1.0d), null, EvaluationReason.off()); } @Test public void doubleVariationDetailSendsEventForUnknownFlag() throws Exception { - client.doubleVariationDetail("key", user, 1.0d); + client.doubleVariationDetail("key", context, 1.0d); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of(1.0), null, EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND)); @@ -264,14 +308,14 @@ public void stringVariationSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of("b")); upsertFlag(dataStore, flag); - client.stringVariation("key", user, "a"); + client.stringVariation("key", context, "a"); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of("b"), LDValue.of("a"), null, null); } @Test public void stringVariationSendsEventForUnknownFlag() throws Exception { - client.stringVariation("key", user, "a"); + client.stringVariation("key", context, "a"); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of("a"), null, null); } @@ -281,14 +325,14 @@ public void stringVariationDetailSendsEvent() throws Exception { DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of("b")); upsertFlag(dataStore, flag); - client.stringVariationDetail("key", user, "a"); + client.stringVariationDetail("key", context, "a"); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, LDValue.of("b"), LDValue.of("a"), null, EvaluationReason.off()); } @Test public void stringVariationDetailSendsEventForUnknownFlag() throws Exception { - client.stringVariationDetail("key", user, "a"); + client.stringVariationDetail("key", context, "a"); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", LDValue.of("a"), null, EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND)); @@ -301,7 +345,7 @@ public void jsonValueVariationDetailSendsEvent() throws Exception { upsertFlag(dataStore, flag); LDValue defaultVal = LDValue.of(42); - client.jsonValueVariationDetail("key", user, defaultVal); + client.jsonValueVariationDetail("key", context, defaultVal); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), flag, data, defaultVal, null, EvaluationReason.off()); } @@ -310,17 +354,35 @@ public void jsonValueVariationDetailSendsEvent() throws Exception { public void jsonValueVariationDetailSendsEventForUnknownFlag() throws Exception { LDValue defaultVal = LDValue.of(42); - client.jsonValueVariationDetail("key", user, defaultVal); + client.jsonValueVariationDetail("key", context, defaultVal); assertEquals(1, eventSink.events.size()); checkUnknownFeatureEvent(eventSink.events.get(0), "key", defaultVal, null, EvaluationReason.error(ErrorKind.FLAG_NOT_FOUND)); } + @Test + public void variationDoesNotSendEventForInvalidContextOrNullUser() throws Exception { + DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of("value")); + upsertFlag(dataStore, flag); + + client.boolVariation(flag.getKey(), invalidContext, false); + assertThat(eventSink.events, empty()); + + client.boolVariation(flag.getKey(), (LDUser)null, false); + assertThat(eventSink.events, empty()); + + client.boolVariationDetail(flag.getKey(), invalidContext, false); + assertThat(eventSink.events, empty()); + + client.boolVariationDetail(flag.getKey(), (LDUser)null, false); + assertThat(eventSink.events, empty()); + } + @Test public void eventTrackingAndReasonCanBeForcedForRule() throws Exception { - DataModel.Clause clause = clauseMatchingUser(user); - DataModel.Rule rule = ruleBuilder().id("id").clauses(clause).variation(1).trackEvents(true).build(); - DataModel.FeatureFlag flag = flagBuilder("flag") + Clause clause = clauseMatchingContext(context); + Rule rule = ruleBuilder().id("id").clauses(clause).variation(1).trackEvents(true).build(); + FeatureFlag flag = flagBuilder("flag") .on(true) .rules(rule) .offVariation(0) @@ -328,7 +390,7 @@ public void eventTrackingAndReasonCanBeForcedForRule() throws Exception { .build(); upsertFlag(dataStore, flag); - client.stringVariation("flag", user, "default"); + client.stringVariation("flag", context, "default"); // Note, we did not call stringVariationDetail and the flag is not tracked, but we should still get // tracking and a reason, because the rule-level trackEvents flag is on for the matched rule. @@ -341,11 +403,11 @@ public void eventTrackingAndReasonCanBeForcedForRule() throws Exception { @Test public void eventTrackingAndReasonAreNotForcedIfFlagIsNotSetForMatchingRule() throws Exception { - DataModel.Clause clause0 = clauseNotMatchingUser(user); - DataModel.Clause clause1 = clauseMatchingUser(user); - DataModel.Rule rule0 = ruleBuilder().id("id0").clauses(clause0).variation(1).trackEvents(true).build(); - DataModel.Rule rule1 = ruleBuilder().id("id1").clauses(clause1).variation(1).trackEvents(false).build(); - DataModel.FeatureFlag flag = flagBuilder("flag") + Clause clause0 = clauseNotMatchingContext(context); + Clause clause1 = clauseMatchingContext(context); + Rule rule0 = ruleBuilder().id("id0").clauses(clause0).variation(1).trackEvents(true).build(); + Rule rule1 = ruleBuilder().id("id1").clauses(clause1).variation(1).trackEvents(false).build(); + FeatureFlag flag = flagBuilder("flag") .on(true) .rules(rule0, rule1) .offVariation(0) @@ -353,7 +415,7 @@ public void eventTrackingAndReasonAreNotForcedIfFlagIsNotSetForMatchingRule() th .build(); upsertFlag(dataStore, flag); - client.stringVariation("flag", user, "default"); + client.stringVariation("flag", context, "default"); // It matched rule1, which has trackEvents: false, so we don't get the override behavior @@ -373,7 +435,7 @@ public void eventTrackingAndReasonCanBeForcedForFallthrough() throws Exception { .build(); upsertFlag(dataStore, flag); - client.stringVariation("flag", user, "default"); + client.stringVariation("flag", context, "default"); // Note, we did not call stringVariationDetail and the flag is not tracked, but we should still get // tracking and a reason, because trackEventsFallthrough is on and the evaluation fell through. @@ -394,7 +456,7 @@ public void eventTrackingAndReasonAreNotForcedForFallthroughIfFlagIsNotSet() thr .build(); upsertFlag(dataStore, flag); - client.stringVariation("flag", user, "default"); + client.stringVariation("flag", context, "default"); assertEquals(1, eventSink.events.size()); Event.FeatureRequest event = (Event.FeatureRequest)eventSink.events.get(0); @@ -413,7 +475,7 @@ public void eventTrackingAndReasonAreNotForcedForFallthroughIfReasonIsNotFallthr .build(); upsertFlag(dataStore, flag); - client.stringVariation("flag", user, "default"); + client.stringVariation("flag", context, "default"); assertEquals(1, eventSink.events.size()); Event.FeatureRequest event = (Event.FeatureRequest)eventSink.events.get(0); @@ -440,7 +502,7 @@ public void eventIsSentForExistingPrererequisiteFlag() throws Exception { upsertFlag(dataStore, f0); upsertFlag(dataStore, f1); - client.stringVariation("feature0", user, "default"); + client.stringVariation("feature0", context, "default"); assertEquals(2, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), f1, LDValue.of("go"), LDValue.ofNull(), "feature0", null); @@ -466,7 +528,7 @@ public void eventIsSentWithReasonForExistingPrererequisiteFlag() throws Exceptio upsertFlag(dataStore, f0); upsertFlag(dataStore, f1); - client.stringVariationDetail("feature0", user, "default"); + client.stringVariationDetail("feature0", context, "default"); assertEquals(2, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), f1, LDValue.of("go"), LDValue.ofNull(), "feature0", EvaluationReason.fallthrough()); @@ -485,7 +547,7 @@ public void eventIsNotSentForUnknownPrererequisiteFlag() throws Exception { .build(); upsertFlag(dataStore, f0); - client.stringVariation("feature0", user, "default"); + client.stringVariation("feature0", context, "default"); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), f0, LDValue.of("off"), LDValue.of("default"), null, null); @@ -503,7 +565,7 @@ public void failureReasonIsGivenForUnknownPrererequisiteFlagIfDetailsWereRequest .build(); upsertFlag(dataStore, f0); - client.stringVariationDetail("feature0", user, "default"); + client.stringVariationDetail("feature0", context, "default"); assertEquals(1, eventSink.events.size()); checkFeatureEvent(eventSink.events.get(0), f0, LDValue.of("off"), LDValue.of("default"), null, @@ -524,7 +586,7 @@ public void identifyWithEventsDisabledDoesNotCauseError() throws Exception { .dataSource(Components.externalUpdatesOnly()) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { - client.identify(user); + client.identify(context); } } @@ -535,7 +597,7 @@ public void trackWithEventsDisabledDoesNotCauseError() throws Exception { .dataSource(Components.externalUpdatesOnly()) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { - client.track("event", user); + client.track("event", context); } } @@ -550,22 +612,6 @@ public void flushWithEventsDisabledDoesNotCauseError() throws Exception { } } - @Test - public void aliasEventIsCorrectlyGenerated() { - LDUser anonymousUser = new LDUser.Builder("anonymous-key").anonymous(true).build(); - - client.alias(user, anonymousUser); - - assertEquals(1, eventSink.events.size()); - Event e = eventSink.events.get(0); - assertEquals(Event.AliasEvent.class, e.getClass()); - Event.AliasEvent evt = (Event.AliasEvent)e; - assertEquals(user.getKey(), evt.getKey()); - assertEquals("user", evt.getContextKind()); - assertEquals(anonymousUser.getKey(), evt.getPreviousKey()); - assertEquals("anonymousUser", evt.getPreviousContextKind()); - } - @Test public void allFlagsStateGeneratesNoEvaluationEvents() { DataModel.FeatureFlag flag = flagBuilder("flag") @@ -577,7 +623,7 @@ public void allFlagsStateGeneratesNoEvaluationEvents() { .build(); upsertFlag(dataStore, flag); - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertThat(state.toValuesMap(), hasKey(flag.getKey())); assertThat(eventSink.events, empty()); @@ -603,7 +649,7 @@ public void allFlagsStateGeneratesNoPrerequisiteEvaluationEvents() { upsertFlag(dataStore, flag1); upsertFlag(dataStore, flag0); - FeatureFlagsState state = client.allFlagsState(user); + FeatureFlagsState state = client.allFlagsState(context); assertThat(state.toValuesMap(), allOf(hasKey(flag0.getKey()), hasKey(flag1.getKey()))); assertThat(eventSink.events, empty()); @@ -614,14 +660,14 @@ private void checkFeatureEvent(Event e, DataModel.FeatureFlag flag, LDValue valu assertEquals(Event.FeatureRequest.class, e.getClass()); Event.FeatureRequest fe = (Event.FeatureRequest)e; assertEquals(flag.getKey(), fe.getKey()); - assertEquals(user.getKey(), fe.getUser().getKey()); + assertEquals(context.getKey(), fe.getContext().getKey()); assertEquals(flag.getVersion(), fe.getVersion()); assertEquals(value, fe.getValue()); assertEquals(defaultVal, fe.getDefaultVal()); assertEquals(prereqOf, fe.getPrereqOf()); assertEquals(reason, fe.getReason()); assertEquals(flag.isTrackEvents(), fe.isTrackEvents()); - assertEquals(flag.getDebugEventsUntilDate() == null ? 0L : flag.getDebugEventsUntilDate().longValue(), fe.getDebugEventsUntilDate()); + assertEquals(flag.getDebugEventsUntilDate(), fe.getDebugEventsUntilDate()); } private void checkUnknownFeatureEvent(Event e, String key, LDValue defaultVal, String prereqOf, @@ -629,7 +675,7 @@ private void checkUnknownFeatureEvent(Event e, String key, LDValue defaultVal, S assertEquals(Event.FeatureRequest.class, e.getClass()); Event.FeatureRequest fe = (Event.FeatureRequest)e; assertEquals(key, fe.getKey()); - assertEquals(user.getKey(), fe.getUser().getKey()); + assertEquals(context.getKey(), fe.getContext().getKey()); assertEquals(-1, fe.getVersion()); assertEquals(-1, fe.getVariation()); assertEquals(defaultVal, fe.getValue()); @@ -637,6 +683,6 @@ private void checkUnknownFeatureEvent(Event e, String key, LDValue defaultVal, S assertEquals(prereqOf, fe.getPrereqOf()); assertEquals(reason, fe.getReason()); assertFalse(fe.isTrackEvents()); - assertEquals(0L, fe.getDebugEventsUntilDate()); + assertNull(fe.getDebugEventsUntilDate()); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java index e3e1658aa..ca91095b5 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientExternalUpdatesOnlyTest.java @@ -1,9 +1,9 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStore; import org.junit.Test; @@ -11,7 +11,7 @@ import static com.launchdarkly.sdk.server.ModelBuilders.flagWithValue; import static com.launchdarkly.sdk.server.TestComponents.initedDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -28,6 +28,17 @@ public void externalUpdatesOnlyClientHasNullDataSource() throws Exception { } } + @Test + public void externalUpdatesOnlyClientHasDefaultEventProcessor() throws Exception { + LDConfig config = baseConfig() + .dataSource(Components.externalUpdatesOnly()) + .events(Components.sendEvents()) + .build(); + try (LDClient client = new LDClient("SDK_KEY", config)) { + assertEquals(DefaultEventProcessorWrapper.class, client.eventProcessor.getClass()); + } + } + @Test public void externalUpdatesOnlyClientIsInitialized() throws Exception { LDConfig config = baseConfig() @@ -45,12 +56,12 @@ public void externalUpdatesOnlyClientGetsFlagFromDataStore() throws IOException DataStore testDataStore = initedDataStore(); LDConfig config = baseConfig() .dataSource(Components.externalUpdatesOnly()) - .dataStore(specificDataStore(testDataStore)) + .dataStore(specificComponent(testDataStore)) .build(); DataModel.FeatureFlag flag = flagWithValue("key", LDValue.of(true)); upsertFlag(testDataStore, flag); try (LDClient client = new LDClient("SDK_KEY", config)) { - assertTrue(client.boolVariation("key", new LDUser("user"), false)); + assertTrue(client.boolVariation("key", LDContext.create("user"), false)); } } } \ No newline at end of file diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientListenersTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientListenersTest.java index 9a13e2232..da871e4b8 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientListenersTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientListenersTest.java @@ -1,21 +1,21 @@ package com.launchdarkly.sdk.server; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.server.TestComponents.DataStoreFactoryThatExposesUpdater; +import com.launchdarkly.sdk.server.TestComponents.ContextCapturingFactory; import com.launchdarkly.sdk.server.integrations.MockPersistentDataStore; import com.launchdarkly.sdk.server.integrations.TestData; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes; -import com.launchdarkly.sdk.server.interfaces.ClientContext; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; import com.launchdarkly.sdk.server.interfaces.FlagValueChangeEvent; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import org.easymock.EasyMockSupport; import org.junit.Test; @@ -27,11 +27,10 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import static com.launchdarkly.sdk.server.TestComponents.specificPersistentDataStore; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.testhelpers.ConcurrentHelpers.assertNoMoreValues; import static com.launchdarkly.testhelpers.ConcurrentHelpers.awaitValue; import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.isA; import static org.easymock.EasyMock.replay; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; @@ -100,8 +99,8 @@ public void clientSendsFlagChangeEvents() throws Exception { @Test public void clientSendsFlagValueChangeEvents() throws Exception { String flagKey = "important-flag"; - LDUser user = new LDUser("important-user"); - LDUser otherUser = new LDUser("unimportant-user"); + LDContext user = LDContext.create("important-user"); + LDContext otherUser = LDContext.create("unimportant-user"); TestData testData = TestData.dataSource(); testData.update(testData.flag(flagKey).on(false)); @@ -210,7 +209,7 @@ public void dataStoreStatusMonitoringIsEnabledForPersistentStore() throws Except LDConfig config = baseConfig() .dataSource(Components.externalUpdatesOnly()) .dataStore( - Components.persistentDataStore(specificPersistentDataStore(new MockPersistentDataStore())) + Components.persistentDataStore(TestComponents.specificComponent(new MockPersistentDataStore())) ) .events(Components.noEvents()) .build(); @@ -221,39 +220,35 @@ public void dataStoreStatusMonitoringIsEnabledForPersistentStore() throws Except @Test public void dataStoreStatusProviderReturnsLatestStatus() throws Exception { - DataStoreFactory underlyingStoreFactory = Components.persistentDataStore( - specificPersistentDataStore(new MockPersistentDataStore())); - DataStoreFactoryThatExposesUpdater factoryWithUpdater = new DataStoreFactoryThatExposesUpdater(underlyingStoreFactory); + ComponentConfigurer underlyingStoreFactory = Components.persistentDataStore( + TestComponents.specificComponent(new MockPersistentDataStore())); + ContextCapturingFactory capturingFactory = new ContextCapturingFactory<>(underlyingStoreFactory); LDConfig config = baseConfig() - .dataSource(Components.externalUpdatesOnly()) - .dataStore(factoryWithUpdater) - .events(Components.noEvents()) + .dataStore(capturingFactory) .build(); try (LDClient client = new LDClient(SDK_KEY, config)) { DataStoreStatusProvider.Status originalStatus = new DataStoreStatusProvider.Status(true, false); DataStoreStatusProvider.Status newStatus = new DataStoreStatusProvider.Status(false, false); assertThat(client.getDataStoreStatusProvider().getStatus(), equalTo(originalStatus)); - factoryWithUpdater.dataStoreUpdates.updateStatus(newStatus); + capturingFactory.clientContext.getDataStoreUpdateSink().updateStatus(newStatus); assertThat(client.getDataStoreStatusProvider().getStatus(), equalTo(newStatus)); } } @Test public void dataStoreStatusProviderSendsStatusUpdates() throws Exception { - DataStoreFactory underlyingStoreFactory = Components.persistentDataStore( - specificPersistentDataStore(new MockPersistentDataStore())); - DataStoreFactoryThatExposesUpdater factoryWithUpdater = new DataStoreFactoryThatExposesUpdater(underlyingStoreFactory); + ComponentConfigurer underlyingStoreFactory = Components.persistentDataStore( + TestComponents.specificComponent(new MockPersistentDataStore())); + ContextCapturingFactory capturingFactory = new ContextCapturingFactory<>(underlyingStoreFactory); LDConfig config = baseConfig() - .dataSource(Components.externalUpdatesOnly()) - .dataStore(factoryWithUpdater) - .events(Components.noEvents()) + .dataStore(capturingFactory) .build(); try (LDClient client = new LDClient(SDK_KEY, config)) { BlockingQueue statuses = new LinkedBlockingQueue<>(); client.getDataStoreStatusProvider().addStatusListener(statuses::add); DataStoreStatusProvider.Status newStatus = new DataStoreStatusProvider.Status(false, false); - factoryWithUpdater.dataStoreUpdates.updateStatus(newStatus); + capturingFactory.clientContext.getDataStoreUpdateSink().updateStatus(newStatus); assertThat(statuses.take(), equalTo(newStatus)); } @@ -308,14 +303,13 @@ public void bigSegmentStoreStatusProviderSendsStatusUpdates() throws Exception { throw new RuntimeException("sorry"); }).anyTimes(); - BigSegmentStoreFactory storeFactoryMock = mocks.strictMock(BigSegmentStoreFactory.class); - expect(storeFactoryMock.createBigSegmentStore(isA(ClientContext.class))).andReturn(storeMock); + ComponentConfigurer storeFactory = specificComponent(storeMock); - replay(storeFactoryMock, storeMock); + replay(storeMock); LDConfig config = baseConfig() .bigSegments( - Components.bigSegments(storeFactoryMock).statusPollInterval(Duration.ofMillis(10)) + Components.bigSegments(storeFactory).statusPollInterval(Duration.ofMillis(10)) ) .build(); diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java index 41679fe15..bf1077a87 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientOfflineTest.java @@ -1,10 +1,10 @@ package com.launchdarkly.sdk.server; import com.google.common.collect.ImmutableMap; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStore; import org.junit.Test; @@ -12,14 +12,14 @@ import static com.launchdarkly.sdk.server.ModelBuilders.flagWithValue; import static com.launchdarkly.sdk.server.TestComponents.initedDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @SuppressWarnings("javadoc") public class LDClientOfflineTest extends BaseTest { - private static final LDUser user = new LDUser("user"); + private static final LDContext user = LDContext.create("user"); @Test public void offlineClientHasNullDataSource() throws IOException { @@ -68,7 +68,7 @@ public void offlineClientGetsFlagsStateFromDataStore() throws IOException { DataStore testDataStore = initedDataStore(); LDConfig config = baseConfig() .offline(true) - .dataStore(specificDataStore(testDataStore)) + .dataStore(specificComponent(testDataStore)) .build(); upsertFlag(testDataStore, flagWithValue("key", LDValue.of(true))); try (LDClient client = new LDClient("SDK_KEY", config)) { diff --git a/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java b/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java index 2f33adeef..c8d62c43c 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDClientTest.java @@ -1,20 +1,18 @@ package com.launchdarkly.sdk.server; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.integrations.MockPersistentDataStore; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataSource; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.EventProcessor; -import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory; import com.launchdarkly.sdk.server.interfaces.LDClientInterface; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; import org.easymock.Capture; -import org.easymock.EasyMock; import org.easymock.EasyMockSupport; import org.junit.Before; import org.junit.Test; @@ -30,13 +28,10 @@ import static com.launchdarkly.sdk.server.TestComponents.dataStoreThatThrowsException; import static com.launchdarkly.sdk.server.TestComponents.failedDataSource; import static com.launchdarkly.sdk.server.TestComponents.initedDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificDataSource; -import static com.launchdarkly.sdk.server.TestComponents.specificDataStore; -import static com.launchdarkly.sdk.server.TestComponents.specificEventProcessor; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static com.launchdarkly.sdk.server.TestUtil.upsertFlag; import static org.easymock.EasyMock.capture; import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.isA; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; @@ -44,7 +39,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -130,7 +124,7 @@ public void clientHasDefaultEventProcessorWithDefaultConfig() throws Exception { .logging(Components.logging(testLogging)) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { - assertEquals(DefaultEventProcessor.class, client.eventProcessor.getClass()); + assertEquals(DefaultEventProcessorWrapper.class, client.eventProcessor.getClass()); } } @@ -143,7 +137,7 @@ public void clientHasDefaultEventProcessorWithSendEvents() throws Exception { .logging(Components.logging(testLogging)) .build(); try (LDClient client = new LDClient("SDK_KEY", config)) { - assertEquals(DefaultEventProcessor.class, client.eventProcessor.getClass()); + assertEquals(DefaultEventProcessorWrapper.class, client.eventProcessor.getClass()); } } @@ -159,25 +153,9 @@ public void clientHasNullEventProcessorWithNoEvents() throws Exception { } } - @Test - public void canSetCustomEventsEndpoint() throws Exception { - URI eu = URI.create("http://fake"); - LDConfig config = new LDConfig.Builder() - .dataSource(Components.externalUpdatesOnly()) - .serviceEndpoints(Components.serviceEndpoints().events(eu)) - .events(Components.sendEvents()) - .diagnosticOptOut(true) - .logging(Components.logging(testLogging)) - .build(); - try (LDClient client = new LDClient(SDK_KEY, config)) { - assertEquals(eu, ((DefaultEventProcessor) client.eventProcessor).dispatcher.eventsConfig.eventsUri); - } - } - @Test public void streamingClientHasStreamProcessor() throws Exception { LDConfig config = new LDConfig.Builder() - .dataSource(Components.streamingDataSource()) .serviceEndpoints(Components.serviceEndpoints().streaming("http://fake")) .events(Components.noEvents()) .logging(Components.logging(testLogging)) @@ -232,34 +210,33 @@ public void canSetCustomPollingEndpoint() throws Exception { } @Test - public void sameDiagnosticAccumulatorPassedToFactoriesWhenSupported() throws IOException { - DataSourceFactory mockDataSourceFactory = mocks.createStrictMock(DataSourceFactory.class); + public void sameDiagnosticStorePassedToFactoriesWhenSupported() throws IOException { + @SuppressWarnings("unchecked") + ComponentConfigurer mockDataSourceFactory = mocks.createStrictMock(ComponentConfigurer.class); LDConfig config = new LDConfig.Builder() - .dataSource(mockDataSourceFactory) .serviceEndpoints(Components.serviceEndpoints().events("fake-host")) // event processor will try to send a diagnostic event here + .dataSource(mockDataSourceFactory) .events(Components.sendEvents()) .logging(Components.logging(testLogging)) .startWait(Duration.ZERO) .build(); Capture capturedDataSourceContext = Capture.newInstance(); - expect(mockDataSourceFactory.createDataSource(capture(capturedDataSourceContext), - isA(DataSourceUpdates.class))).andReturn(failedDataSource()); + expect(mockDataSourceFactory.build(capture(capturedDataSourceContext))).andReturn(failedDataSource()); mocks.replayAll(); try (LDClient client = new LDClient(SDK_KEY, config)) { mocks.verifyAll(); - DiagnosticAccumulator acc = ((DefaultEventProcessor)client.eventProcessor).dispatcher.diagnosticAccumulator; - assertNotNull(acc); - assertSame(acc, ClientContextImpl.get(capturedDataSourceContext.getValue()).diagnosticAccumulator); + assertNotNull(ClientContextImpl.get(capturedDataSourceContext.getValue()).diagnosticStore); } } @Test - public void nullDiagnosticAccumulatorPassedToFactoriesWhenOptedOut() throws IOException { - DataSourceFactory mockDataSourceFactory = mocks.createStrictMock(DataSourceFactory.class); + public void nullDiagnosticStorePassedToFactoriesWhenOptedOut() throws IOException { + @SuppressWarnings("unchecked") + ComponentConfigurer mockDataSourceFactory = mocks.createStrictMock(ComponentConfigurer.class); LDConfig config = new LDConfig.Builder() .dataSource(mockDataSourceFactory) @@ -269,45 +246,13 @@ public void nullDiagnosticAccumulatorPassedToFactoriesWhenOptedOut() throws IOEx .build(); Capture capturedDataSourceContext = Capture.newInstance(); - expect(mockDataSourceFactory.createDataSource(capture(capturedDataSourceContext), - isA(DataSourceUpdates.class))).andReturn(failedDataSource()); - - mocks.replayAll(); - - try (LDClient client = new LDClient(SDK_KEY, config)) { - mocks.verifyAll(); - assertNull(((DefaultEventProcessor)client.eventProcessor).dispatcher.diagnosticAccumulator); - assertNull(ClientContextImpl.get(capturedDataSourceContext.getValue()).diagnosticAccumulator); - } - } - - @Test - public void nullDiagnosticAccumulatorPassedToUpdateFactoryWhenEventProcessorDoesNotSupportDiagnostics() throws IOException { - EventProcessor mockEventProcessor = mocks.createStrictMock(EventProcessor.class); - mockEventProcessor.close(); - EasyMock.expectLastCall().anyTimes(); - EventProcessorFactory mockEventProcessorFactory = mocks.createStrictMock(EventProcessorFactory.class); - DataSourceFactory mockDataSourceFactory = mocks.createStrictMock(DataSourceFactory.class); - - LDConfig config = new LDConfig.Builder() - .events(mockEventProcessorFactory) - .dataSource(mockDataSourceFactory) - .logging(Components.logging(testLogging)) - .startWait(Duration.ZERO) - .build(); - - Capture capturedEventContext = Capture.newInstance(); - Capture capturedDataSourceContext = Capture.newInstance(); - expect(mockEventProcessorFactory.createEventProcessor(capture(capturedEventContext))).andReturn(mockEventProcessor); - expect(mockDataSourceFactory.createDataSource(capture(capturedDataSourceContext), - isA(DataSourceUpdates.class))).andReturn(failedDataSource()); + expect(mockDataSourceFactory.build(capture(capturedDataSourceContext))).andReturn(failedDataSource()); mocks.replayAll(); try (LDClient client = new LDClient(SDK_KEY, config)) { mocks.verifyAll(); - assertNull(ClientContextImpl.get(capturedEventContext.getValue()).diagnosticAccumulator); - assertNull(ClientContextImpl.get(capturedDataSourceContext.getValue()).diagnosticAccumulator); + assertNull(ClientContextImpl.get(capturedDataSourceContext.getValue()).diagnosticStore); } } @@ -394,7 +339,7 @@ public void isFlagKnownReturnsTrueForExistingFlag() throws Exception { DataStore testDataStore = initedDataStore(); LDConfig.Builder config = new LDConfig.Builder() .startWait(Duration.ZERO) - .dataStore(specificDataStore(testDataStore)); + .dataStore(specificComponent(testDataStore)); expect(dataSource.start()).andReturn(initFuture); expect(dataSource.isInitialized()).andReturn(true).times(1); mocks.replayAll(); @@ -411,7 +356,7 @@ public void isFlagKnownReturnsFalseForUnknownFlag() throws Exception { DataStore testDataStore = initedDataStore(); LDConfig.Builder config = new LDConfig.Builder() .startWait(Duration.ZERO) - .dataStore(specificDataStore(testDataStore)); + .dataStore(specificComponent(testDataStore)); expect(dataSource.start()).andReturn(initFuture); expect(dataSource.isInitialized()).andReturn(true).times(1); mocks.replayAll(); @@ -427,7 +372,7 @@ public void isFlagKnownReturnsFalseIfStoreAndClientAreNotInitialized() throws Ex DataStore testDataStore = new InMemoryDataStore(); LDConfig.Builder config = new LDConfig.Builder() .startWait(Duration.ZERO) - .dataStore(specificDataStore(testDataStore)); + .dataStore(specificComponent(testDataStore)); expect(dataSource.start()).andReturn(initFuture); expect(dataSource.isInitialized()).andReturn(false).times(1); mocks.replayAll(); @@ -444,7 +389,7 @@ public void isFlagKnownUsesStoreIfStoreIsInitializedButClientIsNot() throws Exce DataStore testDataStore = initedDataStore(); LDConfig.Builder config = new LDConfig.Builder() .startWait(Duration.ZERO) - .dataStore(specificDataStore(testDataStore)); + .dataStore(specificComponent(testDataStore)); expect(dataSource.start()).andReturn(initFuture); expect(dataSource.isInitialized()).andReturn(false).times(1); mocks.replayAll(); @@ -461,7 +406,7 @@ public void isFlagKnownCatchesExceptionFromDataStore() throws Exception { DataStore badStore = dataStoreThatThrowsException(new RuntimeException("sorry")); LDConfig.Builder config = new LDConfig.Builder() .startWait(Duration.ZERO) - .dataStore(specificDataStore(badStore)); + .dataStore(specificComponent(badStore)); expect(dataSource.start()).andReturn(initFuture); expect(dataSource.isInitialized()).andReturn(false).times(1); mocks.replayAll(); @@ -502,15 +447,18 @@ public void canGetCacheStatsFromDataStoreStatusProvider() throws Exception { @Test public void testSecureModeHash() throws IOException { setupMockDataSourceToInitialize(true); - LDUser user = new LDUser.Builder("userkey").build(); + LDContext context = LDContext.create("userkey"); + LDUser contextAsUser = new LDUser(context.getKey()); String expectedHash = "c097a70924341660427c2e487b86efee789210f9e6dafc3b5f50e75bc596ff99"; client = createMockClient(new LDConfig.Builder() .startWait(Duration.ZERO)); - assertEquals(expectedHash, client.secureModeHash(user)); + assertEquals(expectedHash, client.secureModeHash(context)); + assertEquals(expectedHash, client.secureModeHash(contextAsUser)); - assertNull(client.secureModeHash(null)); - assertNull(client.secureModeHash(new LDUser(null))); + assertNull(client.secureModeHash((LDContext)null)); + assertNull(client.secureModeHash((LDUser)null)); + assertNull(client.secureModeHash(LDContext.create(null))); // invalid context } private void setupMockDataSourceToInitialize(boolean willInitialize) { @@ -520,8 +468,8 @@ private void setupMockDataSourceToInitialize(boolean willInitialize) { } private LDClient createMockClient(LDConfig.Builder config) { - config.dataSource(specificDataSource(dataSource)); - config.events(specificEventProcessor(eventProcessor)); + config.dataSource(specificComponent(dataSource)); + config.events(specificComponent(eventProcessor)); config.logging(Components.logging(testLogging)); return new LDClient(SDK_KEY, config.build()); } diff --git a/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java b/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java index 830451e41..6084bcca4 100644 --- a/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/LDConfigTest.java @@ -1,22 +1,23 @@ package com.launchdarkly.sdk.server; -import static com.launchdarkly.sdk.server.TestComponents.clientContext; - import com.google.common.collect.ImmutableMap; import com.launchdarkly.sdk.server.integrations.BigSegmentsConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder; import com.launchdarkly.sdk.server.integrations.LoggingConfigurationBuilder; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; -import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.sdk.server.interfaces.LoggingConfiguration; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; import org.junit.Test; import java.time.Duration; +import static com.launchdarkly.sdk.server.TestComponents.clientContext; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -26,28 +27,28 @@ @SuppressWarnings("javadoc") public class LDConfigTest { - private static final BasicConfiguration BASIC_CONFIG = new BasicConfiguration("", false, 0, null, null); + private static final ClientContext BASIC_CONTEXT = new ClientContext(""); @Test public void defaults() { LDConfig config = new LDConfig.Builder().build(); - assertNotNull(config.bigSegmentsConfigBuilder); - assertNull(config.bigSegmentsConfigBuilder.createBigSegmentsConfiguration(clientContext("", config)).getStore()); - assertNotNull(config.dataSourceFactory); - assertEquals(Components.streamingDataSource().getClass(), config.dataSourceFactory.getClass()); - assertNotNull(config.dataStoreFactory); - assertEquals(Components.inMemoryDataStore().getClass(), config.dataStoreFactory.getClass()); + assertNotNull(config.bigSegments); + assertNull(config.bigSegments.build(clientContext("", config)).getStore()); + assertNotNull(config.dataSource); + assertEquals(Components.streamingDataSource().getClass(), config.dataSource.getClass()); + assertNotNull(config.dataStore); + assertEquals(Components.inMemoryDataStore().getClass(), config.dataStore.getClass()); assertFalse(config.diagnosticOptOut); - assertNotNull(config.eventProcessorFactory); - assertEquals(Components.sendEvents().getClass(), config.eventProcessorFactory.getClass()); + assertNotNull(config.events); + assertEquals(Components.sendEvents().getClass(), config.events.getClass()); assertFalse(config.offline); - assertNotNull(config.httpConfigFactory); - HttpConfiguration httpConfig = config.httpConfigFactory.createHttpConfiguration(BASIC_CONFIG); + assertNotNull(config.http); + HttpConfiguration httpConfig = config.http.build(BASIC_CONTEXT); assertEquals(HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT, httpConfig.getConnectTimeout()); - assertNotNull(config.loggingConfigFactory); - LoggingConfiguration loggingConfig = config.loggingConfigFactory.createLoggingConfiguration(BASIC_CONFIG); + assertNotNull(config.logging); + LoggingConfiguration loggingConfig = config.logging.build(BASIC_CONTEXT); assertEquals(LoggingConfigurationBuilder.DEFAULT_LOG_DATA_SOURCE_OUTAGE_AS_ERROR_AFTER, loggingConfig.getLogDataSourceOutageAsErrorAfter()); @@ -59,21 +60,21 @@ public void defaults() { public void bigSegmentsConfigFactory() { BigSegmentsConfigurationBuilder f = Components.bigSegments(null); LDConfig config = new LDConfig.Builder().bigSegments(f).build(); - assertSame(f, config.bigSegmentsConfigBuilder); + assertSame(f, config.bigSegments); } @Test public void dataSourceFactory() { - DataSourceFactory f = TestComponents.specificDataSource(null); + ComponentConfigurer f = specificComponent(null); LDConfig config = new LDConfig.Builder().dataSource(f).build(); - assertSame(f, config.dataSourceFactory); + assertSame(f, config.dataSource); } @Test public void dataStoreFactory() { - DataStoreFactory f = TestComponents.specificDataStore(null); + ComponentConfigurer f = specificComponent(null); LDConfig config = new LDConfig.Builder().dataStore(f).build(); - assertSame(f, config.dataStoreFactory); + assertSame(f, config.dataStore); } @Test @@ -87,22 +88,22 @@ public void diagnosticOptOut() { @Test public void eventProcessorFactory() { - EventProcessorFactory f = TestComponents.specificEventProcessor(null); + ComponentConfigurer f = specificComponent(null); LDConfig config = new LDConfig.Builder().events(f).build(); - assertSame(f, config.eventProcessorFactory); + assertSame(f, config.events); } @Test public void offline() { LDConfig config1 = new LDConfig.Builder().offline(true).build(); assertTrue(config1.offline); - assertSame(Components.externalUpdatesOnly(), config1.dataSourceFactory); - assertSame(Components.noEvents(), config1.eventProcessorFactory); + assertSame(Components.externalUpdatesOnly(), config1.dataSource); + assertSame(Components.noEvents(), config1.events); LDConfig config2 = new LDConfig.Builder().offline(true).dataSource(Components.streamingDataSource()).build(); assertTrue(config2.offline); - assertSame(Components.externalUpdatesOnly(), config2.dataSourceFactory); // offline overrides specified factory - assertSame(Components.noEvents(), config2.eventProcessorFactory); + assertSame(Components.externalUpdatesOnly(), config2.dataSource); // offline overrides specified factory + assertSame(Components.noEvents(), config2.events); LDConfig config3 = new LDConfig.Builder().offline(true).offline(false).build(); assertFalse(config3.offline); // just testing that the setter works for both true and false @@ -113,7 +114,7 @@ public void http() { HttpConfigurationBuilder b = Components.httpConfiguration().connectTimeout(Duration.ofSeconds(9)); LDConfig config = new LDConfig.Builder().http(b).build(); assertEquals(Duration.ofSeconds(9), - config.httpConfigFactory.createHttpConfiguration(BASIC_CONFIG).getConnectTimeout()); + config.http.build(BASIC_CONTEXT).getConnectTimeout()); } @Test @@ -121,7 +122,7 @@ public void logging() { LoggingConfigurationBuilder b = Components.logging().logDataSourceOutageAsErrorAfter(Duration.ofSeconds(9)); LDConfig config = new LDConfig.Builder().logging(b).build(); assertEquals(Duration.ofSeconds(9), - config.loggingConfigFactory.createLoggingConfiguration(BASIC_CONFIG).getLogDataSourceOutageAsErrorAfter()); + config.logging.build(BASIC_CONTEXT).getLogDataSourceOutageAsErrorAfter()); } @Test @@ -142,8 +143,8 @@ public void threadPriority() { @Test public void testHttpDefaults() { LDConfig config = new LDConfig.Builder().build(); - HttpConfiguration hc = config.httpConfigFactory.createHttpConfiguration(BASIC_CONFIG); - HttpConfiguration defaults = Components.httpConfiguration().createHttpConfiguration(BASIC_CONFIG); + HttpConfiguration hc = config.http.build(BASIC_CONTEXT); + HttpConfiguration defaults = Components.httpConfiguration().build(BASIC_CONTEXT); assertEquals(defaults.getConnectTimeout(), hc.getConnectTimeout()); assertNull(hc.getProxy()); assertNull(hc.getProxyAuthentication()); diff --git a/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java b/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java index ad4b1c1f0..90c8b7119 100644 --- a/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java +++ b/src/test/java/com/launchdarkly/sdk/server/ModelBuilders.java @@ -2,12 +2,22 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.server.DataModel.Clause; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; +import com.launchdarkly.sdk.server.DataModel.Operator; +import com.launchdarkly.sdk.server.DataModel.Prerequisite; +import com.launchdarkly.sdk.server.DataModel.Rollout; import com.launchdarkly.sdk.server.DataModel.RolloutKind; +import com.launchdarkly.sdk.server.DataModel.Rule; import com.launchdarkly.sdk.server.DataModel.Segment; +import com.launchdarkly.sdk.server.DataModel.SegmentRule; +import com.launchdarkly.sdk.server.DataModel.SegmentTarget; +import com.launchdarkly.sdk.server.DataModel.Target; +import com.launchdarkly.sdk.server.DataModel.VariationOrRollout; import java.util.ArrayList; import java.util.Arrays; @@ -15,6 +25,8 @@ import java.util.List; import java.util.Set; +import static java.util.Arrays.asList; + @SuppressWarnings("javadoc") public abstract class ModelBuilders { public static FlagBuilder flagBuilder(String key) { @@ -25,7 +37,7 @@ public static FlagBuilder flagBuilder(DataModel.FeatureFlag fromFlag) { return new FlagBuilder(fromFlag); } - public static DataModel.FeatureFlag booleanFlagWithClauses(String key, DataModel.Clause... clauses) { + public static FeatureFlag booleanFlagWithClauses(String key, DataModel.Clause... clauses) { DataModel.Rule rule = ruleBuilder().variation(1).clauses(clauses).build(); return flagBuilder(key) .on(true) @@ -36,7 +48,7 @@ public static DataModel.FeatureFlag booleanFlagWithClauses(String key, DataModel .build(); } - public static DataModel.FeatureFlag flagWithValue(String key, LDValue value) { + public static FeatureFlag flagWithValue(String key, LDValue value) { return flagBuilder(key) .on(false) .offVariation(0) @@ -44,7 +56,7 @@ public static DataModel.FeatureFlag flagWithValue(String key, LDValue value) { .build(); } - public static DataModel.VariationOrRollout fallthroughVariation(int variation) { + public static VariationOrRollout fallthroughVariation(int variation) { return new DataModel.VariationOrRollout(variation, null); } @@ -52,36 +64,64 @@ public static RuleBuilder ruleBuilder() { return new RuleBuilder(); } - public static DataModel.Clause clause(UserAttribute attribute, DataModel.Operator op, boolean negate, LDValue... values) { - return new DataModel.Clause(attribute, op, Arrays.asList(values), negate); + public static Clause clause( + ContextKind contextKind, + AttributeRef attribute, + Operator op, + LDValue... values + ) { + return new Clause(contextKind, attribute, op, Arrays.asList(values), false); } - public static DataModel.Clause clause(UserAttribute attribute, DataModel.Operator op, LDValue... values) { - return clause(attribute, op, false, values); + public static Clause clause(ContextKind contextKind, String attributeName, DataModel.Operator op, LDValue... values) { + return clause(contextKind, AttributeRef.fromLiteral(attributeName), op, values); } - - public static DataModel.Clause clauseMatchingUser(LDUser user) { - return clause(UserAttribute.KEY, DataModel.Operator.in, user.getAttribute(UserAttribute.KEY)); + + public static Clause clause(String attributeName, DataModel.Operator op, LDValue... values) { + return clause(null, attributeName, op, values); + } + + public static Clause clauseMatchingContext(LDContext context) { + if (context.isMultiple()) { + return clauseMatchingContext(context.getIndividualContext(0)); + } + return clause(context.getKind(), AttributeRef.fromLiteral("key"), DataModel.Operator.in, LDValue.of(context.getKey())); } - public static DataModel.Clause clauseNotMatchingUser(LDUser user) { - return clause(UserAttribute.KEY, DataModel.Operator.in, LDValue.of("not-" + user.getKey())); + public static Clause clauseNotMatchingContext(LDContext context) { + return negateClause(clauseMatchingContext(context)); } - public static DataModel.Clause clauseMatchingSegment(Segment segment) { - return clause(null, DataModel.Operator.segmentMatch, LDValue.of(segment.getKey())); + public static Clause clauseMatchingSegment(String... segmentKeys) { + LDValue[] values = new LDValue[segmentKeys.length]; + for (int i = 0; i < segmentKeys.length; i++) { + values[i] = LDValue.of(segmentKeys[i]); + } + return clause(null, (AttributeRef)null, DataModel.Operator.segmentMatch, values); + } + + public static Clause clauseMatchingSegment(Segment segment) { + return clauseMatchingSegment(segment.getKey()); } - public static DataModel.Target target(int variation, String... userKeys) { - return new DataModel.Target(ImmutableSet.copyOf(userKeys), variation); + public static Clause negateClause(Clause clause) { + return new Clause(clause.getContextKind(), clause.getAttribute(), clause.getOp(), clause.getValues(), !clause.isNegate()); + } + + public static Target target(ContextKind contextKind, int variation, String... userKeys) { + return new Target(contextKind, ImmutableSet.copyOf(userKeys), variation); } - public static DataModel.Prerequisite prerequisite(String key, int variation) { + public static Target target(int variation, String... userKeys) { + return target(null, variation, userKeys); + } + + public static Prerequisite prerequisite(String key, int variation) { return new DataModel.Prerequisite(key, variation); } - public static DataModel.Rollout emptyRollout() { - return new DataModel.Rollout(ImmutableList.of(), null, RolloutKind.rollout); + public static Rollout emptyRollout() { + return new DataModel.Rollout(null, ImmutableList.of(), null, RolloutKind.rollout, null); } public static SegmentBuilder segmentBuilder(String key) { @@ -96,11 +136,12 @@ public static class FlagBuilder { private String key; private int version; private boolean on; - private List prerequisites = new ArrayList<>(); + private List prerequisites = new ArrayList<>(); private String salt; - private List targets = new ArrayList<>(); - private List rules = new ArrayList<>(); - private DataModel.VariationOrRollout fallthrough; + private List targets = new ArrayList<>(); + private List contextTargets = new ArrayList<>(); + private List rules = new ArrayList<>(); + private VariationOrRollout fallthrough; private Integer offVariation; private List variations = new ArrayList<>(); private boolean clientSide; @@ -122,6 +163,7 @@ private FlagBuilder(DataModel.FeatureFlag f) { this.prerequisites = f.getPrerequisites(); this.salt = f.getSalt(); this.targets = f.getTargets(); + this.contextTargets = f.getContextTargets(); this.rules = f.getRules(); this.fallthrough = f.getFallthrough(); this.offVariation = f.getOffVariation(); @@ -134,62 +176,90 @@ private FlagBuilder(DataModel.FeatureFlag f) { } } - FlagBuilder version(int version) { + public FlagBuilder version(int version) { this.version = version; return this; } - FlagBuilder on(boolean on) { + public FlagBuilder on(boolean on) { this.on = on; return this; } - FlagBuilder prerequisites(DataModel.Prerequisite... prerequisites) { + public FlagBuilder prerequisites(Prerequisite... prerequisites) { this.prerequisites = Arrays.asList(prerequisites); return this; } - FlagBuilder salt(String salt) { + public FlagBuilder salt(String salt) { this.salt = salt; return this; } - FlagBuilder targets(DataModel.Target... targets) { + public FlagBuilder targets(Target... targets) { this.targets = Arrays.asList(targets); return this; } - FlagBuilder rules(DataModel.Rule... rules) { + public FlagBuilder addTarget(int variation, String... values) { + targets.add(target(variation, values)); + return this; + } + + public FlagBuilder contextTargets(Target... contextTargets) { + this.contextTargets = Arrays.asList(contextTargets); + return this; + } + + public FlagBuilder addContextTarget(ContextKind contextKind, int variation, String... values) { + contextTargets.add(target(contextKind, variation, values)); + return this; + } + + public FlagBuilder rules(Rule... rules) { this.rules = Arrays.asList(rules); return this; } - FlagBuilder fallthroughVariation(int fallthroughVariation) { + public FlagBuilder addRule(Rule rule) { + rules.add(rule); + return this; + } + + public FlagBuilder addRule(String id, int variation, String... clausesAsJson) { + Clause[] clauses = new Clause[clausesAsJson.length]; + for (int i = 0; i < clausesAsJson.length; i++) { + clauses[i] = JsonHelpers.deserialize(clausesAsJson[i], Clause.class); + } + return addRule(ruleBuilder().id(id).variation(variation).clauses(clauses).build()); + } + + public FlagBuilder fallthroughVariation(int fallthroughVariation) { this.fallthrough = new DataModel.VariationOrRollout(fallthroughVariation, null); return this; } - FlagBuilder fallthrough(DataModel.Rollout rollout) { + public FlagBuilder fallthrough(Rollout rollout) { this.fallthrough = new DataModel.VariationOrRollout(null, rollout); return this; } - FlagBuilder fallthrough(DataModel.VariationOrRollout fallthrough) { + public FlagBuilder fallthrough(VariationOrRollout fallthrough) { this.fallthrough = fallthrough; return this; } - FlagBuilder offVariation(Integer offVariation) { + public FlagBuilder offVariation(Integer offVariation) { this.offVariation = offVariation; return this; } - FlagBuilder variations(LDValue... variations) { + public FlagBuilder variations(LDValue... variations) { this.variations = Arrays.asList(variations); return this; } - FlagBuilder variations(boolean... variations) { + public FlagBuilder variations(boolean... variations) { List values = new ArrayList<>(); for (boolean v: variations) { values.add(LDValue.of(v)); @@ -198,7 +268,7 @@ FlagBuilder variations(boolean... variations) { return this; } - FlagBuilder variations(String... variations) { + public FlagBuilder variations(String... variations) { List values = new ArrayList<>(); for (String v: variations) { values.add(LDValue.of(v)); @@ -207,7 +277,7 @@ FlagBuilder variations(String... variations) { return this; } - FlagBuilder generatedVariations(int numVariations) { + public FlagBuilder generatedVariations(int numVariations) { variations.clear(); for (int i = 0; i < numVariations; i++) { variations.add(LDValue.of(i)); @@ -215,38 +285,39 @@ FlagBuilder generatedVariations(int numVariations) { return this; } - FlagBuilder clientSide(boolean clientSide) { + public FlagBuilder clientSide(boolean clientSide) { this.clientSide = clientSide; return this; } - FlagBuilder trackEvents(boolean trackEvents) { + public FlagBuilder trackEvents(boolean trackEvents) { this.trackEvents = trackEvents; return this; } - FlagBuilder trackEventsFallthrough(boolean trackEventsFallthrough) { + public FlagBuilder trackEventsFallthrough(boolean trackEventsFallthrough) { this.trackEventsFallthrough = trackEventsFallthrough; return this; } - FlagBuilder debugEventsUntilDate(Long debugEventsUntilDate) { + public FlagBuilder debugEventsUntilDate(Long debugEventsUntilDate) { this.debugEventsUntilDate = debugEventsUntilDate; return this; } - FlagBuilder deleted(boolean deleted) { + public FlagBuilder deleted(boolean deleted) { this.deleted = deleted; return this; } - FlagBuilder disablePreprocessing(boolean disable) { + public FlagBuilder disablePreprocessing(boolean disable) { this.disablePreprocessing = disable; return this; } - DataModel.FeatureFlag build() { - FeatureFlag flag = new DataModel.FeatureFlag(key, version, on, prerequisites, salt, targets, rules, fallthrough, offVariation, variations, + public FeatureFlag build() { + FeatureFlag flag = new DataModel.FeatureFlag(key, version, on, prerequisites, salt, targets, + contextTargets, rules, fallthrough, offVariation, variations, clientSide, trackEvents, trackEventsFallthrough, debugEventsUntilDate, deleted); if (!disablePreprocessing) { flag.afterDeserialized(); @@ -299,43 +370,67 @@ public static class SegmentBuilder { private String key; private Set included = new HashSet<>(); private Set excluded = new HashSet<>(); + private List includedContexts = new ArrayList<>(); + private List excludedContexts = new ArrayList<>(); private String salt = ""; - private List rules = new ArrayList<>(); + private List rules = new ArrayList<>(); private int version = 0; private boolean deleted; private boolean unbounded; + private ContextKind unboundedContextKind; private Integer generation; + private boolean disablePreprocessing; private SegmentBuilder(String key) { this.key = key; } - private SegmentBuilder(DataModel.Segment from) { + private SegmentBuilder(Segment from) { this.key = from.getKey(); - this.included = ImmutableSet.copyOf(from.getIncluded()); - this.excluded = ImmutableSet.copyOf(from.getExcluded()); + this.included = new HashSet<>(from.getIncluded()); + this.excluded = new HashSet<>(from.getExcluded()); + this.includedContexts = new ArrayList<>(from.getIncludedContexts()); + this.excludedContexts = new ArrayList<>(from.getIncludedContexts()); this.salt = from.getSalt(); - this.rules = ImmutableList.copyOf(from.getRules()); + this.rules = new ArrayList<>(from.getRules()); this.version = from.getVersion(); this.deleted = from.isDeleted(); } - public DataModel.Segment build() { - Segment s = new DataModel.Segment(key, included, excluded, salt, rules, version, deleted, unbounded, generation); - s.afterDeserialized(); + public Segment build() { + Segment s = new Segment(key, included, excluded, includedContexts, excludedContexts, + salt, rules, version, deleted, unbounded, unboundedContextKind, generation); + if (!disablePreprocessing) { + s.afterDeserialized(); + } return s; } + + public SegmentBuilder disablePreprocessing(boolean disable) { + this.disablePreprocessing = disable; + return this; + } public SegmentBuilder included(String... included) { - this.included = ImmutableSet.copyOf(included); + this.included.addAll(asList(included)); return this; } public SegmentBuilder excluded(String... excluded) { - this.excluded = ImmutableSet.copyOf(excluded); + this.excluded.addAll(asList(excluded)); return this; } + public SegmentBuilder includedContexts(ContextKind contextKind, String... keys) { + this.includedContexts.add(new SegmentTarget(contextKind, ImmutableSet.copyOf(keys))); + return this; + } + + public SegmentBuilder excludedContexts(ContextKind contextKind, String... keys) { + this.excludedContexts.add(new SegmentTarget(contextKind, ImmutableSet.copyOf(keys))); + return this; + } + public SegmentBuilder salt(String salt) { this.salt = salt; return this; @@ -361,6 +456,11 @@ public SegmentBuilder unbounded(boolean unbounded) { return this; } + public SegmentBuilder unboundedContextKind(ContextKind unboundedContextKind) { + this.unboundedContextKind = unboundedContextKind; + return this; + } + public SegmentBuilder generation(Integer generation) { this.generation = generation; return this; @@ -370,13 +470,14 @@ public SegmentBuilder generation(Integer generation) { public static class SegmentRuleBuilder { private List clauses = new ArrayList<>(); private Integer weight; - private UserAttribute bucketBy; + private ContextKind rolloutContextKind; + private AttributeRef bucketBy; private SegmentRuleBuilder() { } - public DataModel.SegmentRule build() { - return new DataModel.SegmentRule(clauses, weight, bucketBy); + public SegmentRule build() { + return new SegmentRule(clauses, weight, rolloutContextKind, bucketBy); } public SegmentRuleBuilder clauses(DataModel.Clause... clauses) { @@ -389,7 +490,12 @@ public SegmentRuleBuilder weight(Integer weight) { return this; } - public SegmentRuleBuilder bucketBy(UserAttribute bucketBy) { + public SegmentRuleBuilder rolloutContextKind(ContextKind rolloutContextKind) { + this.rolloutContextKind = rolloutContextKind; + return this; + } + + public SegmentRuleBuilder bucketBy(AttributeRef bucketBy) { this.bucketBy = bucketBy; return this; } diff --git a/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperOtherTest.java b/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperOtherTest.java index b68f60b00..374198b9a 100644 --- a/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperOtherTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperOtherTest.java @@ -3,8 +3,8 @@ import com.launchdarkly.sdk.server.DataStoreTestTypes.TestItem; import com.launchdarkly.sdk.server.integrations.MockPersistentDataStore; import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder.StaleValuesPolicy; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import com.launchdarkly.testhelpers.TypeBehavior; import org.junit.Test; diff --git a/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperTest.java b/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperTest.java index 26341a334..d4cf12c1c 100644 --- a/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/PersistentDataStoreWrapperTest.java @@ -8,9 +8,9 @@ import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; import org.junit.After; import org.junit.Assert; diff --git a/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java b/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java index 10c4b3bbf..21a164e77 100644 --- a/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/PollingProcessorTest.java @@ -6,18 +6,22 @@ import com.launchdarkly.sdk.server.TestComponents.MockDataStoreStatusProvider; import com.launchdarkly.sdk.server.TestUtil.ActionCanThrowAnyException; import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.Status; -import com.launchdarkly.sdk.server.interfaces.DataStore; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; import com.launchdarkly.testhelpers.ConcurrentHelpers; import com.launchdarkly.testhelpers.httptest.Handler; import com.launchdarkly.testhelpers.httptest.Handlers; import com.launchdarkly.testhelpers.httptest.HttpServer; import com.launchdarkly.testhelpers.httptest.RequestContext; +import com.launchdarkly.testhelpers.tcptest.TcpHandler; +import com.launchdarkly.testhelpers.tcptest.TcpHandlers; +import com.launchdarkly.testhelpers.tcptest.TcpServer; import org.junit.Before; import org.junit.Test; @@ -32,7 +36,7 @@ import static com.launchdarkly.sdk.server.TestComponents.clientContext; import static com.launchdarkly.sdk.server.TestComponents.dataStoreThatThrowsException; -import static com.launchdarkly.sdk.server.TestComponents.defaultHttpConfiguration; +import static com.launchdarkly.sdk.server.TestComponents.defaultHttpProperties; import static com.launchdarkly.sdk.server.TestComponents.sharedExecutor; import static com.launchdarkly.sdk.server.TestUtil.assertDataSetEquals; import static com.launchdarkly.sdk.server.TestUtil.requireDataSourceStatus; @@ -61,7 +65,7 @@ public void setup() { } private PollingProcessor makeProcessor(URI baseUri, Duration pollInterval) { - FeatureRequestor requestor = new DefaultFeatureRequestor(defaultHttpConfiguration(), baseUri, testLogger); + FeatureRequestor requestor = new DefaultFeatureRequestor(defaultHttpProperties(), baseUri, testLogger); return new PollingProcessor(requestor, dataSourceUpdates, sharedExecutor, pollInterval, testLogger); } @@ -94,8 +98,8 @@ public void setError(int status) { @Test public void builderHasDefaultConfiguration() throws Exception { - DataSourceFactory f = Components.pollingDataSource(); - try (PollingProcessor pp = (PollingProcessor)f.createDataSource(clientContext(SDK_KEY, baseConfig().build()), null)) { + ComponentConfigurer f = Components.pollingDataSource(); + try (PollingProcessor pp = (PollingProcessor)f.build(clientContext(SDK_KEY, baseConfig().build()))) { assertThat(((DefaultFeatureRequestor)pp.requestor).baseUri, equalTo(StandardEndpoints.DEFAULT_POLLING_BASE_URI)); assertThat(pp.pollInterval, equalTo(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL)); } @@ -103,12 +107,9 @@ public void builderHasDefaultConfiguration() throws Exception { @Test public void builderCanSpecifyConfiguration() throws Exception { - URI uri = URI.create("http://fake"); - DataSourceFactory f = Components.pollingDataSource() - .baseURI(uri) + ComponentConfigurer f = Components.pollingDataSource() .pollInterval(LENGTHY_INTERVAL); - try (PollingProcessor pp = (PollingProcessor)f.createDataSource(clientContext(SDK_KEY, baseConfig().build()), null)) { - assertThat(((DefaultFeatureRequestor)pp.requestor).baseUri, equalTo(uri)); + try (PollingProcessor pp = (PollingProcessor)f.build(clientContext(SDK_KEY, baseConfig().build()))) { assertThat(pp.pollInterval, equalTo(LENGTHY_INTERVAL)); } } @@ -154,22 +155,25 @@ public void testTimeoutFromConnectionProblem() throws Exception { BlockingQueue statuses = new LinkedBlockingQueue<>(); dataSourceUpdates.statusBroadcaster.register(statuses::add); - Handler errorThenSuccess = Handlers.sequential( - Handlers.malformedResponse(), // this will cause an IOException - new TestPollHandler() // it should time out before reaching this - ); + Handler successHandler = new TestPollHandler(); // it should time out before reaching this - try (HttpServer server = HttpServer.start(errorThenSuccess)) { - try (PollingProcessor pollingProcessor = makeProcessor(server.getUri(), LENGTHY_INTERVAL)) { - Future initFuture = pollingProcessor.start(); - ConcurrentHelpers.assertFutureIsNotCompleted(initFuture, 200, TimeUnit.MILLISECONDS); - assertFalse(initFuture.isDone()); - assertFalse(pollingProcessor.isInitialized()); - assertEquals(0, dataSourceUpdates.receivedInits.size()); - - Status status = requireDataSourceStatus(statuses, State.INITIALIZING); - assertNotNull(status.getLastError()); - assertEquals(ErrorKind.NETWORK_ERROR, status.getLastError().getKind()); + try (HttpServer server = HttpServer.start(successHandler)) { + TcpHandler errorThenSuccess = TcpHandlers.sequential( + TcpHandlers.noResponse(), // this will cause an IOException due to closing the connection without a response + TcpHandlers.forwardToPort(server.getPort()) + ); + try (TcpServer forwardingServer = TcpServer.start(errorThenSuccess)) { + try (PollingProcessor pollingProcessor = makeProcessor(forwardingServer.getHttpUri(), LENGTHY_INTERVAL)) { + Future initFuture = pollingProcessor.start(); + ConcurrentHelpers.assertFutureIsNotCompleted(initFuture, 200, TimeUnit.MILLISECONDS); + assertFalse(initFuture.isDone()); + assertFalse(pollingProcessor.isInitialized()); + assertEquals(0, dataSourceUpdates.receivedInits.size()); + + Status status = requireDataSourceStatus(statuses, State.INITIALIZING); + assertNotNull(status.getLastError()); + assertEquals(ErrorKind.NETWORK_ERROR, status.getLastError().getKind()); + } } } } diff --git a/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java b/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java index 687aeacef..3e9c37504 100644 --- a/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/RolloutRandomizationConsistencyTest.java @@ -1,8 +1,7 @@ package com.launchdarkly.sdk.server; import com.launchdarkly.sdk.EvaluationReason; -import com.launchdarkly.sdk.LDUser; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Rollout; import com.launchdarkly.sdk.server.DataModel.RolloutKind; @@ -13,6 +12,7 @@ import java.util.ArrayList; import java.util.List; +import static com.launchdarkly.sdk.server.EvaluatorBucketing.computeBucketValue; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.BASE_EVALUATOR; import static com.launchdarkly.sdk.server.EvaluatorTestUtil.expectNoPrerequisiteEvals; import static org.hamcrest.MatcherAssert.assertThat; @@ -34,10 +34,9 @@ private static Rollout buildRollout(boolean isExperiment, boolean untrackedVaria variations.add(new WeightedVariation(0, 10000, untrackedVariations)); variations.add(new WeightedVariation(1, 20000, untrackedVariations)); variations.add(new WeightedVariation(0, 70000, true)); - UserAttribute bucketBy = UserAttribute.KEY; RolloutKind kind = isExperiment ? RolloutKind.experiment : RolloutKind.rollout; Integer seed = 61; - Rollout rollout = new Rollout(variations, bucketBy, kind, seed); + Rollout rollout = new Rollout(null, variations, null, kind, seed); return rollout; } @@ -48,15 +47,15 @@ public void variationIndexForUserInExperimentTest() { String key = "hashKey"; String salt = "saltyA"; - LDUser user1 = new LDUser("userKeyA"); + LDContext user1 = LDContext.create("userKeyA"); // bucketVal = 0.09801207 assertVariationIndexAndExperimentStateForRollout(0, true, rollout, user1, key, salt); - LDUser user2 = new LDUser("userKeyB"); + LDContext user2 = LDContext.create("userKeyB"); // bucketVal = 0.14483777 assertVariationIndexAndExperimentStateForRollout(1, true, rollout, user2, key, salt); - LDUser user3 = new LDUser("userKeyC"); + LDContext user3 = LDContext.create("userKeyC"); // bucketVal = 0.9242641 assertVariationIndexAndExperimentStateForRollout(0, false, rollout, user3, key, salt); } @@ -65,7 +64,7 @@ private static void assertVariationIndexAndExperimentStateForRollout( int expectedVariation, boolean expectedInExperiment, Rollout rollout, - LDUser user, + LDContext context, String flagKey, String salt ) { @@ -75,7 +74,7 @@ private static void assertVariationIndexAndExperimentStateForRollout( .fallthrough(rollout) .salt(salt) .build(); - EvalResult result = BASE_EVALUATOR.evaluate(flag, user, expectNoPrerequisiteEvals()); + EvalResult result = BASE_EVALUATOR.evaluate(flag, context, expectNoPrerequisiteEvals()); assertThat(result.getVariationIndex(), equalTo(expectedVariation)); assertThat(result.getReason().getKind(), equalTo(EvaluationReason.Kind.FALLTHROUGH)); assertThat(result.getReason().isInExperiment(), equalTo(expectedInExperiment)); @@ -83,16 +82,16 @@ private static void assertVariationIndexAndExperimentStateForRollout( @Test public void bucketUserByKeyTest() { - LDUser user1 = new LDUser("userKeyA"); - Float point1 = EvaluatorBucketing.bucketUser(noSeed, user1, "hashKey", UserAttribute.KEY, "saltyA"); + LDContext user1 = LDContext.create("userKeyA"); + float point1 = computeBucketValue(false, noSeed, user1, null, "hashKey", null, "saltyA"); assertEquals(0.42157587, point1, 0.0000001); - LDUser user2 = new LDUser("userKeyB"); - Float point2 = EvaluatorBucketing.bucketUser(noSeed, user2, "hashKey", UserAttribute.KEY, "saltyA"); + LDContext user2 = LDContext.create("userKeyB"); + float point2 = computeBucketValue(false, noSeed, user2, null, "hashKey", null, "saltyA"); assertEquals(0.6708485, point2, 0.0000001); - LDUser user3 = new LDUser("userKeyC"); - Float point3 = EvaluatorBucketing.bucketUser(noSeed, user3, "hashKey", UserAttribute.KEY, "saltyA"); + LDContext user3 = LDContext.create("userKeyC"); + float point3 = computeBucketValue(false, noSeed, user3, null, "hashKey", null, "saltyA"); assertEquals(0.10343106, point3, 0.0000001); } @@ -100,16 +99,16 @@ public void bucketUserByKeyTest() { public void bucketUserWithSeedTest() { Integer seed = 61; - LDUser user1 = new LDUser("userKeyA"); - Float point1 = EvaluatorBucketing.bucketUser(seed, user1, "hashKey", UserAttribute.KEY, "saltyA"); + LDContext user1 = LDContext.create("userKeyA"); + Float point1 = computeBucketValue(true, seed, user1, null, "hashKey", null, "saltyA"); assertEquals(0.09801207, point1, 0.0000001); - LDUser user2 = new LDUser("userKeyB"); - Float point2 = EvaluatorBucketing.bucketUser(seed, user2, "hashKey", UserAttribute.KEY, "saltyA"); + LDContext user2 = LDContext.create("userKeyB"); + Float point2 = computeBucketValue(true, seed, user2, null, "hashKey", null, "saltyA"); assertEquals(0.14483777, point2, 0.0000001); - LDUser user3 = new LDUser("userKeyC"); - Float point3 = EvaluatorBucketing.bucketUser(seed, user3, "hashKey", UserAttribute.KEY, "saltyA"); + LDContext user3 = LDContext.create("userKeyC"); + Float point3 = computeBucketValue(true, seed, user3, null, "hashKey", null, "saltyA"); assertEquals(0.9242641, point3, 0.0000001); } diff --git a/src/test/java/com/launchdarkly/sdk/server/DiagnosticEventTest.java b/src/test/java/com/launchdarkly/sdk/server/ServerSideDiagnosticEventsTest.java similarity index 63% rename from src/test/java/com/launchdarkly/sdk/server/DiagnosticEventTest.java rename to src/test/java/com/launchdarkly/sdk/server/ServerSideDiagnosticEventsTest.java index d261cea4e..488beb0cd 100644 --- a/src/test/java/com/launchdarkly/sdk/server/DiagnosticEventTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/ServerSideDiagnosticEventsTest.java @@ -1,70 +1,92 @@ package com.launchdarkly.sdk.server; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.internal.events.DiagnosticStore; import com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataSource; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; -import com.launchdarkly.sdk.server.interfaces.DataStoreUpdates; -import com.launchdarkly.sdk.server.interfaces.DiagnosticDescription; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStore; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DiagnosticDescription; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import org.junit.Test; import java.net.URI; import java.time.Duration; -import java.util.Collections; -import java.util.List; -import java.util.UUID; import static com.launchdarkly.sdk.server.TestComponents.clientContext; +import static com.launchdarkly.sdk.server.TestUtil.assertJsonEquals; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonEqualsValue; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonProperty; +import static com.launchdarkly.testhelpers.JsonAssertions.jsonUndefined; +import static com.launchdarkly.testhelpers.JsonTestValue.jsonFromValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasItem; -import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.allOf; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; @SuppressWarnings("javadoc") -public class DiagnosticEventTest { +public class ServerSideDiagnosticEventsTest { - private static Gson gson = new Gson(); - private static List testStreamInits = Collections.singletonList(new DiagnosticEvent.StreamInit(1500, 100, true)); private static final URI CUSTOM_URI = URI.create("http://1.1.1.1"); @Test - public void testSerialization() { - DiagnosticId diagnosticId = new DiagnosticId("SDK_KEY"); - DiagnosticEvent.Statistics diagnosticStatisticsEvent = new DiagnosticEvent.Statistics(2000, diagnosticId, 1000, 1, 2, 3, testStreamInits); - JsonObject jsonObject = gson.toJsonTree(diagnosticStatisticsEvent).getAsJsonObject(); - assertEquals(8, jsonObject.size()); - assertEquals("diagnostic", diagnosticStatisticsEvent.kind); - assertEquals(2000, jsonObject.getAsJsonPrimitive("creationDate").getAsLong()); - JsonObject idObject = jsonObject.getAsJsonObject("id"); - assertEquals("DK_KEY", idObject.getAsJsonPrimitive("sdkKeySuffix").getAsString()); - // Throws InvalidArgumentException on invalid UUID - @SuppressWarnings("unused") - UUID uuid = UUID.fromString(idObject.getAsJsonPrimitive("diagnosticId").getAsString()); - assertEquals(1000, jsonObject.getAsJsonPrimitive("dataSinceDate").getAsLong()); - assertEquals(1, jsonObject.getAsJsonPrimitive("droppedEvents").getAsLong()); - assertEquals(2, jsonObject.getAsJsonPrimitive("deduplicatedUsers").getAsLong()); - assertEquals(3, jsonObject.getAsJsonPrimitive("eventsInLastBatch").getAsLong()); - JsonArray initsJson = jsonObject.getAsJsonArray("streamInits"); - assertEquals(1, initsJson.size()); - JsonObject initJson = initsJson.get(0).getAsJsonObject(); - assertEquals(1500, initJson.getAsJsonPrimitive("timestamp").getAsInt()); - assertEquals(100, initJson.getAsJsonPrimitive("durationMillis").getAsInt()); - assertTrue(initJson.getAsJsonPrimitive("failed").getAsBoolean()); + public void sdkDataProperties() { + LDValue sdkData = makeSdkData(LDConfig.DEFAULT); + assertThat(jsonFromValue(sdkData), allOf( + jsonProperty("name", jsonEqualsValue("java-server-sdk")), + jsonProperty("version", jsonEqualsValue(Version.SDK_VERSION)), + jsonProperty("wrapperName", jsonUndefined()), + jsonProperty("wrapperVersion", jsonUndefined()) + )); } + @Test + public void sdkDataWrapperProperties() { + LDConfig config1 = new LDConfig.Builder() + .http(Components.httpConfiguration().wrapper("Scala", "0.1.0")) + .build(); + LDValue sdkData1 = makeSdkData(config1); + assertThat(jsonFromValue(sdkData1), allOf( + jsonProperty("wrapperName", jsonEqualsValue("Scala")), + jsonProperty("wrapperVersion", jsonEqualsValue("0.1.0")) + )); + + LDConfig config2 = new LDConfig.Builder() + .http(Components.httpConfiguration().wrapper("Scala", null)) + .build(); + LDValue sdkData2 = makeSdkData(config2); + assertThat(jsonFromValue(sdkData2), allOf( + jsonProperty("wrapperName", jsonEqualsValue("Scala")), + jsonProperty("wrapperVersion", jsonUndefined()) + )); + } + + @Test + public void platformDataOsNames() { + String realOsName = System.getProperty("os.name"); + try { + System.setProperty("os.name", "Mac OS X"); + assertThat(jsonFromValue(makePlatformData()), + jsonProperty("osName", jsonEqualsValue("MacOS"))); + + System.setProperty("os.name", "Windows 10"); + assertThat(jsonFromValue(makePlatformData()), + jsonProperty("osName", jsonEqualsValue("Windows"))); + + System.setProperty("os.name", "Linux"); + assertThat(jsonFromValue(makePlatformData()), + jsonProperty("osName", jsonEqualsValue("Linux"))); + + System.clearProperty("os.name"); + assertThat(jsonFromValue(makePlatformData()), + jsonProperty("osName", jsonUndefined())); + } finally { + System.setProperty("os.name", realOsName); + } + } + private ObjectBuilder expectedDefaultProperties() { return expectedDefaultPropertiesWithoutStreaming() .put("reconnectTimeMillis", 1_000); @@ -81,7 +103,6 @@ private ObjectBuilder expectedDefaultPropertiesWithoutStreaming() { .put("diagnosticRecordingIntervalMillis", 900_000) .put("eventsCapacity", 10_000) .put("eventsFlushIntervalMillis",5_000) - .put("inlineUsersInEvents", false) .put("samplingInterval", 0) .put("socketTimeoutMillis", 10_000) .put("startWaitMillis", 5_000) @@ -93,9 +114,23 @@ private ObjectBuilder expectedDefaultPropertiesWithoutStreaming() { .put("usingRelayDaemon", false); } + private static LDValue makeSdkData(LDConfig config) { + return makeDiagnosticInitEvent(config).get("sdk"); + } + + private static LDValue makePlatformData() { + return makeDiagnosticInitEvent(LDConfig.DEFAULT).get("platform"); + } + private static LDValue makeConfigData(LDConfig config) { + return makeDiagnosticInitEvent(config).get("configuration"); + } + + private static LDValue makeDiagnosticInitEvent(LDConfig config) { ClientContext context = clientContext("SDK_KEY", config); // the SDK key doesn't matter for these tests - return DiagnosticEvent.Init.getConfigurationData(config, context.getBasic(), context.getHttp()); + DiagnosticStore diagnosticStore = new DiagnosticStore( + ServerSideDiagnosticEvents.getSdkDiagnosticParams(context, config)); + return diagnosticStore.getInitEvent().getJsonValue(); } @Test @@ -118,7 +153,7 @@ public void testCustomDiagnosticConfigurationGeneralProperties() { .put("startWaitMillis", 10_000) .build(); - assertEquals(expected, diagnosticJson); + assertJsonEquals(expected, diagnosticJson); } @Test @@ -136,7 +171,7 @@ public void testCustomDiagnosticConfigurationForServiceEndpoints() { .put("customStreamURI", true) .put("customEventsURI", true) .build(); - assertEquals(expected1, makeConfigData(ldConfig1)); + assertJsonEquals(expected1, makeConfigData(ldConfig1)); LDConfig ldConfig2 = new LDConfig.Builder() .serviceEndpoints( @@ -156,7 +191,7 @@ public void testCustomDiagnosticConfigurationForServiceEndpoints() { .put("pollingIntervalMillis", PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL.toMillis()) .put("streamingDisabled", true) .build(); - assertEquals(expected2, makeConfigData(ldConfig2)); + assertJsonEquals(expected2, makeConfigData(ldConfig2)); } @Test @@ -164,37 +199,19 @@ public void testCustomDiagnosticConfigurationForStreaming() { LDConfig ldConfig1 = new LDConfig.Builder() .dataSource( Components.streamingDataSource() - .baseURI(CUSTOM_URI) .initialReconnectDelay(Duration.ofSeconds(2)) ) .build(); LDValue expected1 = expectedDefaultPropertiesWithoutStreaming() - .put("customBaseURI", false) - .put("customStreamURI", true) .put("reconnectTimeMillis", 2_000) .build(); - assertEquals(expected1, makeConfigData(ldConfig1)); + assertJsonEquals(expected1, makeConfigData(ldConfig1)); LDConfig ldConfig2 = new LDConfig.Builder() .dataSource(Components.streamingDataSource()) // no custom base URIs .build(); LDValue expected2 = expectedDefaultProperties().build(); assertEquals(expected2, makeConfigData(ldConfig2)); - - LDConfig ldConfig3 = new LDConfig.Builder() - .dataSource(Components.streamingDataSource().baseURI(StandardEndpoints.DEFAULT_STREAMING_BASE_URI)) // set a URI, but not a custom one - .build(); - LDValue expected3 = expectedDefaultProperties().build(); - assertEquals(expected3, makeConfigData(ldConfig3)); - - LDConfig ldConfig6 = new LDConfig.Builder() - .dataSource(Components.streamingDataSource().baseURI(CUSTOM_URI)) - .build(); - LDValue expected6 = expectedDefaultProperties() - .put("customBaseURI", false) - .put("customStreamURI", true) - .build(); - assertEquals(expected6, makeConfigData(ldConfig6)); } @Test @@ -202,16 +219,14 @@ public void testCustomDiagnosticConfigurationForPolling() { LDConfig ldConfig1 = new LDConfig.Builder() .dataSource( Components.pollingDataSource() - .baseURI(CUSTOM_URI) .pollInterval(Duration.ofSeconds(60)) ) .build(); LDValue expected1 = expectedDefaultPropertiesWithoutStreaming() - .put("customBaseURI", true) .put("pollingIntervalMillis", 60_000) .put("streamingDisabled", true) .build(); - assertEquals(expected1, makeConfigData(ldConfig1)); + assertJsonEquals(expected1, makeConfigData(ldConfig1)); LDConfig ldConfig2 = new LDConfig.Builder() .dataSource(Components.pollingDataSource()) // no custom base URI @@ -220,12 +235,7 @@ public void testCustomDiagnosticConfigurationForPolling() { .put("pollingIntervalMillis", PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL.toMillis()) .put("streamingDisabled", true) .build(); - assertEquals(expected2, makeConfigData(ldConfig2)); - - LDConfig ldConfig3 = new LDConfig.Builder() - .dataSource(Components.pollingDataSource().baseURI(StandardEndpoints.DEFAULT_POLLING_BASE_URI)) // set a URI, but not a custom one - .build(); - assertEquals(expected2, makeConfigData(ldConfig3)); // result is same as previous test case + assertJsonEquals(expected2, makeConfigData(ldConfig2)); } @Test @@ -234,25 +244,25 @@ public void testCustomDiagnosticConfigurationForCustomDataStore() { .dataStore(new DataStoreFactoryWithDiagnosticDescription(LDValue.of("my-test-store"))) .build(); LDValue expected1 = expectedDefaultProperties().put("dataStoreType", "my-test-store").build(); - assertEquals(expected1, makeConfigData(ldConfig1)); + assertJsonEquals(expected1, makeConfigData(ldConfig1)); LDConfig ldConfig2 = new LDConfig.Builder() .dataStore(new DataStoreFactoryWithoutDiagnosticDescription()) .build(); LDValue expected2 = expectedDefaultProperties().put("dataStoreType", "custom").build(); - assertEquals(expected2, makeConfigData(ldConfig2)); + assertJsonEquals(expected2, makeConfigData(ldConfig2)); LDConfig ldConfig3 = new LDConfig.Builder() .dataStore(new DataStoreFactoryWithDiagnosticDescription(null)) .build(); LDValue expected3 = expectedDefaultProperties().put("dataStoreType", "custom").build(); - assertEquals(expected3, makeConfigData(ldConfig3)); + assertJsonEquals(expected3, makeConfigData(ldConfig3)); LDConfig ldConfig4 = new LDConfig.Builder() .dataStore(new DataStoreFactoryWithDiagnosticDescription(LDValue.of(4))) .build(); LDValue expected4 = expectedDefaultProperties().put("dataStoreType", "custom").build(); - assertEquals(expected4, makeConfigData(ldConfig4)); + assertJsonEquals(expected4, makeConfigData(ldConfig4)); } @Test @@ -264,7 +274,7 @@ public void testCustomDiagnosticConfigurationForPersistentDataStore() { LDValue diagnosticJson1 = makeConfigData(ldConfig1); LDValue expected1 = expectedDefaultProperties().put("dataStoreType", "my-test-store").build(); - assertEquals(expected1, diagnosticJson1); + assertJsonEquals(expected1, diagnosticJson1); LDConfig ldConfig2 = new LDConfig.Builder() .dataStore(Components.persistentDataStore(new PersistentDataStoreFactoryWithoutComponentName())) @@ -273,7 +283,7 @@ public void testCustomDiagnosticConfigurationForPersistentDataStore() { LDValue diagnosticJson2 = makeConfigData(ldConfig2); LDValue expected2 = expectedDefaultProperties().put("dataStoreType", "custom").build(); - assertEquals(expected2, diagnosticJson2); + assertJsonEquals(expected2, diagnosticJson2); } @Test @@ -282,11 +292,9 @@ public void testCustomDiagnosticConfigurationForEvents() { .events( Components.sendEvents() .allAttributesPrivate(true) - .baseURI(URI.create("http://custom")) .capacity(20_000) .diagnosticRecordingInterval(Duration.ofSeconds(1_800)) .flushInterval(Duration.ofSeconds(10)) - .inlineUsersInEvents(true) .userKeysCapacity(2_000) .userKeysFlushInterval(Duration.ofSeconds(600)) ) @@ -295,16 +303,14 @@ public void testCustomDiagnosticConfigurationForEvents() { LDValue diagnosticJson1 = makeConfigData(ldConfig1); LDValue expected1 = expectedDefaultProperties() .put("allAttributesPrivate", true) - .put("customEventsURI", true) .put("diagnosticRecordingIntervalMillis", 1_800_000) .put("eventsCapacity", 20_000) .put("eventsFlushIntervalMillis", 10_000) - .put("inlineUsersInEvents", true) .put("userKeysCapacity", 2_000) .put("userKeysFlushIntervalMillis", 600_000) .build(); - assertEquals(expected1, diagnosticJson1); + assertJsonEquals(expected1, diagnosticJson1); LDConfig ldConfig2 = new LDConfig.Builder() .events(Components.sendEvents()) // no custom base URI @@ -313,14 +319,8 @@ public void testCustomDiagnosticConfigurationForEvents() { LDValue diagnosticJson2 = makeConfigData(ldConfig2); LDValue expected2 = expectedDefaultProperties().build(); - assertEquals(expected2, diagnosticJson2); - - LDConfig ldConfig3 = new LDConfig.Builder() - .events(Components.sendEvents().baseURI(StandardEndpoints.DEFAULT_EVENTS_BASE_URI)) // set a base URI, but not a custom one - .build(); - - assertEquals(expected2, makeConfigData(ldConfig3)); // result is same as previous test case -} + assertJsonEquals(expected2, diagnosticJson2); + } @Test public void testCustomDiagnosticConfigurationForDaemonMode() { @@ -333,7 +333,7 @@ public void testCustomDiagnosticConfigurationForDaemonMode() { .put("usingRelayDaemon", true) .build(); - assertEquals(expected, diagnosticJson); + assertJsonEquals(expected, diagnosticJson); } @Test @@ -356,7 +356,7 @@ public void testCustomDiagnosticConfigurationHttpProperties() { .put("usingProxyAuthenticator", true) .build(); - assertEquals(expected, diagnosticJson); + assertJsonEquals(expected, diagnosticJson); } @Test @@ -369,7 +369,7 @@ public void customComponentCannotInjectUnsupportedConfigProperty() { LDValue diagnosticJson = makeConfigData(config); - assertThat(diagnosticJson.keys(), not(hasItem(unsupportedPropertyName))); + assertThat(jsonFromValue(diagnosticJson), jsonProperty(unsupportedPropertyName, jsonUndefined())); } @Test @@ -381,7 +381,7 @@ public void customComponentCannotInjectSupportedConfigPropertyWithWrongType() { LDValue diagnosticJson = makeConfigData(config); - assertThat(diagnosticJson.keys(), not(hasItem("streamingDisabled"))); + assertThat(jsonFromValue(diagnosticJson), jsonProperty("streamingDisabled", jsonUndefined())); } @Test @@ -396,10 +396,10 @@ public void customComponentDescriptionOfUnsupportedTypeIsIgnored() { LDValue diagnosticJson1 = makeConfigData(config1); LDValue diagnosticJson2 = makeConfigData(config2); - assertEquals(diagnosticJson1, diagnosticJson2); + assertJsonEquals(diagnosticJson1, diagnosticJson2); } - private static class DataSourceFactoryWithDiagnosticDescription implements DataSourceFactory, DiagnosticDescription { + private static class DataSourceFactoryWithDiagnosticDescription implements ComponentConfigurer, DiagnosticDescription { private final LDValue value; DataSourceFactoryWithDiagnosticDescription(LDValue value) { @@ -407,24 +407,24 @@ private static class DataSourceFactoryWithDiagnosticDescription implements DataS } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfig) { + public LDValue describeConfiguration(ClientContext clientContext) { return value; } @Override - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) { + public DataSource build(ClientContext context) { return null; } } - private static class DataSourceFactoryWithoutDiagnosticDescription implements DataSourceFactory { + private static class DataSourceFactoryWithoutDiagnosticDescription implements ComponentConfigurer { @Override - public DataSource createDataSource(ClientContext context, DataSourceUpdates dataSourceUpdates) { + public DataSource build(ClientContext context) { return null; } } - private static class DataStoreFactoryWithDiagnosticDescription implements DataStoreFactory, DiagnosticDescription { + private static class DataStoreFactoryWithDiagnosticDescription implements ComponentConfigurer, DiagnosticDescription { private final LDValue value; DataStoreFactoryWithDiagnosticDescription(LDValue value) { @@ -432,38 +432,38 @@ private static class DataStoreFactoryWithDiagnosticDescription implements DataSt } @Override - public LDValue describeConfiguration(BasicConfiguration basicConfig) { + public LDValue describeConfiguration(ClientContext clientContext) { return value; } @Override - public DataStore createDataStore(ClientContext context, DataStoreUpdates dataStoreUpdates) { + public DataStore build(ClientContext context) { return null; } } - private static class DataStoreFactoryWithoutDiagnosticDescription implements DataStoreFactory { + private static class DataStoreFactoryWithoutDiagnosticDescription implements ComponentConfigurer { @Override - public DataStore createDataStore(ClientContext context, DataStoreUpdates dataStoreUpdates) { + public DataStore build(ClientContext context) { return null; } } - private static class PersistentDataStoreFactoryWithComponentName implements PersistentDataStoreFactory, DiagnosticDescription { + private static class PersistentDataStoreFactoryWithComponentName implements ComponentConfigurer, DiagnosticDescription { @Override - public LDValue describeConfiguration(BasicConfiguration basicConfig) { + public LDValue describeConfiguration(ClientContext clientContext) { return LDValue.of("my-test-store"); } @Override - public PersistentDataStore createPersistentDataStore(ClientContext context) { + public PersistentDataStore build(ClientContext context) { return null; } } - private static class PersistentDataStoreFactoryWithoutComponentName implements PersistentDataStoreFactory { + private static class PersistentDataStoreFactoryWithoutComponentName implements ComponentConfigurer { @Override - public PersistentDataStore createPersistentDataStore(ClientContext context) { + public PersistentDataStore build(ClientContext context) { return null; } } diff --git a/src/test/java/com/launchdarkly/sdk/server/ServerSideEventContextDeduplicatorTest.java b/src/test/java/com/launchdarkly/sdk/server/ServerSideEventContextDeduplicatorTest.java new file mode 100644 index 000000000..7c8e6f129 --- /dev/null +++ b/src/test/java/com/launchdarkly/sdk/server/ServerSideEventContextDeduplicatorTest.java @@ -0,0 +1,66 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.sdk.ContextKind; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.internal.events.EventContextDeduplicator; + +import org.junit.Test; + +import java.time.Duration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +@SuppressWarnings("javadoc") +public class ServerSideEventContextDeduplicatorTest { + private static final Duration LONG_INTERVAL = Duration.ofHours(3); + + @Test + public void configuredFlushIntervalIsReturned() { + EventContextDeduplicator ecd = new ServerSideEventContextDeduplicator(1000, LONG_INTERVAL); + assertThat(ecd.getFlushInterval(), equalTo(LONG_INTERVAL.toMillis())); + } + + @Test + public void singleKindContextKeysAreDeduplicated() { + EventContextDeduplicator ecd = new ServerSideEventContextDeduplicator(1000, LONG_INTERVAL); + + assertThat(ecd.processContext(LDContext.create("a")), is(true)); + assertThat(ecd.processContext(LDContext.create("b")), is(true)); + assertThat(ecd.processContext(LDContext.create("a")), is(false)); + assertThat(ecd.processContext(LDContext.create("c")), is(true)); + assertThat(ecd.processContext(LDContext.create("c")), is(false)); + assertThat(ecd.processContext(LDContext.create("b")), is(false)); + } + + @Test + public void keysAreDisambiguatedByKind() { + EventContextDeduplicator ecd = new ServerSideEventContextDeduplicator(1000, LONG_INTERVAL); + ContextKind kind1 = ContextKind.of("kind1"), kind2 = ContextKind.of("kind2"); + + assertThat(ecd.processContext(LDContext.create(kind1, "a")), is(true)); + assertThat(ecd.processContext(LDContext.create(kind1, "b")), is(true)); + assertThat(ecd.processContext(LDContext.create(kind1, "a")), is(false)); + assertThat(ecd.processContext(LDContext.create(kind2, "a")), is(true)); + assertThat(ecd.processContext(LDContext.create(kind2, "a")), is(false)); + } + + @Test + public void multiKindContextIsDisambiguatedFromSingleKinds() { + // This should work automatically because of the defined behavior of LDContext.fullyQualifiedKey() + EventContextDeduplicator ecd = new ServerSideEventContextDeduplicator(1000, LONG_INTERVAL); + ContextKind kind1 = ContextKind.of("kind1"), kind2 = ContextKind.of("kind2"); + + LDContext c1 = LDContext.create(kind1, "a"); + LDContext c2 = LDContext.create(kind2, "a"); + LDContext mc = LDContext.createMulti(c1, c2); + + assertThat(ecd.processContext(c1), is(true)); + assertThat(ecd.processContext(c2), is(true)); + assertThat(ecd.processContext(c1), is(false)); + assertThat(ecd.processContext(c2), is(false)); + assertThat(ecd.processContext(mc), is(true)); + assertThat(ecd.processContext(mc), is(false)); + } +} diff --git a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorEventsTest.java b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorEventsTest.java index 45c8e9f90..6bc69f76f 100644 --- a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorEventsTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorEventsTest.java @@ -6,9 +6,9 @@ import com.launchdarkly.sdk.server.StreamProcessorEvents.DeleteData; import com.launchdarkly.sdk.server.StreamProcessorEvents.PatchData; import com.launchdarkly.sdk.server.StreamProcessorEvents.PutData; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.SerializationException; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.SerializationException; import org.junit.Test; diff --git a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java index a8b4cd356..00d333088 100644 --- a/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/StreamProcessorTest.java @@ -2,6 +2,8 @@ import com.launchdarkly.eventsource.ConnectionErrorHandler; import com.launchdarkly.eventsource.MessageEvent; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.internal.events.DiagnosticStore; import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Segment; import com.launchdarkly.sdk.server.DataModel.VersionedData; @@ -11,24 +13,30 @@ import com.launchdarkly.sdk.server.TestComponents.MockDataSourceUpdates.UpsertParams; import com.launchdarkly.sdk.server.TestComponents.MockDataStoreStatusProvider; import com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorKind; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.Status; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; import com.launchdarkly.testhelpers.httptest.Handler; import com.launchdarkly.testhelpers.httptest.Handlers; import com.launchdarkly.testhelpers.httptest.HttpServer; import com.launchdarkly.testhelpers.httptest.RequestInfo; +import com.launchdarkly.testhelpers.httptest.SpecialHttpConfigurations; +import com.launchdarkly.testhelpers.tcptest.TcpHandler; +import com.launchdarkly.testhelpers.tcptest.TcpHandlers; +import com.launchdarkly.testhelpers.tcptest.TcpServer; import org.hamcrest.MatcherAssert; import org.junit.Before; import org.junit.Test; import java.io.EOFException; +import java.io.IOException; import java.net.URI; import java.time.Duration; import java.util.Map; @@ -43,11 +51,13 @@ import static com.launchdarkly.sdk.server.DataModel.SEGMENTS; import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; import static com.launchdarkly.sdk.server.ModelBuilders.segmentBuilder; +import static com.launchdarkly.sdk.server.TestComponents.basicDiagnosticStore; import static com.launchdarkly.sdk.server.TestComponents.clientContext; import static com.launchdarkly.sdk.server.TestComponents.dataSourceUpdates; import static com.launchdarkly.sdk.server.TestUtil.requireDataSourceStatus; import static com.launchdarkly.testhelpers.ConcurrentHelpers.assertFutureIsCompleted; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -57,7 +67,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -135,9 +144,9 @@ public void setup() { @Test public void builderHasDefaultConfiguration() throws Exception { - DataSourceFactory f = Components.streamingDataSource(); - try (StreamProcessor sp = (StreamProcessor)f.createDataSource(clientContext(SDK_KEY, LDConfig.DEFAULT), - dataSourceUpdates)) { + ComponentConfigurer f = Components.streamingDataSource(); + try (StreamProcessor sp = (StreamProcessor)f.build(clientContext(SDK_KEY, LDConfig.DEFAULT) + .withDataSourceUpdateSink(dataSourceUpdates))) { assertThat(sp.initialReconnectDelay, equalTo(StreamingDataSourceBuilder.DEFAULT_INITIAL_RECONNECT_DELAY)); assertThat(sp.streamUri, equalTo(StandardEndpoints.DEFAULT_STREAMING_BASE_URI)); } @@ -145,14 +154,11 @@ public void builderHasDefaultConfiguration() throws Exception { @Test public void builderCanSpecifyConfiguration() throws Exception { - URI streamUri = URI.create("http://fake"); - DataSourceFactory f = Components.streamingDataSource() - .baseURI(streamUri) + ComponentConfigurer f = Components.streamingDataSource() .initialReconnectDelay(Duration.ofMillis(5555)); - try (StreamProcessor sp = (StreamProcessor)f.createDataSource(clientContext(SDK_KEY, LDConfig.DEFAULT), - dataSourceUpdates(dataStore))) { + try (StreamProcessor sp = (StreamProcessor)f.build(clientContext(SDK_KEY, LDConfig.DEFAULT) + .withDataSourceUpdateSink(dataSourceUpdates(dataStore)))) { assertThat(sp.initialReconnectDelay, equalTo(Duration.ofMillis(5555))); - assertThat(sp.streamUri, equalTo(streamUri)); } } @@ -354,24 +360,28 @@ public void unknownEventTypeDoesNotCauseError() throws Exception { @Test public void streamWillReconnectAfterGeneralIOException() throws Exception { - Handler errorHandler = Handlers.malformedResponse(); Handler streamHandler = streamResponse(EMPTY_DATA_EVENT); - Handler errorThenSuccess = Handlers.sequential(errorHandler, streamHandler); - try (HttpServer server = HttpServer.start(errorThenSuccess)) { - try (StreamProcessor sp = createStreamProcessor(null, server.getUri())) { - startAndWait(sp); - - assertThat(server.getRecorder().count(), equalTo(2)); - assertThat(dataSourceUpdates.getLastStatus().getLastError(), notNullValue()); - assertThat(dataSourceUpdates.getLastStatus().getLastError().getKind(), equalTo(ErrorKind.NETWORK_ERROR)); + try (HttpServer server = HttpServer.start(streamHandler)) { + TcpHandler errorThenSuccess = TcpHandlers.sequential( + TcpHandlers.noResponse(), // this will cause an IOException due to closing the connection without a response + TcpHandlers.forwardToPort(server.getPort()) + ); + try (TcpServer forwardingServer = TcpServer.start(errorThenSuccess)) { + try (StreamProcessor sp = createStreamProcessor(null, forwardingServer.getHttpUri())) { + startAndWait(sp); + + assertThat(server.getRecorder().count(), equalTo(1)); // the HTTP server doesn't see the initial request that the forwardingServer rejected + assertThat(dataSourceUpdates.getLastStatus().getLastError(), notNullValue()); + assertThat(dataSourceUpdates.getLastStatus().getLastError().getKind(), equalTo(ErrorKind.NETWORK_ERROR)); + } } } } @Test public void streamInitDiagnosticRecordedOnOpen() throws Exception { - DiagnosticAccumulator acc = new DiagnosticAccumulator(new DiagnosticId(SDK_KEY)); + DiagnosticStore acc = basicDiagnosticStore(); long startTime = System.currentTimeMillis(); try (HttpServer server = HttpServer.start(streamResponse(EMPTY_DATA_EVENT))) { @@ -379,20 +389,21 @@ public void streamInitDiagnosticRecordedOnOpen() throws Exception { startAndWait(sp); long timeAfterOpen = System.currentTimeMillis(); - DiagnosticEvent.Statistics event = acc.createEventAndReset(0, 0); - assertEquals(1, event.streamInits.size()); - DiagnosticEvent.StreamInit init = event.streamInits.get(0); - assertFalse(init.failed); - assertThat(init.timestamp, greaterThanOrEqualTo(startTime)); - assertThat(init.timestamp, lessThanOrEqualTo(timeAfterOpen)); - assertThat(init.durationMillis, lessThanOrEqualTo(timeAfterOpen - startTime)); + LDValue event = acc.createEventAndReset(0, 0).getJsonValue(); + LDValue streamInits = event.get("streamInits"); + assertEquals(1, streamInits.size()); + LDValue init = streamInits.get(0); + assertFalse(init.get("failed").booleanValue()); + assertThat(init.get("timestamp").longValue(), + allOf(greaterThanOrEqualTo(startTime), lessThanOrEqualTo(timeAfterOpen))); + assertThat(init.get("durationMillis").longValue(), lessThanOrEqualTo(timeAfterOpen - startTime)); } } } @Test public void streamInitDiagnosticRecordedOnErrorDuringInit() throws Exception { - DiagnosticAccumulator acc = new DiagnosticAccumulator(new DiagnosticId(SDK_KEY)); + DiagnosticStore acc = basicDiagnosticStore(); long startTime = System.currentTimeMillis(); Handler errorHandler = Handlers.status(503); @@ -404,19 +415,20 @@ public void streamInitDiagnosticRecordedOnErrorDuringInit() throws Exception { startAndWait(sp); long timeAfterOpen = System.currentTimeMillis(); - DiagnosticEvent.Statistics event = acc.createEventAndReset(0, 0); + LDValue event = acc.createEventAndReset(0, 0).getJsonValue(); - assertEquals(2, event.streamInits.size()); - DiagnosticEvent.StreamInit init0 = event.streamInits.get(0); - assertTrue(init0.failed); - assertThat(init0.timestamp, greaterThanOrEqualTo(startTime)); - assertThat(init0.timestamp, lessThanOrEqualTo(timeAfterOpen)); - assertThat(init0.durationMillis, lessThanOrEqualTo(timeAfterOpen - startTime)); - - DiagnosticEvent.StreamInit init1 = event.streamInits.get(1); - assertFalse(init1.failed); - assertThat(init1.timestamp, greaterThanOrEqualTo(init0.timestamp)); - assertThat(init1.timestamp, lessThanOrEqualTo(timeAfterOpen)); + LDValue streamInits = event.get("streamInits"); + assertEquals(2, streamInits.size()); + LDValue init0 = streamInits.get(0); + assertTrue(init0.get("failed").booleanValue()); + assertThat(init0.get("timestamp").longValue(), + allOf(greaterThanOrEqualTo(startTime), lessThanOrEqualTo(timeAfterOpen))); + assertThat(init0.get("durationMillis").longValue(), lessThanOrEqualTo(timeAfterOpen - startTime)); + + LDValue init1 = streamInits.get(1); + assertFalse(init1.get("failed").booleanValue()); + assertThat(init1.get("timestamp").longValue(), + allOf(greaterThanOrEqualTo(init0.get("timestamp").longValue()), lessThanOrEqualTo(timeAfterOpen))); } } } @@ -636,30 +648,22 @@ private void verifyEventCausesStreamRestart(String eventName, String eventData, public void testSpecialHttpConfigurations() throws Exception { Handler handler = streamResponse(EMPTY_DATA_EVENT); - TestHttpUtil.testWithSpecialHttpConfigurations(handler, - (targetUri, goodHttpConfig) -> { - LDConfig config = new LDConfig.Builder().http(goodHttpConfig).build(); + SpecialHttpConfigurations.testAll(handler, + (URI serverUri, SpecialHttpConfigurations.Params params) -> { + LDConfig config = new LDConfig.Builder() + .http(TestUtil.makeHttpConfigurationFromTestParams(params)) + .build(); ConnectionErrorSink errorSink = new ConnectionErrorSink(); - try (StreamProcessor sp = createStreamProcessor(config, targetUri)) { + try (StreamProcessor sp = createStreamProcessor(config, serverUri)) { sp.connectionErrorHandler = errorSink; startAndWait(sp); - assertNull(errorSink.errors.peek()); - } - }, - (targetUri, badHttpConfig) -> { - LDConfig config = new LDConfig.Builder().http(badHttpConfig).build(); - ConnectionErrorSink errorSink = new ConnectionErrorSink(); - - try (StreamProcessor sp = createStreamProcessor(config, targetUri)) { - sp.connectionErrorHandler = errorSink; - startAndWait(sp); - - Throwable error = errorSink.errors.peek(); - assertNotNull(error); + if (errorSink.errors.size() != 0) { + throw new IOException(errorSink.errors.peek()); + } + return true; } - } - ); + }); } static class ConnectionErrorSink implements ConnectionErrorHandler { @@ -746,9 +750,9 @@ private StreamProcessor createStreamProcessor(URI streamUri) { return createStreamProcessor(LDConfig.DEFAULT, streamUri, null); } - private StreamProcessor createStreamProcessor(LDConfig config, URI streamUri, DiagnosticAccumulator acc) { + private StreamProcessor createStreamProcessor(LDConfig config, URI streamUri, DiagnosticStore acc) { return new StreamProcessor( - clientContext(SDK_KEY, config == null ? LDConfig.DEFAULT : config).getHttp(), + ComponentsImpl.toHttpProperties(clientContext(SDK_KEY, config == null ? LDConfig.DEFAULT : config).getHttp()), dataSourceUpdates, Thread.MIN_PRIORITY, acc, diff --git a/src/test/java/com/launchdarkly/sdk/server/TestComponents.java b/src/test/java/com/launchdarkly/sdk/server/TestComponents.java index 13a7a91cc..362f87c61 100644 --- a/src/test/java/com/launchdarkly/sdk/server/TestComponents.java +++ b/src/test/java/com/launchdarkly/sdk/server/TestComponents.java @@ -3,37 +3,39 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.Logs; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.AttributeRef; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDContext; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.internal.events.DiagnosticStore; +import com.launchdarkly.sdk.internal.events.Event; +import com.launchdarkly.sdk.internal.events.EventsConfiguration; +import com.launchdarkly.sdk.internal.events.DiagnosticStore.SdkDiagnosticParams; +import com.launchdarkly.sdk.internal.http.HttpProperties; import com.launchdarkly.sdk.server.integrations.EventProcessorBuilder; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataSource; -import com.launchdarkly.sdk.server.interfaces.DataSourceFactory; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.DataStoreFactory; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider.CacheStats; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.DataStoreUpdates; -import com.launchdarkly.sdk.server.interfaces.Event; -import com.launchdarkly.sdk.server.interfaces.EventProcessor; -import com.launchdarkly.sdk.server.interfaces.EventProcessorFactory; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; import com.launchdarkly.sdk.server.interfaces.FlagChangeListener; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStore; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.EventProcessor; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; @@ -47,22 +49,31 @@ @SuppressWarnings("javadoc") public class TestComponents { - static ScheduledExecutorService sharedExecutor = newSingleThreadScheduledExecutor( + public static ScheduledExecutorService sharedExecutor = newSingleThreadScheduledExecutor( new ThreadFactoryBuilder().setNameFormat("TestComponents-sharedExecutor-%d").build()); - + public static LDLogger nullLogger = LDLogger.withAdapter(Logs.none(), ""); + + public static DiagnosticStore basicDiagnosticStore() { + return new DiagnosticStore(new SdkDiagnosticParams("sdk_key", "sdk", "1.0.0", "Java", null, null, null)); + } - public static ClientContext clientContext(final String sdkKey, final LDConfig config) { - return new ClientContextImpl(sdkKey, config, sharedExecutor, null); + public static ClientContextImpl clientContext(String sdkKey, LDConfig config) { + return ClientContextImpl.fromConfig(sdkKey, config, sharedExecutor); } - public static ClientContext clientContext(final String sdkKey, final LDConfig config, DiagnosticAccumulator diagnosticAccumulator) { - return new ClientContextImpl(sdkKey, config, sharedExecutor, diagnosticAccumulator); + public static ClientContextImpl clientContext(String sdkKey, LDConfig config, + DataSourceUpdateSink dataSourceUpdateSink) { + return ClientContextImpl.fromConfig(sdkKey, config, sharedExecutor).withDataSourceUpdateSink(dataSourceUpdateSink); } public static HttpConfiguration defaultHttpConfiguration() { return clientContext("", LDConfig.DEFAULT).getHttp(); } + + public static HttpProperties defaultHttpProperties() { + return ComponentsImpl.toHttpProperties(defaultHttpConfiguration()); + } public static DataStore dataStoreThatThrowsException(RuntimeException e) { return new DataStoreThatThrowsException(e); @@ -77,7 +88,7 @@ public static MockDataSourceUpdates dataSourceUpdates(DataStore store, DataStore } static EventsConfiguration defaultEventsConfig() { - return makeEventsConfig(false, false, null); + return makeEventsConfig(false, null); } public static DataSource failedDataSource() { @@ -94,36 +105,26 @@ public static DataStore initedDataStore() { return store; } - static EventsConfiguration makeEventsConfig(boolean allAttributesPrivate, boolean inlineUsersInEvents, - Set privateAttributes) { + static EventsConfiguration makeEventsConfig(boolean allAttributesPrivate, + Collection privateAttributes) { return new EventsConfiguration( allAttributesPrivate, 0, null, + EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL.toMillis(), + null, null, - EventProcessorBuilder.DEFAULT_FLUSH_INTERVAL, - inlineUsersInEvents, - privateAttributes, 0, - EventProcessorBuilder.DEFAULT_USER_KEYS_FLUSH_INTERVAL, - EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL + null, + EventProcessorBuilder.DEFAULT_FLUSH_INTERVAL.toMillis(), + false, + false, + privateAttributes ); } - public static DataSourceFactory specificDataSource(final DataSource up) { - return (context, dataSourceUpdates) -> up; - } - - public static DataStoreFactory specificDataStore(final DataStore store) { - return (context, statusUpdater) -> store; - } - - public static PersistentDataStoreFactory specificPersistentDataStore(final PersistentDataStore store) { - return context -> store; - } - - public static EventProcessorFactory specificEventProcessor(final EventProcessor ep) { - return context -> ep; + public static ComponentConfigurer specificComponent(final T instance) { + return context -> instance; } public static class TestEventProcessor implements EventProcessor { @@ -131,16 +132,31 @@ public static class TestEventProcessor implements EventProcessor { volatile int flushCount; @Override - public void close() throws IOException {} - + public void flush() { + flushCount++; + } + @Override - public void sendEvent(Event e) { - events.add(e); + public void close() throws IOException { } - + @Override - public void flush() { - flushCount++; + public void recordEvaluationEvent(LDContext context, String flagKey, int flagVersion, int variation, LDValue value, + EvaluationReason reason, LDValue defaultValue, String prerequisiteOfFlagKey, boolean requireFullEvent, + Long debugEventsUntilDate) { + events.add(new Event.FeatureRequest(System.currentTimeMillis(), flagKey, context, flagVersion, + variation, value, defaultValue, reason, prerequisiteOfFlagKey, requireFullEvent, debugEventsUntilDate, false)); + } + + @Override + public void recordIdentifyEvent(LDContext context) { + events.add(new Event.Identify(System.currentTimeMillis(), context)); + } + + + @Override + public void recordCustomEvent(LDContext context, String eventKey, LDValue data, Double metricValue) { + events.add(new Event.Custom(System.currentTimeMillis(), eventKey, context, data, metricValue)); } } @@ -157,7 +173,7 @@ public void close() throws IOException { } }; - public static class MockDataSourceUpdates implements DataSourceUpdates { + public static class MockDataSourceUpdates implements DataSourceUpdateSink { public static class UpsertParams { public final DataKind kind; public final String key; @@ -236,18 +252,18 @@ public UpsertParams awaitUpsert() { } } - public static class DataStoreFactoryThatExposesUpdater implements DataStoreFactory { - public volatile DataStoreUpdates dataStoreUpdates; - private final DataStoreFactory wrappedFactory; + public static class ContextCapturingFactory implements ComponentConfigurer { + public volatile ClientContext clientContext; + private final ComponentConfigurer wrappedFactory; - public DataStoreFactoryThatExposesUpdater(DataStoreFactory wrappedFactory) { + public ContextCapturingFactory(ComponentConfigurer wrappedFactory) { this.wrappedFactory = wrappedFactory; } @Override - public DataStore createDataStore(ClientContext context, DataStoreUpdates dataStoreUpdates) { - this.dataStoreUpdates = dataStoreUpdates; - return wrappedFactory.createDataStore(context, dataStoreUpdates); + public T build(ClientContext context) { + this.clientContext = context; + return wrappedFactory.build(context); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/TestHttpUtil.java b/src/test/java/com/launchdarkly/sdk/server/TestHttpUtil.java deleted file mode 100644 index 11c5c7659..000000000 --- a/src/test/java/com/launchdarkly/sdk/server/TestHttpUtil.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.launchdarkly.sdk.server; - -import com.launchdarkly.sdk.server.interfaces.HttpConfigurationFactory; -import com.launchdarkly.testhelpers.httptest.Handler; -import com.launchdarkly.testhelpers.httptest.HttpServer; -import com.launchdarkly.testhelpers.httptest.RequestInfo; -import com.launchdarkly.testhelpers.httptest.ServerTLSConfiguration; - -import java.io.IOException; -import java.net.URI; - -import static com.launchdarkly.sdk.server.TestUtil.makeSocketFactorySingleHost; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.nullValue; - -class TestHttpUtil { - // Used for testWithSpecialHttpConfigurations - static interface HttpConfigurationTestAction { - void accept(URI targetUri, HttpConfigurationFactory httpConfig) throws IOException; - } - - /** - * A test suite for all SDK components that support our standard HTTP configuration options. - *

- * Although all of our supported HTTP behaviors are implemented in shared code, there is no - * guarantee that all of our components are using that code, or using it correctly. So we - * should run this test suite on each component that can be affected by HttpConfigurationBuilder - * properties. It works as follows: - *

    - *
  • For each HTTP configuration variant that is expected to work (trusted certificate; - * proxy server; etc.), set up a server that will produce whatever expected response was - * specified in {@code handler}. Then run {@code testActionShouldSucceed}, which should create - * its component with the given configuration and base URI and verify that the component - * behaves correctly. - *
  • Do the same for each HTTP configuration variant that is expected to fail, but run - * {@code testActionShouldFail} instead. - *
- * - * @param handler the response that the server should provide for all requests - * @param testActionShouldSucceed an action that asserts that the component works - * @param testActionShouldFail an action that asserts that the component does not work - * @throws IOException - */ - static void testWithSpecialHttpConfigurations( - Handler handler, - HttpConfigurationTestAction testActionShouldSucceed, - HttpConfigurationTestAction testActionShouldFail - ) throws IOException { - - testHttpClientDoesNotAllowSelfSignedCertByDefault(handler, testActionShouldFail); - testHttpClientCanBeConfiguredToAllowSelfSignedCert(handler, testActionShouldSucceed); - testHttpClientCanUseCustomSocketFactory(handler, testActionShouldSucceed); - testHttpClientCanUseProxy(handler, testActionShouldSucceed); - testHttpClientCanUseProxyWithBasicAuth(handler, testActionShouldSucceed); - } - - static void testHttpClientDoesNotAllowSelfSignedCertByDefault(Handler handler, - HttpConfigurationTestAction testActionShouldFail) { - try { - ServerTLSConfiguration tlsConfig = ServerTLSConfiguration.makeSelfSignedCertificate(); - try (HttpServer secureServer = HttpServer.startSecure(tlsConfig, handler)) { - testActionShouldFail.accept(secureServer.getUri(), Components.httpConfiguration()); - assertThat(secureServer.getRecorder().count(), equalTo(0)); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - static void testHttpClientCanBeConfiguredToAllowSelfSignedCert(Handler handler, - HttpConfigurationTestAction testActionShouldSucceed) { - try { - ServerTLSConfiguration tlsConfig = ServerTLSConfiguration.makeSelfSignedCertificate(); - HttpConfigurationFactory httpConfig = Components.httpConfiguration() - .sslSocketFactory(tlsConfig.getSocketFactory(), tlsConfig.getTrustManager()); - try (HttpServer secureServer = HttpServer.startSecure(tlsConfig, handler)) { - testActionShouldSucceed.accept(secureServer.getUri(), httpConfig); - assertThat(secureServer.getRecorder().count(), equalTo(1)); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - static void testHttpClientCanUseCustomSocketFactory(Handler handler, - HttpConfigurationTestAction testActionShouldSucceed) { - try { - try (HttpServer server = HttpServer.start(handler)) { - HttpConfigurationFactory httpConfig = Components.httpConfiguration() - .socketFactory(makeSocketFactorySingleHost(server.getUri().getHost(), server.getPort())); - - URI uriWithWrongPort = URI.create("http://localhost:1"); - testActionShouldSucceed.accept(uriWithWrongPort, httpConfig); - assertThat(server.getRecorder().count(), equalTo(1)); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - static void testHttpClientCanUseProxy(Handler handler, - HttpConfigurationTestAction testActionShouldSucceed) { - try { - try (HttpServer server = HttpServer.start(handler)) { - HttpConfigurationFactory httpConfig = Components.httpConfiguration() - .proxyHostAndPort(server.getUri().getHost(), server.getPort()); - - URI fakeBaseUri = URI.create("http://not-a-real-host"); - testActionShouldSucceed.accept(fakeBaseUri, httpConfig); - assertThat(server.getRecorder().count(), equalTo(1)); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - static void testHttpClientCanUseProxyWithBasicAuth(Handler handler, - HttpConfigurationTestAction testActionShouldSucceed) { - Handler proxyHandler = ctx -> { - if (ctx.getRequest().getHeader("Proxy-Authorization") == null) { - ctx.setStatus(407); - ctx.setHeader("Proxy-Authenticate", "Basic realm=x"); - } else { - handler.apply(ctx); - } - }; - try { - try (HttpServer server = HttpServer.start(proxyHandler)) { - HttpConfigurationFactory httpConfig = Components.httpConfiguration() - .proxyHostAndPort(server.getUri().getHost(), server.getPort()) - .proxyAuth(Components.httpBasicAuthentication("user", "pass")); - - URI fakeBaseUri = URI.create("http://not-a-real-host"); - testActionShouldSucceed.accept(fakeBaseUri, httpConfig); - - assertThat(server.getRecorder().count(), equalTo(2)); - RequestInfo req1 = server.getRecorder().requireRequest(); - assertThat(req1.getHeader("Proxy-Authorization"), nullValue()); - RequestInfo req2 = server.getRecorder().requireRequest(); - assertThat(req2.getHeader("Proxy-Authorization"), equalTo("Basic dXNlcjpwYXNz")); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/test/java/com/launchdarkly/sdk/server/TestUtil.java b/src/test/java/com/launchdarkly/sdk/server/TestUtil.java index 5f47ffce5..4752d91b2 100644 --- a/src/test/java/com/launchdarkly/sdk/server/TestUtil.java +++ b/src/test/java/com/launchdarkly/sdk/server/TestUtil.java @@ -10,20 +10,18 @@ import com.launchdarkly.sdk.server.DataModel.FeatureFlag; import com.launchdarkly.sdk.server.DataModel.Segment; import com.launchdarkly.sdk.server.DataModel.VersionedData; +import com.launchdarkly.sdk.server.integrations.HttpConfigurationBuilder; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStore; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; import com.launchdarkly.sdk.server.interfaces.FlagChangeEvent; +import com.launchdarkly.sdk.server.subsystems.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import com.launchdarkly.testhelpers.Assertions; +import com.launchdarkly.testhelpers.JsonAssertions; +import com.launchdarkly.testhelpers.httptest.SpecialHttpConfigurations; -import java.io.IOException; import java.io.StringReader; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; import java.time.Duration; import java.util.HashSet; import java.util.Set; @@ -33,15 +31,12 @@ import java.util.function.Function; import java.util.function.Supplier; -import javax.net.SocketFactory; - import static com.launchdarkly.sdk.server.DataModel.FEATURES; import static com.launchdarkly.sdk.server.DataModel.SEGMENTS; import static com.launchdarkly.sdk.server.DataStoreTestTypes.toDataMap; import static com.launchdarkly.sdk.server.JsonHelpers.serialize; import static com.launchdarkly.testhelpers.ConcurrentHelpers.assertNoMoreValues; import static com.launchdarkly.testhelpers.ConcurrentHelpers.awaitValue; -import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonEquals; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; @@ -64,6 +59,11 @@ public static String getSdkVersion() { return Version.SDK_VERSION; } + public static void assertJsonEquals(LDValue expected, LDValue actual) { + // Gives a better failure diff than assertEquals + JsonAssertions.assertJsonEquals(expected.toJsonString(), actual.toJsonString()); + } + public static void upsertFlag(DataStore store, FeatureFlag flag) { store.upsert(FEATURES, flag.getKey(), new ItemDescriptor(flag.getVersion(), flag)); } @@ -110,13 +110,13 @@ public static DataSourceStatusProvider.Status requireDataSourceStatusEventually( public static void assertDataSetEquals(FullDataSet expected, FullDataSet actual) { String expectedJson = TEST_GSON_INSTANCE.toJson(toDataMap(expected)); String actualJson = TEST_GSON_INSTANCE.toJson(toDataMap(actual)); - assertJsonEquals(expectedJson, actualJson); + JsonAssertions.assertJsonEquals(expectedJson, actualJson); } public static void assertItemEquals(VersionedData expected, ItemDescriptor item) { assertEquals(expected.getVersion(), item.getVersion()); assertEquals(expected.getClass(), item.getItem().getClass()); - assertJsonEquals(serialize(expected), serialize(item.getItem())); + JsonAssertions.assertJsonEquals(serialize(expected), serialize(item.getItem())); } public static String describeDataSet(FullDataSet data) { @@ -156,67 +156,6 @@ public static EvalResult simpleEvaluation(int variation, LDValue value) { return EvalResult.of(value, variation, EvaluationReason.fallthrough()); } - // returns a socket factory that creates sockets that only connect to host and port - static SocketFactorySingleHost makeSocketFactorySingleHost(String host, int port) { - return new SocketFactorySingleHost(host, port); - } - - private static final class SocketSingleHost extends Socket { - private final String host; - private final int port; - - SocketSingleHost(String host, int port) { - this.host = host; - this.port = port; - } - - @Override public void connect(SocketAddress endpoint) throws IOException { - super.connect(new InetSocketAddress(this.host, this.port), 0); - } - - @Override public void connect(SocketAddress endpoint, int timeout) throws IOException { - super.connect(new InetSocketAddress(this.host, this.port), timeout); - } - } - - public static final class SocketFactorySingleHost extends SocketFactory { - private final String host; - private final int port; - - public SocketFactorySingleHost(String host, int port) { - this.host = host; - this.port = port; - } - - @Override public Socket createSocket() throws IOException { - return new SocketSingleHost(this.host, this.port); - } - - @Override public Socket createSocket(String host, int port) throws IOException { - Socket socket = createSocket(); - socket.connect(new InetSocketAddress(this.host, this.port)); - return socket; - } - - @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { - Socket socket = createSocket(); - socket.connect(new InetSocketAddress(this.host, this.port)); - return socket; - } - - @Override public Socket createSocket(InetAddress host, int port) throws IOException { - Socket socket = createSocket(); - socket.connect(new InetSocketAddress(this.host, this.port)); - return socket; - } - - @Override public Socket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException { - Socket socket = createSocket(); - socket.connect(new InetSocketAddress(this.host, this.port)); - return socket; - } - } - public static void assertFullyEqual(T a, T b) { assertEquals(a, b); assertEquals(b, a); @@ -237,6 +176,24 @@ public static void assertThrows(Class exceptionClass, Runnable r) { } } + public static HttpConfigurationBuilder makeHttpConfigurationFromTestParams( + SpecialHttpConfigurations.Params params) { + HttpConfigurationBuilder b = Components.httpConfiguration(); + if (params.getTlsConfig() != null) { + b.sslSocketFactory(params.getTlsConfig().getSocketFactory(), params.getTlsConfig().getTrustManager()); + } + if (params.getProxyHost() != null) { + b.proxyHostAndPort(params.getProxyHost(), params.getProxyPort()); + if (params.getProxyBasicAuthUser() != null) { + b.proxyAuth(Components.httpBasicAuthentication(params.getProxyBasicAuthUser(), params.getProxyBasicAuthPassword())); + } + } + if (params.getSocketFactory() != null) { + b.socketFactory(params.getSocketFactory()); + } + return b; + } + public interface BuilderPropertyTester { void assertDefault(TValue defaultValue); void assertCanSet(TValue newValue); diff --git a/src/test/java/com/launchdarkly/sdk/server/UtilTest.java b/src/test/java/com/launchdarkly/sdk/server/UtilTest.java index e85a35617..ef7267e36 100644 --- a/src/test/java/com/launchdarkly/sdk/server/UtilTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/UtilTest.java @@ -2,60 +2,26 @@ import com.launchdarkly.sdk.server.interfaces.ApplicationInfo; import com.launchdarkly.sdk.server.interfaces.HttpAuthentication; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; import org.junit.Test; import java.time.Duration; -import static com.launchdarkly.sdk.server.TestComponents.clientContext; import static com.launchdarkly.sdk.server.Util.applicationTagHeader; -import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder; -import static com.launchdarkly.sdk.server.Util.shutdownHttpClient; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import okhttp3.Authenticator; -import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; @SuppressWarnings("javadoc") public class UtilTest extends BaseTest { - @Test - public void testConnectTimeout() { - LDConfig config = new LDConfig.Builder().http(Components.httpConfiguration().connectTimeout(Duration.ofSeconds(3))).build(); - HttpConfiguration httpConfig = clientContext("", config).getHttp(); - OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); - configureHttpClientBuilder(httpConfig, httpBuilder); - OkHttpClient httpClient = httpBuilder.build(); - try { - assertEquals(3000, httpClient.connectTimeoutMillis()); - } finally { - shutdownHttpClient(httpClient); - } - } - - @Test - public void testSocketTimeout() { - LDConfig config = new LDConfig.Builder().http(Components.httpConfiguration().socketTimeout(Duration.ofSeconds(3))).build(); - HttpConfiguration httpConfig = clientContext("", config).getHttp(); - OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder(); - configureHttpClientBuilder(httpConfig, httpBuilder); - OkHttpClient httpClient = httpBuilder.build(); - try { - assertEquals(3000, httpClient.readTimeoutMillis()); - } finally { - shutdownHttpClient(httpClient); - } - } - @Test public void useOurBasicAuthenticatorAsOkhttpProxyAuthenticator() throws Exception { HttpAuthentication ourAuth = Components.httpBasicAuthentication("user", "pass"); - Authenticator okhttpAuth = Util.okhttpAuthenticatorFromHttpAuthStrategy(ourAuth, - "Proxy-Authentication", "Proxy-Authorization"); + Authenticator okhttpAuth = Util.okhttpAuthenticatorFromHttpAuthStrategy(ourAuth); Request originalRequest = new Request.Builder().url("http://proxy").build(); Response resp1 = new Response.Builder() @@ -81,7 +47,7 @@ public void useOurBasicAuthenticatorAsOkhttpProxyAuthenticator() throws Exceptio assertNull(okhttpAuth.authenticate(null, resp2)); // null tells OkHttp to give up } - + @Test public void describeDuration() { assertEquals("15 milliseconds", Util.describeDuration(Duration.ofMillis(15))); diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBase.java b/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBase.java index be6a9c79d..ce07b004c 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBase.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBase.java @@ -1,11 +1,11 @@ package com.launchdarkly.sdk.server.integrations; import com.launchdarkly.sdk.server.BaseTest; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.Membership; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.StoreMetadata; -import com.launchdarkly.sdk.server.interfaces.ClientContext; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.Membership; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.StoreMetadata; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; import org.junit.Assert; import org.junit.Test; @@ -16,7 +16,7 @@ import java.util.Objects; import static com.launchdarkly.sdk.server.TestComponents.clientContext; -import static com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.createMembershipFromSegmentRefs; +import static com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.createMembershipFromSegmentRefs; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -43,7 +43,7 @@ private ClientContext makeClientContext() { } private BigSegmentStore makeEmptyStore() throws Exception { - BigSegmentStore store = makeStore(prefix).createBigSegmentStore(makeClientContext()); + BigSegmentStore store = makeStore(prefix).build(makeClientContext()); try { clearData(prefix); } catch (RuntimeException ex) { @@ -146,7 +146,7 @@ private void assertEqualMembership(Membership expected, Membership actual) { * @param prefix the database prefix * @return the configured factory */ - protected abstract BigSegmentStoreFactory makeStore(String prefix); + protected abstract ComponentConfigurer makeStore(String prefix); /** * Test classes should override this method to clear all data from the underlying data store for diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBaseTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBaseTest.java index 6d5fa3225..a7878034f 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBaseTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentStoreTestBaseTest.java @@ -1,17 +1,17 @@ package com.launchdarkly.sdk.server.integrations; -import static com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.createMembershipFromSegmentRefs; - -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.StoreMetadata; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.Membership; -import com.launchdarkly.sdk.server.interfaces.ClientContext; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.Membership; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.StoreMetadata; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; import java.io.IOException; import java.util.HashMap; import java.util.Map; +import static com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.createMembershipFromSegmentRefs; + @SuppressWarnings("javadoc") public class BigSegmentStoreTestBaseTest extends BigSegmentStoreTestBase { // This runs BigSegmentStoreTestBase against a mock store implementation that is known to behave @@ -30,7 +30,7 @@ private DataSet getOrCreateDataSet(String prefix) { } @Override - protected BigSegmentStoreFactory makeStore(String prefix) { + protected ComponentConfigurer makeStore(String prefix) { return new MockStoreFactory(getOrCreateDataSet(prefix)); } @@ -53,7 +53,7 @@ protected void setSegments(String prefix, String userHashKey, Iterable i dataSet.memberships.put(userHashKey, createMembershipFromSegmentRefs(includedSegmentRefs, excludedSegmentRefs)); } - private static final class MockStoreFactory implements BigSegmentStoreFactory { + private static final class MockStoreFactory implements ComponentConfigurer { private final DataSet data; private MockStoreFactory(DataSet data) { @@ -61,7 +61,7 @@ private MockStoreFactory(DataSet data) { } @Override - public BigSegmentStore createBigSegmentStore(ClientContext context) { + public BigSegmentStore build(ClientContext context) { return new MockStore(data); } } diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilderTest.java index 9a48af0e3..ed8f5b76f 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/BigSegmentsConfigurationBuilderTest.java @@ -1,22 +1,23 @@ package com.launchdarkly.sdk.server.integrations; -import static com.launchdarkly.sdk.server.TestUtil.BuilderPropertyTester; -import static com.launchdarkly.sdk.server.TestUtil.BuilderTestUtil; -import static org.easymock.EasyMock.createStrictControl; -import static org.easymock.EasyMock.expectLastCall; -import static org.junit.Assert.assertSame; - import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStore; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreFactory; +import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.TestComponents; +import com.launchdarkly.sdk.server.TestUtil.BuilderPropertyTester; +import com.launchdarkly.sdk.server.TestUtil.BuilderTestUtil; import com.launchdarkly.sdk.server.interfaces.BigSegmentsConfiguration; -import com.launchdarkly.sdk.server.interfaces.ClientContext; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStore; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; import org.easymock.IMocksControl; import org.junit.Test; import java.time.Duration; +import static com.launchdarkly.sdk.server.TestComponents.specificComponent; +import static org.easymock.EasyMock.createStrictControl; +import static org.junit.Assert.assertSame; + @SuppressWarnings("javadoc") public class BigSegmentsConfigurationBuilderTest { @@ -24,25 +25,19 @@ public class BigSegmentsConfigurationBuilderTest { public BigSegmentsConfigurationBuilderTest() { tester = new BuilderTestUtil<>(() -> Components.bigSegments(null), - b -> b.createBigSegmentsConfiguration(null)); + b -> b.build(null)); } @Test public void storeFactory() { IMocksControl ctrl = createStrictControl(); - ClientContext contextMock = ctrl.createMock(ClientContext.class); BigSegmentStore storeMock = ctrl.createMock(BigSegmentStore.class); - BigSegmentStoreFactory storeFactoryMock = ctrl.createMock(BigSegmentStoreFactory.class); - - storeFactoryMock.createBigSegmentStore(contextMock); - expectLastCall().andReturn(storeMock); - ctrl.replay(); + ComponentConfigurer storeFactory = specificComponent(storeMock); - BigSegmentsConfigurationBuilder b = Components.bigSegments(storeFactoryMock); - BigSegmentsConfiguration c = b.createBigSegmentsConfiguration(contextMock); + BigSegmentsConfigurationBuilder b = Components.bigSegments(storeFactory); + BigSegmentsConfiguration c = b.build(TestComponents.clientContext("", new LDConfig.Builder().build())); assertSame(storeMock, c.getStore()); - ctrl.verify(); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/ClientWithFileDataSourceTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/ClientWithFileDataSourceTest.java index 57951edae..a706fc3f3 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/ClientWithFileDataSourceTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/ClientWithFileDataSourceTest.java @@ -1,6 +1,6 @@ package com.launchdarkly.sdk.server.integrations; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.server.Components; import com.launchdarkly.sdk.server.LDClient; @@ -18,7 +18,7 @@ @SuppressWarnings("javadoc") public class ClientWithFileDataSourceTest { - private static final LDUser user = new LDUser.Builder("userkey").build(); + private static final LDContext user = LDContext.create("userkey"); private LDClient makeClient() throws Exception { FileDataSourceBuilder fdsb = FileData.dataSource() diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java index bdd99f4fc..f572d861f 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/DataLoaderTest.java @@ -4,10 +4,10 @@ import com.launchdarkly.sdk.server.integrations.FileDataSourceImpl.DataBuilder; import com.launchdarkly.sdk.server.integrations.FileDataSourceImpl.DataLoader; import com.launchdarkly.sdk.server.integrations.FileDataSourceParsing.FileDataException; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; import com.launchdarkly.testhelpers.JsonTestValue; import org.junit.Assert; diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilderTest.java index e13e52639..29f6c1f1e 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/EventProcessorBuilderTest.java @@ -1,13 +1,13 @@ package com.launchdarkly.sdk.server.integrations; import com.google.common.collect.ImmutableSet; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.AttributeRef; import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.interfaces.EventSenderFactory; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.EventSender; import org.junit.Test; -import java.net.URI; import java.time.Duration; import static com.launchdarkly.sdk.server.Components.sendEvents; @@ -35,18 +35,6 @@ public void allAttributesPrivate() { .allAttributesPrivate); } - @Test - public void baseURI() { - assertNull(sendEvents().baseURI); - - assertEquals(URI.create("x"), sendEvents().baseURI(URI.create("x")).baseURI); - - assertNull(sendEvents() - .baseURI(URI.create("x")) - .baseURI(null) - .baseURI); - } - @Test public void capacity() { assertEquals(DEFAULT_CAPACITY, sendEvents().capacity); @@ -74,12 +62,12 @@ public void diagnosticRecordingInterval() { @Test public void eventSender() { - assertNull(sendEvents().eventSenderFactory); + assertNull(sendEvents().eventSenderConfigurer); - EventSenderFactory f = (ec, hc) -> null; - assertSame(f, sendEvents().eventSender(f).eventSenderFactory); + ComponentConfigurer f = (ctx) -> null; + assertSame(f, sendEvents().eventSender(f).eventSenderConfigurer); - assertNull(sendEvents().eventSender(f).eventSender(null).eventSenderFactory); + assertNull(sendEvents().eventSender(f).eventSender(null).eventSenderConfigurer); } @Test @@ -95,31 +83,13 @@ public void flushInterval() { .flushInterval(null); // null sets it back to the default assertEquals(DEFAULT_FLUSH_INTERVAL, builder3.flushInterval); } - - @Test - public void inlineUsersInEvents() { - assertEquals(false, sendEvents().inlineUsersInEvents); - - assertEquals(true, sendEvents().inlineUsersInEvents(true).inlineUsersInEvents); - - assertEquals(false, sendEvents() - .inlineUsersInEvents(true) - .inlineUsersInEvents(false) - .inlineUsersInEvents); - } - - @Test - public void privateAttributeNames() { - assertNull(sendEvents().privateAttributes); - assertEquals(ImmutableSet.of(UserAttribute.forName("a"), UserAttribute.forName("b")), - sendEvents().privateAttributeNames("a", "b").privateAttributes); - } - @Test public void privateAttributes() { - assertEquals(ImmutableSet.of(UserAttribute.EMAIL, UserAttribute.NAME), - sendEvents().privateAttributes(UserAttribute.EMAIL, UserAttribute.NAME).privateAttributes); + assertNull(sendEvents().privateAttributes); + + assertEquals(ImmutableSet.of(AttributeRef.fromLiteral("email"), AttributeRef.fromPath("/address/street")), + sendEvents().privateAttributes("email", "/address/street").privateAttributes); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceAutoUpdateTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceAutoUpdateTest.java index 38b479cb7..7dae5f829 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceAutoUpdateTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceAutoUpdateTest.java @@ -4,9 +4,9 @@ import com.launchdarkly.sdk.server.LDConfig; import com.launchdarkly.sdk.server.TestComponents; import com.launchdarkly.sdk.server.TestComponents.MockDataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataSource; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; import com.launchdarkly.testhelpers.TempDir; import com.launchdarkly.testhelpers.TempFile; @@ -48,7 +48,7 @@ private static FileDataSourceBuilder makeFactoryWithFile(Path path) { } private DataSource makeDataSource(FileDataSourceBuilder builder) { - return builder.createDataSource(clientContext("", config), dataSourceUpdates); + return builder.build(clientContext("", config, dataSourceUpdates)); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceTest.java index e27e96882..9ac12dbf7 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/FileDataSourceTest.java @@ -5,9 +5,9 @@ import com.launchdarkly.sdk.server.LDConfig; import com.launchdarkly.sdk.server.TestComponents; import com.launchdarkly.sdk.server.TestComponents.MockDataSourceUpdates; -import com.launchdarkly.sdk.server.interfaces.DataSource; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStore; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataStore; import org.junit.Test; @@ -49,7 +49,7 @@ private static FileDataSourceBuilder makeFactoryWithFile(Path path) { } private DataSource makeDataSource(FileDataSourceBuilder builder) { - return builder.createDataSource(clientContext("", config), dataSourceUpdates); + return builder.build(clientContext("", config, dataSourceUpdates)); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java index 19956b8f0..237de74dc 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/HttpConfigurationBuilderTest.java @@ -3,8 +3,8 @@ import com.google.common.collect.ImmutableMap; import com.launchdarkly.sdk.server.Components; import com.launchdarkly.sdk.server.interfaces.ApplicationInfo; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.HttpConfiguration; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.HttpConfiguration; import org.junit.Test; @@ -33,7 +33,7 @@ @SuppressWarnings("javadoc") public class HttpConfigurationBuilderTest { private static final String SDK_KEY = "sdk-key"; - private static final BasicConfiguration BASIC_CONFIG = new BasicConfiguration(SDK_KEY, false, 0, null, null); + private static final ClientContext BASIC_CONTEXT = new ClientContext(SDK_KEY); private static ImmutableMap.Builder buildBasicHeaders() { return ImmutableMap.builder() @@ -43,7 +43,7 @@ private static ImmutableMap.Builder buildBasicHeaders() { @Test public void testDefaults() { - HttpConfiguration hc = Components.httpConfiguration().createHttpConfiguration(BASIC_CONFIG); + HttpConfiguration hc = Components.httpConfiguration().build(BASIC_CONTEXT); assertEquals(DEFAULT_CONNECT_TIMEOUT, hc.getConnectTimeout()); assertNull(hc.getProxy()); assertNull(hc.getProxyAuthentication()); @@ -58,13 +58,13 @@ public void testDefaults() { public void testConnectTimeout() { HttpConfiguration hc = Components.httpConfiguration() .connectTimeout(Duration.ofMillis(999)) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals(999, hc.getConnectTimeout().toMillis()); HttpConfiguration hc2 = Components.httpConfiguration() .connectTimeout(Duration.ofMillis(999)) .connectTimeout(null) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals(DEFAULT_CONNECT_TIMEOUT, hc2.getConnectTimeout()); } @@ -72,7 +72,7 @@ public void testConnectTimeout() { public void testProxy() { HttpConfiguration hc = Components.httpConfiguration() .proxyHostAndPort("my-proxy", 1234) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("my-proxy", 1234)), hc.getProxy()); assertNull(hc.getProxyAuthentication()); } @@ -82,7 +82,7 @@ public void testProxyBasicAuth() { HttpConfiguration hc = Components.httpConfiguration() .proxyHostAndPort("my-proxy", 1234) .proxyAuth(Components.httpBasicAuthentication("user", "pass")) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("my-proxy", 1234)), hc.getProxy()); assertNotNull(hc.getProxyAuthentication()); assertEquals("Basic dXNlcjpwYXNz", hc.getProxyAuthentication().provideAuthorization(null)); @@ -92,13 +92,13 @@ public void testProxyBasicAuth() { public void testSocketTimeout() { HttpConfiguration hc1 = Components.httpConfiguration() .socketTimeout(Duration.ofMillis(999)) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals(999, hc1.getSocketTimeout().toMillis()); HttpConfiguration hc2 = Components.httpConfiguration() .socketTimeout(Duration.ofMillis(999)) .socketTimeout(null) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals(DEFAULT_SOCKET_TIMEOUT, hc2.getSocketTimeout()); } @@ -107,7 +107,7 @@ public void testSocketFactory() { SocketFactory sf = new StubSocketFactory(); HttpConfiguration hc = Components.httpConfiguration() .socketFactory(sf) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertSame(sf, hc.getSocketFactory()); } @@ -117,7 +117,7 @@ public void testSslOptions() { X509TrustManager tm = new StubX509TrustManager(); HttpConfiguration hc = Components.httpConfiguration() .sslSocketFactory(sf, tm) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertSame(sf, hc.getSslSocketFactory()); assertSame(tm, hc.getTrustManager()); } @@ -126,7 +126,7 @@ public void testSslOptions() { public void testWrapperNameOnly() { HttpConfiguration hc = Components.httpConfiguration() .wrapper("Scala", null) - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals("Scala", ImmutableMap.copyOf(hc.getDefaultHeaders()).get("X-LaunchDarkly-Wrapper")); } @@ -134,16 +134,16 @@ public void testWrapperNameOnly() { public void testWrapperWithVersion() { HttpConfiguration hc = Components.httpConfiguration() .wrapper("Scala", "0.1.0") - .createHttpConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals("Scala/0.1.0", ImmutableMap.copyOf(hc.getDefaultHeaders()).get("X-LaunchDarkly-Wrapper")); } @Test public void testApplicationTags() { ApplicationInfo info = new ApplicationInfo("authentication-service", "1.0.0"); - BasicConfiguration basicConfigWithTags = new BasicConfiguration(SDK_KEY, false, 0, info, null); + ClientContext contextWithTags = new ClientContext(SDK_KEY, info, null, null, false, null, 0); HttpConfiguration hc = Components.httpConfiguration() - .createHttpConfiguration(basicConfigWithTags); + .build(contextWithTags); assertEquals("application-id/authentication-service application-version/1.0.0", ImmutableMap.copyOf(hc.getDefaultHeaders()).get("X-LaunchDarkly-Tags")); } diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilderTest.java index cc547e55e..e90d5fd39 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/LoggingConfigurationBuilderTest.java @@ -6,27 +6,30 @@ import com.launchdarkly.logging.LogCapture; import com.launchdarkly.logging.Logs; import com.launchdarkly.sdk.server.Components; -import com.launchdarkly.sdk.server.interfaces.BasicConfiguration; -import com.launchdarkly.sdk.server.interfaces.LoggingConfiguration; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration; +import org.hamcrest.Matchers; import org.junit.Test; import java.time.Duration; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.sameInstance; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @SuppressWarnings("javadoc") public class LoggingConfigurationBuilderTest { private static final String SDK_KEY = "sdk-key"; - private static final BasicConfiguration BASIC_CONFIG = new BasicConfiguration(SDK_KEY, false, 0, null, null); + private static final ClientContext BASIC_CONTEXT = new ClientContext(SDK_KEY); @Test public void testDefaults() { - LoggingConfiguration c = Components.logging().createLoggingConfiguration(BASIC_CONFIG); + LoggingConfiguration c = Components.logging().build(BASIC_CONTEXT); assertEquals(LoggingConfigurationBuilder.DEFAULT_LOG_DATA_SOURCE_OUTAGE_AS_ERROR_AFTER, c.getLogDataSourceOutageAsErrorAfter()); } @@ -35,20 +38,24 @@ public void testDefaults() { public void logDataSourceOutageAsErrorAfter() { LoggingConfiguration c1 = Components.logging() .logDataSourceOutageAsErrorAfter(Duration.ofMinutes(9)) - .createLoggingConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertEquals(Duration.ofMinutes(9), c1.getLogDataSourceOutageAsErrorAfter()); LoggingConfiguration c2 = Components.logging() .logDataSourceOutageAsErrorAfter(null) - .createLoggingConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); assertNull(c2.getLogDataSourceOutageAsErrorAfter()); } @Test - public void defaultLogAdapterIsSLF4J() { + public void defaultLogAdapterIsNotSLF4J() { LoggingConfiguration c = Components.logging() - .createLoggingConfiguration(BASIC_CONFIG); - assertThat(c.getLogAdapter(), sameInstance(LDSLF4J.adapter())); + .build(BASIC_CONTEXT); + assertThat(c.getLogAdapter().getClass().getCanonicalName(), + not(startsWith("com.launchdarkly.logging.LDSLF4J"))); + // Note that we're checking the class name here rather than comparing directly to + // LDSLF4J.adapter(), because calling that method isn't safe if you don't have + // SLF4J in the classpath. } @Test @@ -57,7 +64,7 @@ public void canSetLogAdapterAndLevel() { LoggingConfiguration c = Components.logging() .adapter(logSink) .level(LDLogLevel.WARN) - .createLoggingConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); LDLogger logger = LDLogger.withAdapter(c.getLogAdapter(), ""); logger.debug("message 1"); logger.info("message 2"); @@ -71,7 +78,7 @@ public void defaultLevelIsInfo() { LogCapture logSink = Logs.capture(); LoggingConfiguration c = Components.logging() .adapter(logSink) - .createLoggingConfiguration(BASIC_CONFIG); + .build(BASIC_CONTEXT); LDLogger logger = LDLogger.withAdapter(c.getLogAdapter(), ""); logger.debug("message 1"); logger.info("message 2"); diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/MockPersistentDataStore.java b/src/test/java/com/launchdarkly/sdk/server/integrations/MockPersistentDataStore.java index f1542e4b5..30a025229 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/MockPersistentDataStore.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/MockPersistentDataStore.java @@ -2,11 +2,11 @@ import com.google.common.collect.ImmutableList; import com.launchdarkly.sdk.server.DataStoreTestTypes.TestItem; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStore; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; import java.io.IOException; import java.util.HashMap; diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilderTest.java index aac3f122a..b12db5216 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreBuilderTest.java @@ -1,7 +1,8 @@ package com.launchdarkly.sdk.server.integrations; import com.launchdarkly.sdk.server.integrations.PersistentDataStoreBuilder.StaleValuesPolicy; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import org.junit.Test; @@ -16,11 +17,11 @@ @SuppressWarnings("javadoc") public class PersistentDataStoreBuilderTest { - private static final PersistentDataStoreFactory factory = context -> null; + private static final ComponentConfigurer factory = context -> null; @Test public void factory() { - assertSame(factory, persistentDataStore(factory).persistentDataStoreFactory); + assertSame(factory, persistentDataStore(factory).persistentDataStoreConfigurer); } @Test diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreGenericTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreGenericTest.java index f2066c834..b31df60d9 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreGenericTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreGenericTest.java @@ -2,7 +2,8 @@ import com.google.common.collect.ImmutableList; import com.launchdarkly.sdk.server.TestComponents; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -50,10 +51,10 @@ public PersistentDataStoreGenericTest(TestMode testMode) { } @Override - protected PersistentDataStoreFactory buildStore(String prefix) { + protected ComponentConfigurer buildStore(String prefix) { MockPersistentDataStore store = new MockPersistentDataStore(sharedData, prefix); store.persistOnlyAsString = testMode.persistOnlyAsString; - return TestComponents.specificPersistentDataStore(store); + return TestComponents.specificComponent(store); } @Override diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreTestBase.java b/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreTestBase.java index c83ee811f..ebac8ad77 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreTestBase.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/PersistentDataStoreTestBase.java @@ -3,12 +3,12 @@ import com.launchdarkly.sdk.server.BaseTest; import com.launchdarkly.sdk.server.DataStoreTestTypes.DataBuilder; import com.launchdarkly.sdk.server.DataStoreTestTypes.TestItem; -import com.launchdarkly.sdk.server.interfaces.ClientContext; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStore; -import com.launchdarkly.sdk.server.interfaces.PersistentDataStoreFactory; +import com.launchdarkly.sdk.server.subsystems.ClientContext; +import com.launchdarkly.sdk.server.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.PersistentDataStore; import org.junit.After; import org.junit.Assume; @@ -49,41 +49,18 @@ private ClientContext makeClientContext() { @SuppressWarnings("unchecked") private T makeConfiguredStore() { - PersistentDataStoreFactory builder = buildStore(null); - if (builder == null) { - return makeStore(); - } - return (T)builder.createPersistentDataStore(makeClientContext()); + return (T)buildStore(null).build(makeClientContext()); } @SuppressWarnings("unchecked") private T makeConfiguredStoreWithPrefix(String prefix) { - PersistentDataStoreFactory builder = buildStore(prefix); + ComponentConfigurer builder = buildStore(prefix); if (builder == null) { - return makeStoreWithPrefix(prefix); + return null; } - return (T)builder.createPersistentDataStore(makeClientContext()); + return (T)builder.build(makeClientContext()); } - /** - * Test subclasses can override either this method or {@link #buildStore(String)} to create an instance - * of the feature store class with default properties. - * @deprecated It is preferable to override {@link #buildStore(String)} instead. - */ - @Deprecated - protected T makeStore() { - throw new RuntimeException("test subclasses must override either makeStore or buildStore"); - } - - /** - * Test subclasses can implement this (or override {@link #buildStore(String)}) if the feature store - * class supports a key prefix option for keeping data sets distinct within the same database. - * @deprecated It is preferable to override {@link #buildStore(String)} instead. - */ - protected T makeStoreWithPrefix(String prefix) { - return null; - } - /** * Test subclasses should override this method to prepare an instance of the data store class. * They are allowed to return null if {@code prefix} is non-null and they do not support prefixes. @@ -91,7 +68,7 @@ protected T makeStoreWithPrefix(String prefix) { * @param prefix a database prefix or null * @return a factory for creating the data store */ - protected PersistentDataStoreFactory buildStore(String prefix) { + protected ComponentConfigurer buildStore(String prefix) { return null; } diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilderTest.java index 9adc68341..4950c5035 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/PollingDataSourceBuilderTest.java @@ -2,25 +2,14 @@ import org.junit.Test; -import java.net.URI; import java.time.Duration; import static com.launchdarkly.sdk.server.Components.pollingDataSource; import static com.launchdarkly.sdk.server.integrations.PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; @SuppressWarnings("javadoc") public class PollingDataSourceBuilderTest { - @Test - public void baseURI() { - assertNull(pollingDataSource().baseURI); - - assertEquals(URI.create("x"), pollingDataSource().baseURI(URI.create("x")).baseURI); - - assertNull(pollingDataSource().baseURI(URI.create("X")).baseURI(null).baseURI); - } - @Test public void pollInterval() { assertEquals(DEFAULT_POLL_INTERVAL, pollingDataSource().pollInterval); diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilderTest.java index 8a2eebcaf..02fa46f04 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/StreamingDataSourceBuilderTest.java @@ -2,25 +2,14 @@ import org.junit.Test; -import java.net.URI; import java.time.Duration; import static com.launchdarkly.sdk.server.Components.streamingDataSource; import static com.launchdarkly.sdk.server.integrations.StreamingDataSourceBuilder.DEFAULT_INITIAL_RECONNECT_DELAY; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; @SuppressWarnings("javadoc") public class StreamingDataSourceBuilderTest { - @Test - public void baseURI() { - assertNull(streamingDataSource().baseURI); - - assertEquals(URI.create("x"), streamingDataSource().baseURI(URI.create("x")).baseURI); - - assertNull(streamingDataSource().baseURI(URI.create("X")).baseURI(null).baseURI); - } - @Test public void initialReconnectDelay() { assertEquals(DEFAULT_INITIAL_RECONNECT_DELAY, streamingDataSource().initialReconnectDelay); @@ -31,13 +20,4 @@ public void initialReconnectDelay() { assertEquals(DEFAULT_INITIAL_RECONNECT_DELAY, streamingDataSource().initialReconnectDelay(Duration.ofMillis(222)).initialReconnectDelay(null).initialReconnectDelay); } - - @Test - public void pollingBaseURI() { - // The pollingBaseURI option is now ignored, so this test just verifies that changing it does *not* - // change the stream's regular baseURI property. - StreamingDataSourceBuilder b = streamingDataSource(); - b.pollingBaseURI(URI.create("x")); - assertNull(b.baseURI); - } } diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataTest.java index 329805360..47b903ad6 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataTest.java @@ -1,18 +1,19 @@ package com.launchdarkly.sdk.server.integrations; import com.google.common.collect.ImmutableMap; +import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.DataModel; -import com.launchdarkly.sdk.server.interfaces.DataSource; +import com.launchdarkly.sdk.server.LDConfig; +import com.launchdarkly.sdk.server.ModelBuilders; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.ErrorInfo; import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider.State; -import com.launchdarkly.sdk.server.interfaces.DataSourceUpdates; import com.launchdarkly.sdk.server.interfaces.DataStoreStatusProvider; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.testhelpers.JsonAssertions; +import com.launchdarkly.sdk.server.subsystems.DataSource; +import com.launchdarkly.sdk.server.subsystems.DataSourceUpdateSink; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; import org.junit.Test; @@ -23,9 +24,9 @@ import java.util.function.Function; import static com.google.common.collect.Iterables.get; +import static com.launchdarkly.sdk.server.ModelBuilders.flagBuilder; +import static com.launchdarkly.sdk.server.TestComponents.clientContext; import static com.launchdarkly.testhelpers.JsonAssertions.assertJsonEquals; -import static com.launchdarkly.testhelpers.JsonAssertions.jsonProperty; -import static com.launchdarkly.testhelpers.JsonTestValue.jsonOf; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyIterable; import static org.hamcrest.Matchers.equalTo; @@ -41,10 +42,15 @@ public class TestDataTest { private CapturingDataSourceUpdates updates = new CapturingDataSourceUpdates(); + // Test implementation note: We're using the ModelBuilders test helpers to build the expected + // flag JSON. However, we have to use them in a slightly different way than we do in other tests + // (for instance, writing out an expected clause as a JSON literal), because specific data model + // classes like FeatureFlag and Clause aren't visible from the integrations package. + @Test public void initializesWithEmptyData() throws Exception { TestData td = TestData.dataSource(); - DataSource ds = td.createDataSource(null, updates); + DataSource ds = td.build(clientContext("", new LDConfig.Builder().build(), updates)); Future started = ds.start(); assertThat(started.isDone(), is(true)); @@ -64,7 +70,7 @@ public void initializesWithFlags() throws Exception { td.update(td.flag("flag1").on(true)) .update(td.flag("flag2").on(false)); - DataSource ds = td.createDataSource(null, updates); + DataSource ds = td.build(clientContext("", new LDConfig.Builder().build(), updates)); Future started = ds.start(); assertThat(started.isDone(), is(true)); @@ -76,21 +82,25 @@ public void initializesWithFlags() throws Exception { assertThat(get(data.getData(), 0).getKey(), equalTo(DataModel.FEATURES)); assertThat(get(data.getData(), 0).getValue().getItems(), iterableWithSize(2)); + ModelBuilders.FlagBuilder expectedFlag1 = flagBuilder("flag1").version(1).salt("") + .on(true).offVariation(1).fallthroughVariation(0).variations(true, false); + ModelBuilders.FlagBuilder expectedFlag2 = flagBuilder("flag2").version(1).salt("") + .on(false).offVariation(1).fallthroughVariation(0).variations(true, false); + Map flags = ImmutableMap.copyOf(get(data.getData(), 0).getValue().getItems()); ItemDescriptor flag1 = flags.get("flag1"); ItemDescriptor flag2 = flags.get("flag2"); assertThat(flag1, not(nullValue())); assertThat(flag2, not(nullValue())); - assertThat(flag1.getVersion(), equalTo(1)); - assertThat(flag2.getVersion(), equalTo(1)); - assertThat(jsonOf(flagJson(flag1)), jsonProperty("on", true)); - assertThat(jsonOf(flagJson(flag2)), jsonProperty("on", false)); + + assertJsonEquals(flagJson(expectedFlag1, 1), flagJson(flag1)); + assertJsonEquals(flagJson(expectedFlag2, 1), flagJson(flag2)); } @Test public void addsFlag() throws Exception { TestData td = TestData.dataSource(); - DataSource ds = td.createDataSource(null, updates); + DataSource ds = td.build(clientContext("", new LDConfig.Builder().build(), updates)); Future started = ds.start(); assertThat(started.isDone(), is(true)); @@ -98,13 +108,16 @@ public void addsFlag() throws Exception { td.update(td.flag("flag1").on(true)); + ModelBuilders.FlagBuilder expectedFlag = flagBuilder("flag1").version(1).salt("") + .on(true).offVariation(1).fallthroughVariation(0).variations(true, false); + assertThat(updates.upserts.size(), equalTo(1)); UpsertParams up = updates.upserts.take(); assertThat(up.kind, is(DataModel.FEATURES)); assertThat(up.key, equalTo("flag1")); ItemDescriptor flag1 = up.item; - assertThat(flag1.getVersion(), equalTo(1)); - assertThat(jsonOf(flagJson(flag1)), jsonProperty("on", true)); + + assertJsonEquals(flagJson(expectedFlag, 2), flagJson(flag1)); } @Test @@ -113,9 +126,15 @@ public void updatesFlag() throws Exception { td.update(td.flag("flag1") .on(false) .variationForUser("a", true) - .ifMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(true)); - - DataSource ds = td.createDataSource(null, updates); + .ifMatch("name", LDValue.of("Lucy")).thenReturn(true)); + // Here we're verifying that the original targets & rules are copied over if we didn't change them + + ModelBuilders.FlagBuilder expectedFlag = flagBuilder("flag1").version(1).salt("") + .on(false).offVariation(1).fallthroughVariation(0).variations(true, false) + .addTarget(0, "a").addContextTarget(ContextKind.DEFAULT, 0) + .addRule("rule0", 0, "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}"); + + DataSource ds = td.build(clientContext("", new LDConfig.Builder().build(), updates)); Future started = ds.start(); assertThat(started.isDone(), is(true)); @@ -128,200 +147,259 @@ public void updatesFlag() throws Exception { assertThat(up.kind, is(DataModel.FEATURES)); assertThat(up.key, equalTo("flag1")); ItemDescriptor flag1 = up.item; - assertThat(flag1.getVersion(), equalTo(2)); - assertThat(jsonOf(flagJson(flag1)), jsonProperty("on", true)); - - String expectedJson = "{\"trackEventsFallthrough\":false,\"deleted\":false," - + "\"variations\":[true,false],\"clientSide\":false,\"rules\":[{\"clauses\":" - + "[{\"op\":\"in\",\"negate\":false,\"values\":[\"Lucy\"],\"attribute\":\"name\"}]," - + "\"id\":\"rule0\",\"trackEvents\":false,\"variation\":0}],\"trackEvents\":false," - + "\"fallthrough\":{\"variation\":0},\"offVariation\":1,\"version\":2,\"targets\":" - + "[{\"values\":[\"a\"],\"variation\":0}],\"key\":\"flag1\",\"on\":true}"; - assertThat(jsonOf(flagJson(flag1)), JsonAssertions.jsonEquals(expectedJson)); + + expectedFlag.on(true).version(2); + assertJsonEquals(flagJson(expectedFlag, 2), flagJson(flag1)); } @Test public void flagConfigSimpleBoolean() throws Exception { - String basicProps = "\"variations\":[true,false],\"offVariation\":1"; - String onProps = basicProps + ",\"on\":true"; - String offProps = basicProps + ",\"on\":false"; - String fallthroughTrue = ",\"fallthrough\":{\"variation\":0}"; - String fallthroughFalse = ",\"fallthrough\":{\"variation\":1}"; - - verifyFlag(f -> f, onProps + fallthroughTrue); - verifyFlag(f -> f.booleanFlag(), onProps + fallthroughTrue); - verifyFlag(f -> f.on(true), onProps + fallthroughTrue); - verifyFlag(f -> f.on(false), offProps + fallthroughTrue); - verifyFlag(f -> f.variationForAll(false), onProps + fallthroughFalse); - verifyFlag(f -> f.variationForAll(true), onProps + fallthroughTrue); - - verifyFlag( - f -> f.fallthroughVariation(true).offVariation(false), - onProps + fallthroughTrue - ); + Function expectedBooleanFlag = fb -> + fb.on(true).variations(true, false).offVariation(1).fallthroughVariation(0); + verifyFlag(f -> f, expectedBooleanFlag); + verifyFlag(f -> f.booleanFlag(), expectedBooleanFlag); // already the default + verifyFlag(f -> f.on(true), expectedBooleanFlag); // already the default + verifyFlag(f -> f.on(false), fb -> expectedBooleanFlag.apply(fb).on(false)); + verifyFlag(f -> f.variationForAll(false), fb -> expectedBooleanFlag.apply(fb).fallthroughVariation(1)); + verifyFlag(f -> f.variationForAll(true), expectedBooleanFlag); // already the default + verifyFlag(f -> f.fallthroughVariation(true).offVariation(false), expectedBooleanFlag); // already the default + verifyFlag( f -> f.fallthroughVariation(false).offVariation(true), - "\"variations\":[true,false],\"on\":true,\"offVariation\":0,\"fallthrough\":{\"variation\":1}" + fb -> expectedBooleanFlag.apply(fb).fallthroughVariation(1).offVariation(0) ); } @Test public void usingBooleanConfigMethodsForcesFlagToBeBoolean() throws Exception { - String booleanProps = "\"on\":true" - + ",\"variations\":[true,false],\"offVariation\":1,\"fallthrough\":{\"variation\":0}"; + Function expectedBooleanFlag = fb -> + fb.on(true).variations(true, false).offVariation(1).fallthroughVariation(0); - verifyFlag( - f -> f.variations(LDValue.of(1), LDValue.of(2)) - .booleanFlag(), - booleanProps - ); - verifyFlag( - f -> f.variations(LDValue.of(true), LDValue.of(2)) - .booleanFlag(), - booleanProps - ); - verifyFlag( - f -> f.booleanFlag(), - booleanProps - ); + verifyFlag( + f -> f.variations(LDValue.of(1), LDValue.of(2)).booleanFlag(), + expectedBooleanFlag + ); + verifyFlag( + f -> f.variations(LDValue.of(true), LDValue.of(2)).booleanFlag(), + expectedBooleanFlag + ); + verifyFlag( + f -> f.booleanFlag(), + expectedBooleanFlag + ); } @Test public void flagConfigStringVariations() throws Exception { - String basicProps = "\"variations\":[\"red\",\"green\",\"blue\"],\"on\":true" - + ",\"offVariation\":0,\"fallthrough\":{\"variation\":2}"; - verifyFlag( f -> f.variations(THREE_STRING_VALUES).offVariation(0).fallthroughVariation(2), - basicProps + fb -> fb.variations("red", "green", "blue").on(true).offVariation(0).fallthroughVariation(2) ); } @Test public void userTargets() throws Exception { - String booleanFlagBasicProps = "\"on\":true,\"variations\":[true,false]" + - ",\"offVariation\":1,\"fallthrough\":{\"variation\":0}"; + Function expectedBooleanFlag = fb -> + fb.variations(true, false).on(true).offVariation(1).fallthroughVariation(0); + verifyFlag( f -> f.variationForUser("a", true).variationForUser("b", true), - booleanFlagBasicProps + ",\"targets\":[{\"variation\":0,\"values\":[\"a\",\"b\"]}]" + fb -> expectedBooleanFlag.apply(fb).addTarget(0, "a", "b") + .addContextTarget(ContextKind.DEFAULT, 0) ); verifyFlag( - f -> f.variationForUser("a", true).variationForUser("a", true), - booleanFlagBasicProps + ",\"targets\":[{\"variation\":0,\"values\":[\"a\"]}]" + f -> f.variationForUser("a", true).variationForUser("a", true), + fb -> expectedBooleanFlag.apply(fb).addTarget(0, "a") + .addContextTarget(ContextKind.DEFAULT, 0) ); verifyFlag( - f -> f.variationForUser("a", false).variationForUser("b", true).variationForUser("c", false), - booleanFlagBasicProps + ",\"targets\":[{\"variation\":0,\"values\":[\"b\"]}" + - ",{\"variation\":1,\"values\":[\"a\",\"c\"]}]" + f -> f.variationForUser("a", true).variationForUser("a", false), + fb -> expectedBooleanFlag.apply(fb).addTarget(1, "a") + .addContextTarget(ContextKind.DEFAULT, 1) ); verifyFlag( - f -> f.variationForUser("a", true).variationForUser("b", true).variationForUser("a", false), - booleanFlagBasicProps + ",\"targets\":[{\"variation\":0,\"values\":[\"b\"]}" + - ",{\"variation\":1,\"values\":[\"a\"]}]" + f -> f.variationForUser("a", false).variationForUser("b", true).variationForUser("c", false), + fb -> expectedBooleanFlag.apply(fb).addTarget(0, "b").addTarget(1, "a", "c") + .addContextTarget(ContextKind.DEFAULT, 0).addContextTarget(ContextKind.DEFAULT, 1) ); - - String stringFlagBasicProps = "\"variations\":[\"red\",\"green\",\"blue\"],\"on\":true" - + ",\"offVariation\":0,\"fallthrough\":{\"variation\":2}"; verifyFlag( - f -> f.variations(THREE_STRING_VALUES).offVariation(0).fallthroughVariation(2) + f -> f.variationForUser("a", true).variationForUser("b", true).variationForUser("a", false), + fb -> expectedBooleanFlag.apply(fb).addTarget(0, "b").addTarget(1, "a") + .addContextTarget(ContextKind.DEFAULT, 0).addContextTarget(ContextKind.DEFAULT, 1) + ); + + Function expectedStringFlag = fb -> + fb.variations("red", "green", "blue").on(true).offVariation(0).fallthroughVariation(2); + + verifyFlag( + f -> f.variations(THREE_STRING_VALUES).offVariation(0).fallthroughVariation(2) .variationForUser("a", 2).variationForUser("b", 2), - stringFlagBasicProps + ",\"targets\":[{\"variation\":2,\"values\":[\"a\",\"b\"]}]" + fb -> expectedStringFlag.apply(fb).addTarget(2, "a", "b") + .addContextTarget(ContextKind.DEFAULT, 2) ); verifyFlag( - f -> f.variations(THREE_STRING_VALUES).offVariation(0).fallthroughVariation(2) + f -> f.variations(THREE_STRING_VALUES).offVariation(0).fallthroughVariation(2) .variationForUser("a", 2).variationForUser("b", 1).variationForUser("c", 2), - stringFlagBasicProps + ",\"targets\":[{\"variation\":1,\"values\":[\"b\"]}" + - ",{\"variation\":2,\"values\":[\"a\",\"c\"]}]" + fb -> expectedStringFlag.apply(fb).addTarget(1, "b").addTarget(2, "a", "c") + .addContextTarget(ContextKind.DEFAULT, 1).addContextTarget(ContextKind.DEFAULT, 2) + ); + + // clear previously set targets + verifyFlag( + f -> f.variationForUser("a", true).clearTargets(), + expectedBooleanFlag + ); + } + + @Test + public void contextTargets() throws Exception { + ContextKind kind1 = ContextKind.of("org"), kind2 = ContextKind.of("other"); + + Function expectedBooleanFlag = fb -> + fb.variations(true, false).on(true).offVariation(1).fallthroughVariation(0); + + verifyFlag( + f -> f.variationForKey(kind1, "a", true).variationForKey(kind1, "b", true), + fb -> expectedBooleanFlag.apply(fb).addContextTarget(kind1, 0, "a", "b") + ); + verifyFlag( + f -> f.variationForKey(kind1, "a", true).variationForKey(kind2, "a", true), + fb -> expectedBooleanFlag.apply(fb).addContextTarget(kind1, 0, "a").addContextTarget(kind2, 0, "a") + ); + verifyFlag( + f -> f.variationForKey(kind1, "a", true).variationForKey(kind1, "a", true), + fb -> expectedBooleanFlag.apply(fb).addContextTarget(kind1, 0, "a") + ); + verifyFlag( + f -> f.variationForKey(kind1, "a", true).variationForKey(kind1, "a", false), + fb -> expectedBooleanFlag.apply(fb).addContextTarget(kind1, 1, "a") + ); + + Function expectedStringFlag = fb -> + fb.variations("red", "green", "blue").on(true).offVariation(0).fallthroughVariation(2); + + verifyFlag( + f -> f.variations(THREE_STRING_VALUES).offVariation(0).fallthroughVariation(2) + .variationForKey(kind1, "a", 2).variationForKey(kind1, "b", 2), + fb -> expectedStringFlag.apply(fb).addContextTarget(kind1, 2, "a", "b") + ); + + // clear previously set targets + verifyFlag( + f -> f.variationForKey(kind1, "a", true).clearTargets(), + expectedBooleanFlag ); } @Test public void flagRules() throws Exception { - String basicProps = "\"variations\":[true,false]" + - ",\"on\":true,\"offVariation\":1,\"fallthrough\":{\"variation\":0}"; + Function expectedBooleanFlag = fb -> + fb.variations(true, false).on(true).offVariation(1).fallthroughVariation(0); // match that returns variation 0/true - String matchReturnsVariation0 = basicProps + - ",\"rules\":[{\"id\":\"rule0\",\"variation\":0,\"trackEvents\":false,\"clauses\":[" + - "{\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"],\"negate\":false}" + - "]}]"; + Function matchReturnsVariation0 = fb -> + expectedBooleanFlag.apply(fb).addRule("rule0", 0, + "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}"); + verifyFlag( - f -> f.ifMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(true), + f -> f.ifMatch("name", LDValue.of("Lucy")).thenReturn(true), matchReturnsVariation0 ); verifyFlag( - f -> f.ifMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(0), + f -> f.ifMatch("name", LDValue.of("Lucy")).thenReturn(0), matchReturnsVariation0 ); - + // match that returns variation 1/false - String matchReturnsVariation1 = basicProps + - ",\"rules\":[{\"id\":\"rule0\",\"variation\":1,\"trackEvents\":false,\"clauses\":[" + - "{\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"],\"negate\":false}" + - "]}]"; + Function matchReturnsVariation1 = fb -> + expectedBooleanFlag.apply(fb).addRule("rule0", 1, + "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}"); + verifyFlag( - f -> f.ifMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(false), + f -> f.ifMatch("name", LDValue.of("Lucy")).thenReturn(false), matchReturnsVariation1 ); verifyFlag( - f -> f.ifMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(1), + f -> f.ifMatch("name", LDValue.of("Lucy")).thenReturn(1), matchReturnsVariation1 ); - + // negated match verifyFlag( - f -> f.ifNotMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(true), - basicProps + ",\"rules\":[{\"id\":\"rule0\",\"variation\":0,\"trackEvents\":false,\"clauses\":[" + - "{\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"],\"negate\":true}" + - "]}]" + f -> f.ifNotMatch("name", LDValue.of("Lucy")).thenReturn(true), + fb -> expectedBooleanFlag.apply(fb).addRule("rule0", 0, + "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"],\"negate\":true}") + ); + + // context kinds + verifyFlag( + f -> f.ifMatch(ContextKind.of("org"), "name", LDValue.of("Catco")).thenReturn(true), + fb -> expectedBooleanFlag.apply(fb).addRule("rule0", 0, + "{\"contextKind\":\"org\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Catco\"]}") + ); + verifyFlag( + f -> f.ifNotMatch(ContextKind.of("org"), "name", LDValue.of("Catco")).thenReturn(true), + fb -> expectedBooleanFlag.apply(fb).addRule("rule0", 0, + "{\"contextKind\":\"org\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Catco\"],\"negate\":true}") ); // multiple clauses verifyFlag( - f -> f.ifMatch(UserAttribute.NAME, LDValue.of("Lucy")) - .andMatch(UserAttribute.COUNTRY, LDValue.of("gb")) + f -> f.ifMatch("name", LDValue.of("Lucy")) + .andMatch("country", LDValue.of("gb")) .thenReturn(true), - basicProps + ",\"rules\":[{\"id\":\"rule0\",\"variation\":0,\"trackEvents\":false,\"clauses\":[" + - "{\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"],\"negate\":false}," + - "{\"attribute\":\"country\",\"op\":\"in\",\"values\":[\"gb\"],\"negate\":false}" + - "]}]" + fb -> expectedBooleanFlag.apply(fb).addRule("rule0", 0, + "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}", + "{\"contextKind\":\"user\",\"attribute\":\"country\",\"op\":\"in\",\"values\":[\"gb\"]}") ); - + verifyFlag( + f -> f.ifMatch("name", LDValue.of("Lucy")) + .andMatch("country", LDValue.of("gb")) + .thenReturn(true), + fb -> expectedBooleanFlag.apply(fb).addRule("rule0", 0, + "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}", + "{\"contextKind\":\"user\",\"attribute\":\"country\",\"op\":\"in\",\"values\":[\"gb\"]}") + ); + // multiple rules verifyFlag( - f -> f.ifMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(true) - .ifMatch(UserAttribute.NAME, LDValue.of("Mina")).thenReturn(true), - basicProps + ",\"rules\":[" - + "{\"id\":\"rule0\",\"variation\":0,\"trackEvents\":false,\"clauses\":[" + - "{\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"],\"negate\":false}" + - "]}," - + "{\"id\":\"rule1\",\"variation\":0,\"trackEvents\":false,\"clauses\":[" + - "{\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Mina\"],\"negate\":false}" + - "]}" - + "]" + f -> f.ifMatch("name", LDValue.of("Lucy")).thenReturn(true) + .ifMatch("name", LDValue.of("Mina")).thenReturn(false), + fb -> expectedBooleanFlag.apply(fb) + .addRule("rule0", 0, "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Lucy\"]}") + .addRule("rule1", 1, "{\"contextKind\":\"user\",\"attribute\":\"name\",\"op\":\"in\",\"values\":[\"Mina\"]}") + ); + + // clear previously set rules + verifyFlag( + f -> f.ifMatch("name", LDValue.of("Lucy")).thenReturn(true).clearRules(), + expectedBooleanFlag ); - } - + private void verifyFlag( - Function configureFlag, - String expectedProps - ) throws Exception { - String expectedJson = "{\"key\":\"flagkey\",\"version\":1," + expectedProps + - ",\"clientSide\":false,\"deleted\":false,\"trackEvents\":false,\"trackEventsFallthrough\":false}"; - + Function configureFlag, + Function configureExpectedFlag + ) throws Exception { + ModelBuilders.FlagBuilder expectedFlag = flagBuilder("flagkey").version(1).salt(""); + expectedFlag = configureExpectedFlag.apply(expectedFlag); + TestData td = TestData.dataSource(); - DataSource ds = td.createDataSource(null, updates); + DataSource ds = td.build(clientContext("", new LDConfig.Builder().build(), updates)); ds.start(); - + td.update(configureFlag.apply(td.flag("flagkey"))); assertThat(updates.upserts.size(), equalTo(1)); UpsertParams up = updates.upserts.take(); ItemDescriptor flag = up.item; - assertJsonEquals(expectedJson, flagJson(flag)); + assertJsonEquals(flagJson(expectedFlag, 1), flagJson(flag)); + } + + private static String flagJson(ModelBuilders.FlagBuilder flagBuilder, int version) { + return DataModel.FEATURES.serialize(new ItemDescriptor(version, flagBuilder.build())); } private static String flagJson(ItemDescriptor flag) { @@ -340,7 +418,7 @@ private static class UpsertParams { } } - private static class CapturingDataSourceUpdates implements DataSourceUpdates { + private static class CapturingDataSourceUpdates implements DataSourceUpdateSink { BlockingQueue> inits = new LinkedBlockingQueue<>(); BlockingQueue upserts = new LinkedBlockingQueue<>(); boolean valid; diff --git a/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataWithClientTest.java b/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataWithClientTest.java index 452b82c17..e0635244f 100644 --- a/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataWithClientTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/integrations/TestDataWithClientTest.java @@ -1,8 +1,7 @@ package com.launchdarkly.sdk.server.integrations; -import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; import com.launchdarkly.sdk.server.Components; import com.launchdarkly.sdk.server.LDClient; import com.launchdarkly.sdk.server.LDConfig; @@ -37,7 +36,7 @@ public void initializesWithFlag() throws Exception { td.update(td.flag("flag").on(true)); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertThat(client.boolVariation("flag", new LDUser("user"), false), is(true)); + assertThat(client.boolVariation("flag", LDContext.create("user"), false), is(true)); } } @@ -46,11 +45,11 @@ public void updatesFlag() throws Exception { td.update(td.flag("flag").on(false)); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertThat(client.boolVariation("flag", new LDUser("user"), false), is(false)); + assertThat(client.boolVariation("flag", LDContext.create("user"), false), is(false)); td.update(td.flag("flag").on(true)); - assertThat(client.boolVariation("flag", new LDUser("user"), false), is(true)); + assertThat(client.boolVariation("flag", LDContext.create("user"), false), is(true)); } } @@ -59,21 +58,21 @@ public void usesTargets() throws Exception { td.update(td.flag("flag").fallthroughVariation(false).variationForUser("user1", true)); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertThat(client.boolVariation("flag", new LDUser("user1"), false), is(true)); - assertThat(client.boolVariation("flag", new LDUser("user2"), false), is(false)); + assertThat(client.boolVariation("flag", LDContext.create("user1"), false), is(true)); + assertThat(client.boolVariation("flag", LDContext.create("user2"), false), is(false)); } } @Test public void usesRules() throws Exception { td.update(td.flag("flag").fallthroughVariation(false) - .ifMatch(UserAttribute.NAME, LDValue.of("Lucy")).thenReturn(true) - .ifMatch(UserAttribute.NAME, LDValue.of("Mina")).thenReturn(true)); + .ifMatch("name", LDValue.of("Lucy")).thenReturn(true) + .ifMatch("name", LDValue.of("Mina")).thenReturn(true)); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertThat(client.boolVariation("flag", new LDUser.Builder("user1").name("Lucy").build(), false), is(true)); - assertThat(client.boolVariation("flag", new LDUser.Builder("user2").name("Mina").build(), false), is(true)); - assertThat(client.boolVariation("flag", new LDUser.Builder("user3").name("Quincy").build(), false), is(false)); + assertThat(client.boolVariation("flag", LDContext.builder("user1").name("Lucy").build(), false), is(true)); + assertThat(client.boolVariation("flag", LDContext.builder("user2").name("Mina").build(), false), is(true)); + assertThat(client.boolVariation("flag", LDContext.builder("user3").name("Quincy").build(), false), is(false)); } } @@ -82,16 +81,16 @@ public void nonBooleanFlags() throws Exception { td.update(td.flag("flag").variations(LDValue.of("red"), LDValue.of("green"), LDValue.of("blue")) .offVariation(0).fallthroughVariation(2) .variationForUser("user1", 1) - .ifMatch(UserAttribute.NAME, LDValue.of("Mina")).thenReturn(1)); + .ifMatch("name", LDValue.of("Mina")).thenReturn(1)); try (LDClient client = new LDClient(SDK_KEY, config)) { - assertThat(client.stringVariation("flag", new LDUser.Builder("user1").name("Lucy").build(), ""), equalTo("green")); - assertThat(client.stringVariation("flag", new LDUser.Builder("user2").name("Mina").build(), ""), equalTo("green")); - assertThat(client.stringVariation("flag", new LDUser.Builder("user3").name("Quincy").build(), ""), equalTo("blue")); + assertThat(client.stringVariation("flag", LDContext.builder("user1").name("Lucy").build(), ""), equalTo("green")); + assertThat(client.stringVariation("flag", LDContext.builder("user2").name("Mina").build(), ""), equalTo("green")); + assertThat(client.stringVariation("flag", LDContext.builder("user3").name("Quincy").build(), ""), equalTo("blue")); td.update(td.flag("flag").on(false)); - assertThat(client.stringVariation("flag", new LDUser.Builder("user1").name("Lucy").build(), ""), equalTo("red")); + assertThat(client.stringVariation("flag", LDContext.builder("user1").name("Lucy").build(), ""), equalTo("red")); } } @@ -114,13 +113,13 @@ public void dataSourcePropagatesToMultipleClients() throws Exception { try (LDClient client1 = new LDClient(SDK_KEY, config)) { try (LDClient client2 = new LDClient(SDK_KEY, config)) { - assertThat(client1.boolVariation("flag", new LDUser("user"), false), is(true)); - assertThat(client2.boolVariation("flag", new LDUser("user"), false), is(true)); + assertThat(client1.boolVariation("flag", LDContext.create("user"), false), is(true)); + assertThat(client2.boolVariation("flag", LDContext.create("user"), false), is(true)); td.update(td.flag("flag").on(false)); - assertThat(client1.boolVariation("flag", new LDUser("user"), false), is(false)); - assertThat(client2.boolVariation("flag", new LDUser("user"), false), is(false)); + assertThat(client1.boolVariation("flag", LDContext.create("user"), false), is(false)); + assertThat(client2.boolVariation("flag", LDContext.create("user"), false), is(false)); } } } diff --git a/src/test/java/com/launchdarkly/sdk/server/interfaces/BigSegmentMembershipBuilderTest.java b/src/test/java/com/launchdarkly/sdk/server/interfaces/BigSegmentMembershipBuilderTest.java index 82cff2832..ea0941e37 100644 --- a/src/test/java/com/launchdarkly/sdk/server/interfaces/BigSegmentMembershipBuilderTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/interfaces/BigSegmentMembershipBuilderTest.java @@ -2,14 +2,14 @@ import static com.launchdarkly.sdk.server.TestUtil.assertFullyEqual; import static com.launchdarkly.sdk.server.TestUtil.assertFullyUnequal; -import static com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.createMembershipFromSegmentRefs; +import static com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.createMembershipFromSegmentRefs; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import com.launchdarkly.sdk.server.interfaces.BigSegmentStoreTypes.Membership; +import com.launchdarkly.sdk.server.subsystems.BigSegmentStoreTypes.Membership; import org.junit.Test; diff --git a/src/test/java/com/launchdarkly/sdk/server/interfaces/DataStoreTypesTest.java b/src/test/java/com/launchdarkly/sdk/server/interfaces/DataStoreTypesTest.java index 51dc94d90..b51ce329b 100644 --- a/src/test/java/com/launchdarkly/sdk/server/interfaces/DataStoreTypesTest.java +++ b/src/test/java/com/launchdarkly/sdk/server/interfaces/DataStoreTypesTest.java @@ -3,11 +3,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedMap; import com.launchdarkly.sdk.server.DataModel; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems; -import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.SerializedItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.DataKind; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.FullDataSet; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.ItemDescriptor; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.KeyedItems; +import com.launchdarkly.sdk.server.subsystems.DataStoreTypes.SerializedItemDescriptor; import com.launchdarkly.testhelpers.TypeBehavior; import org.junit.Test;