Skip to content

Commit bc5b610

Browse files
authored
feat: Update dependencies and spec support. (#19)
1 parent 351302c commit bc5b610

File tree

17 files changed

+353
-230
lines changed

17 files changed

+353
-230
lines changed

.circleci/config.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ workflows:
77
test:
88
jobs:
99
- build-linux
10-
- test-linux:
11-
name: Java 8 - Linux - OpenJDK
12-
docker-image: cimg/openjdk:8.0
13-
requires:
14-
- build-linux
1510
- test-linux:
1611
name: Java 11 - Linux - OpenJDK
1712
docker-image: cimg/openjdk:11.0
@@ -36,7 +31,7 @@ workflows:
3631
jobs:
3732
build-linux:
3833
docker:
39-
- image: cimg/openjdk:8.0
34+
- image: cimg/openjdk:11.0
4035
steps:
4136
- checkout
4237
- run: java -version
@@ -81,7 +76,7 @@ jobs:
8176

8277
packaging:
8378
docker:
84-
- image: cimg/openjdk:8.0
79+
- image: cimg/openjdk:11.0
8580
steps:
8681
- run: java -version
8782
- run: sudo apt-get install make -y -q

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ Your project will need compatible versions of the LaunchDarkly Server-Side SDK f
2424

2525
Example gradle dependencies:
2626
```groovy
27-
implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[6.0.0, 7.0.0)'
28-
implementation 'dev.openfeature:sdk:[1.2.0,2.0.0)'
27+
implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[7.1.0, 8.0.0)'
28+
implementation 'dev.openfeature:sdk:[1.7.0,2.0.0)'
2929
```
3030

3131
### Installation
@@ -54,8 +54,7 @@ import com.launchdarkly.openfeature.serverprovider.Provider;
5454

5555
public class Main {
5656
public static void main(String[] args) {
57-
LDClient ldClient = new LDClient("my-sdk-key");
58-
OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient));
57+
OpenFeatureAPI.getInstance().setProvider(new Provider("my-sdk-key"));
5958

6059
// Refer to OpenFeature documentation for getting a client and performing evaluations.
6160
}
@@ -86,6 +85,16 @@ There are several other attributes which have special functionality within a sin
8685
- A key of `anonymous`. Must be a boolean value. [Equivalent to the 'anonymous' builder method in the SDK.](https://launchdarkly.github.io/java-server-sdk/com/launchdarkly/sdk/ContextBuilder.html#anonymous(boolean))
8786
- A key of `name`. Must be a string. [Equivalent to the 'name' builder method in the SDK.](https://launchdarkly.github.io/java-server-sdk/com/launchdarkly/sdk/ContextBuilder.html#name(java.lang.String))
8887

88+
### Initialization and Shutdown
89+
90+
The LaunchDarkly supports Initialization and Shutdown using the OpenFeature API. The provider begins initialization as soon as it is constructed, and the underlying LaunchDarkly SDK will block execution based on the configured start wait time. If you wish to defer the blocking behavior, then you can use the `startWait` function when building the `LDConfig`.
91+
92+
OpenFeature will report when the provider is ready, and additionally the `setProviderAndWait` function of the OpenFeature
93+
API can be used to wait until the provider is ready, or it has encountered a permanent error.
94+
95+
It the provider has been shutdown, because the OpenFeature API has been shutdown, or because the provider was no longer in use by the OpenFeature API, then the underlying LaunchDarkly SDK will be closed.
96+
This is an important consideration if you are using the `getLdClient` method of the provider to access the underlying SDK instance.
97+
8998
### Examples
9099

91100
#### A single user context

build.gradle

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ java {
1919
repositories {
2020
mavenLocal()
2121
mavenCentral()
22+
// Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots:
23+
maven { url "https://oss.sonatype.org/content/groups/public/" }
2224
}
2325

2426
test {
@@ -41,6 +43,15 @@ checkstyle {
4143
checkstyleTest.enabled = false
4244
}
4345

46+
task generateJava(type: Copy) {
47+
// This updates Version.java
48+
from 'src/templates/java'
49+
into "src/main/java"
50+
filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [VERSION: version.toString()])
51+
}
52+
53+
compileJava.dependsOn 'generateJava'
54+
4455
publishing {
4556
publications {
4657
mavenJava(MavenPublication) {
@@ -111,11 +122,19 @@ dependencies {
111122
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
112123
implementation 'com.google.guava:guava:23.0'
113124

114-
implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[6.0.0, 7.0.0)'
115-
implementation 'dev.openfeature:sdk:[1.2.0,2.0.0)'
125+
implementation group: 'com.launchdarkly', name: 'launchdarkly-java-server-sdk', version: '[7.1.0, 8.0.0)'
126+
127+
implementation 'dev.openfeature:sdk:[1.7.0,2.0.0)'
116128

117129
// Use JUnit test framework
118-
testImplementation 'junit:junit:4.12'
130+
testImplementation(platform('org.junit:junit-bom:5.10.0'))
131+
testImplementation('org.junit.jupiter:junit-jupiter')
119132
testImplementation "org.mockito:mockito-core:3.+"
120133
}
121134

135+
test {
136+
useJUnitPlatform()
137+
testLogging {
138+
events "passed", "skipped", "failed"
139+
}
140+
}

src/main/java/com/launchdarkly/openfeature/serverprovider/EvaluationContextConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ private String getTargetingKey(String targetingKey, Value keyAsValue) {
9090
targetingKey = !Objects.equals(targetingKey, "") ? targetingKey : keyAsValue.asString();
9191
}
9292

93-
if (targetingKey == null || targetingKey.equals("")) {
93+
if (targetingKey == null || targetingKey.isEmpty()) {
9494
logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the type " + "must be a string.");
9595
}
9696
return targetingKey;
Lines changed: 123 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,36 @@
11
package com.launchdarkly.openfeature.serverprovider;
22

3-
import com.launchdarkly.logging.LDLogAdapter;
43
import com.launchdarkly.logging.LDLogger;
54
import com.launchdarkly.sdk.EvaluationDetail;
65
import com.launchdarkly.sdk.LDValue;
6+
import com.launchdarkly.sdk.server.Components;
77
import com.launchdarkly.sdk.server.LDClient;
8+
import com.launchdarkly.sdk.server.LDConfig;
9+
import com.launchdarkly.sdk.server.interfaces.DataSourceStatusProvider;
810
import com.launchdarkly.sdk.server.interfaces.LDClientInterface;
9-
import com.launchdarkly.sdk.server.subsystems.LoggingConfiguration;
1011
import dev.openfeature.sdk.*;
1112

13+
import java.io.IOException;
14+
import java.time.temporal.ChronoUnit;
15+
import java.util.Collections;
16+
import java.util.concurrent.TimeoutException;
17+
1218
/**
1319
* An OpenFeature {@link FeatureProvider} which enables the use of the LaunchDarkly Server-Side SDK for Java
1420
* with OpenFeature.
1521
* <pre><code>
16-
*import dev.openfeature.sdk.OpenFeatureAPI;
17-
*import com.launchdarkly.sdk.server.LDClient;
22+
* import dev.openfeature.sdk.OpenFeatureAPI;
1823
*
19-
*public class Main {
24+
* public class Main {
2025
* public static void main(String[] args) {
21-
* LDClient ldClient = new LDClient("my-sdk-key");
22-
* OpenFeatureAPI.getInstance().setProvider(new Provider(ldClient));
26+
* OpenFeatureAPI.getInstance().setProvider(new Provider("fake-key"));
2327
*
2428
* // Refer to OpenFeature documentation for getting a client and performing evaluations.
2529
* }
26-
*}
30+
* }
2731
* </code></pre>
2832
*/
29-
public class Provider implements FeatureProvider {
33+
public class Provider extends EventProvider {
3034
private static final class ProviderMetaData implements Metadata {
3135
@Override
3236
public String getName() {
@@ -43,39 +47,38 @@ public String getName() {
4347

4448
private final LDClientInterface client;
4549

50+
private ProviderState state = ProviderState.NOT_READY;
51+
4652
/**
47-
* Create a provider with the given LaunchDarkly client and provider configuration.
48-
* <pre><code>
49-
* // Using the provider with a custom log level.
50-
* new Provider(ldclient, ProviderConfiguration
51-
* .builder()
52-
* .logging(Components.logging().level(LDLogLevel.INFO)
53-
* .build());
54-
* </code></pre>
53+
* Create a provider with the specified SDK and default configuration.
54+
* <p>
55+
* If you need to specify any configuration use {@link Provider#Provider(String, LDConfig)} instead.
5556
*
56-
* @param client A {@link LDClient} instance.
57-
* @param config Configuration for the provider.
57+
* @param sdkKey the SDK key for your LaunchDarkly environment
5858
*/
59-
public Provider(LDClientInterface client, ProviderConfiguration config) {
60-
this.client = client;
61-
LoggingConfiguration loggingConfig = config.getLoggingConfigurationFactory().build(null);
62-
LDLogAdapter adapter = loggingConfig.getLogAdapter();
63-
logger = LDLogger.withAdapter(adapter, loggingConfig.getBaseLoggerName());
64-
65-
evaluationContextConverter = new EvaluationContextConverter(logger);
66-
evaluationDetailConverter = new EvaluationDetailConverter(logger);
67-
valueConverter = new ValueConverter(logger);
59+
public Provider(String sdkKey) {
60+
this(sdkKey, new LDConfig.Builder().build());
6861
}
6962

7063
/**
71-
* Create a provider with the given LaunchDarkly client.
72-
* <p>
73-
* The provider will be created with default configuration.
64+
* Crate a provider with the specified SDK key and configuration.
7465
*
75-
* @param client A {@link LDClient} instance.
66+
* @param sdkKey the SDK key for your LaunchDarkly environment
67+
* @param config a client configuration object
7668
*/
77-
public Provider(LDClientInterface client) {
78-
this(client, ProviderConfiguration.builder().build());
69+
public Provider(String sdkKey, LDConfig config) {
70+
this(new LDClient(sdkKey, LDConfig.Builder.fromConfig(config)
71+
.wrapper(Components.wrapperInfo()
72+
.wrapperName("open-feature-java-server")
73+
.wrapperVersion(Version.SDK_VERSION)).build()));
74+
}
75+
76+
Provider(LDClientInterface client) {
77+
this.client = client;
78+
logger = client.getLogger();
79+
evaluationContextConverter = new EvaluationContextConverter(logger);
80+
evaluationDetailConverter = new EvaluationDetailConverter(logger);
81+
valueConverter = new ValueConverter(logger);
7982
}
8083

8184
@Override
@@ -86,41 +89,121 @@ public Metadata getMetadata() {
8689
@Override
8790
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
8891
EvaluationDetail<Boolean> detail
89-
= this.client.boolVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
92+
= this.client.boolVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
9093

9194
return evaluationDetailConverter.toEvaluationDetails(detail);
9295
}
9396

9497
@Override
9598
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
9699
EvaluationDetail<String> detail
97-
= this.client.stringVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
100+
= this.client.stringVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
98101

99102
return evaluationDetailConverter.toEvaluationDetails(detail);
100103
}
101104

102105
@Override
103106
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
104107
EvaluationDetail<Integer> detail
105-
= this.client.intVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
108+
= this.client.intVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
106109

107110
return evaluationDetailConverter.toEvaluationDetails(detail);
108111
}
109112

110113
@Override
111114
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
112115
EvaluationDetail<Double> detail
113-
= this.client.doubleVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
116+
= this.client.doubleVariationDetail(key, evaluationContextConverter.toLdContext(ctx), defaultValue);
114117

115118
return evaluationDetailConverter.toEvaluationDetails(detail);
116119
}
117120

118121
@Override
119122
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
120123
EvaluationDetail<LDValue> detail
121-
= this.client.jsonValueVariationDetail(
122-
key, evaluationContextConverter.toLdContext(ctx), valueConverter.toLdValue(defaultValue));
124+
= this.client.jsonValueVariationDetail(
125+
key, evaluationContextConverter.toLdContext(ctx), valueConverter.toLdValue(defaultValue));
123126

124127
return evaluationDetailConverter.toEvaluationDetailsLdValue(detail);
125128
}
129+
130+
@Override
131+
public ProviderState getState() {
132+
return state;
133+
}
134+
135+
@Override
136+
public void initialize(EvaluationContext evaluationContext) throws Exception {
137+
// If we are ready, then set the state. Don't return, because we still need to listen for future
138+
// changes.
139+
if (client.isInitialized()) {
140+
state = ProviderState.READY;
141+
}
142+
143+
client.getFlagTracker().addFlagChangeListener(detail -> {
144+
emitProviderConfigurationChanged(
145+
ProviderEventDetails.builder().flagsChanged(Collections.singletonList(detail.getKey())).build());
146+
});
147+
// Listen for future status changes.
148+
client.getDataSourceStatusProvider().addStatusListener((res) -> {
149+
switch (res.getState()) {
150+
// We will not re-enter INITIALIZING, but it is here to make the switch exhaustive.
151+
case INITIALIZING: {
152+
}
153+
break;
154+
case INTERRUPTED: {
155+
state = ProviderState.STALE;
156+
var message = res.getLastError() != null ? res.getLastError().getMessage() : "encountered an unknown error";
157+
emitProviderStale(ProviderEventDetails.builder().message(message).build());
158+
}
159+
break;
160+
case VALID: {
161+
// If we are ready, then we don't want to emit it again. Other conditions we may be updating the
162+
// reason we are stale or interrupted, so we want to emit an event each time.
163+
if (state != ProviderState.READY) {
164+
state = ProviderState.READY;
165+
emitProviderReady(ProviderEventDetails.builder().build());
166+
}
167+
}
168+
break;
169+
case OFF: {
170+
// Currently there is not a shutdown state.
171+
// Our client/provider cannot be restarted, so we just go to error.
172+
state = ProviderState.ERROR;
173+
emitProviderError(ProviderEventDetails.builder().message("Provider shutdown").build());
174+
}
175+
}
176+
});
177+
if (state == ProviderState.READY) {
178+
return;
179+
}
180+
181+
boolean initialized = client.getDataSourceStatusProvider().waitFor(DataSourceStatusProvider.State.VALID,
182+
ChronoUnit.FOREVER.getDuration());
183+
184+
if (!initialized) {
185+
// Here we throw an exception for the OpenFeature SDK, which will handle emitting an event.
186+
throw new RuntimeException("Failed to initialize LaunchDarkly client.");
187+
}
188+
}
189+
190+
@Override
191+
public void shutdown() {
192+
try {
193+
client.close();
194+
} catch (IOException e) {
195+
throw new RuntimeException(e);
196+
}
197+
}
198+
199+
/**
200+
* Get the LaunchDarkly client associated with this provider.
201+
* <p>
202+
* This can be used to access LaunchDarkly features which are not available in OpenFeature.
203+
*
204+
* @return the launchdarkly client instance
205+
*/
206+
public LDClientInterface getLdClient() {
207+
return client;
208+
}
126209
}

0 commit comments

Comments
 (0)