Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit e5f5d7e

Browse files
prepare 5.5.0 release (#237)
1 parent 2f539af commit e5f5d7e

File tree

15 files changed

+627
-92
lines changed

15 files changed

+627
-92
lines changed

build.gradle

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ ext.versions = [
7272
"gson": "2.7",
7373
"guava": "30.1-jre",
7474
"jackson": "2.11.2",
75-
"launchdarklyJavaSdkCommon": "1.1.1",
75+
"launchdarklyJavaSdkCommon": "1.2.0",
7676
"okhttp": "4.8.1", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource
7777
"okhttpEventsource": "2.3.1",
7878
"slf4j": "1.7.21",
@@ -83,6 +83,16 @@ ext.versions = [
8383
// Add dependencies to "libraries.internal" that are not exposed in our public API. These
8484
// will be completely omitted from the "thin" jar, and will be embedded with shaded names
8585
// in the other two SDK jars.
86+
//
87+
// Note that Gson is included here but Jackson is not, even though there is some Jackson
88+
// helper code in java-sdk-common. The reason is that the SDK always needs to use Gson for
89+
// its own usual business, so (except in the "thin" jar) we will be embedding a shaded
90+
// copy of Gson; but we do not use Jackson normally, we just provide those helpers for use
91+
// by applications that are already using Jackson. So we do not want to embed it and we do
92+
// not want it to show up as a dependency at all in our pom (and it's been excluded from
93+
// the launchdarkly-java-sdk-common pom for the same reason). However, we do include
94+
// Jackson in "libraries.optional" because we need to generate OSGi optional import
95+
// headers for it.
8696
libraries.internal = [
8797
"com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}",
8898
"commons-codec:commons-codec:${versions.commonsCodec}",
@@ -96,7 +106,15 @@ libraries.internal = [
96106
// Add dependencies to "libraries.external" that are exposed in our public API, or that have
97107
// global state that must be shared between the SDK and the caller.
98108
libraries.external = [
99-
"org.slf4j:slf4j-api:${versions.slf4j}",
109+
"org.slf4j:slf4j-api:${versions.slf4j}"
110+
]
111+
112+
// Add dependencies to "libraries.optional" that are not exposed in our public API and are
113+
// *not* embedded in the SDK jar. These are for optional things that will only work if
114+
// they are already in the application classpath; we do not want show them as a dependency
115+
// because that would cause them to be pulled in automatically in all builds. The reason
116+
// we need to even mention them here at all is for the sake of OSGi optional import headers.
117+
libraries.optional = [
100118
"com.fasterxml.jackson.core:jackson-core:${versions.jackson}",
101119
"com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
102120
]
@@ -114,24 +132,29 @@ libraries.test = [
114132
"com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
115133
]
116134

135+
configurations {
136+
// We need to define "internal" as a custom configuration that contains the same things as
137+
// "implementation", because "implementation" has special behavior in Gradle that prevents us
138+
// from referencing it the way we do in shadeDependencies().
139+
internal.extendsFrom implementation
140+
optional
141+
imports
142+
}
143+
117144
dependencies {
118145
implementation libraries.internal
119146
api libraries.external
120147
testImplementation libraries.test, libraries.internal, libraries.external
148+
optional libraries.optional
121149

122150
commonClasses "com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}"
123151
commonDoc "com.launchdarkly:launchdarkly-java-sdk-common:${versions.launchdarklyJavaSdkCommon}:sources"
124152

125153
// Unlike what the name might suggest, the "shadow" configuration specifies dependencies that
126154
// should *not* be shaded by the Shadow plugin when we build our shaded jars.
127-
shadow libraries.external
128-
}
155+
shadow libraries.external, libraries.optional
129156

130-
configurations {
131-
// We need to define "internal" as a custom configuration that contains the same things as
132-
// "implementation", because "implementation" has special behavior in Gradle that prevents us
133-
// from referencing it the way we do in shadeDependencies().
134-
internal.extendsFrom implementation
157+
imports libraries.external
135158
}
136159

137160
checkstyle {
@@ -171,7 +194,6 @@ shadowJar {
171194

172195
dependencies {
173196
exclude(dependency('org.slf4j:.*:.*'))
174-
exclude(dependency('com.fasterxml.jackson.core:.*:.*'))
175197
}
176198

177199
// Kotlin metadata for shaded classes should not be included - it confuses IDEs
@@ -185,7 +207,7 @@ shadowJar {
185207
shadeDependencies(project.tasks.shadowJar)
186208
// Note that "configurations.shadow" is the same as "libraries.external", except it contains
187209
// objects with detailed information about the resolved dependencies.
188-
addOsgiManifest(project.tasks.shadowJar, [ project.configurations.shadow ], [])
210+
addOsgiManifest(project.tasks.shadowJar, [ project.configurations.imports ], [])
189211
}
190212

191213
doLast {
@@ -208,17 +230,17 @@ task shadowJarAll(type: com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJ
208230
exclude '**/*.kotlin_builtins'
209231

210232
dependencies {
211-
exclude(dependency('com.fasterxml.jackson.core:.*:.*'))
233+
// Currently we don't need to exclude anything - SLF4J will be embedded, unshaded
212234
}
213235

214236
// doFirst causes the following steps to be run during Gradle's execution phase rather than the
215237
// configuration phase; this is necessary because they access the build products
216238
doFirst {
217239
shadeDependencies(project.tasks.shadowJarAll)
218-
// The "all" jar exposes its bundled Gson and SLF4j dependencies as exports - but, like the
219-
// default jar, it *also* imports them ("self-wiring"), which allows the bundle to use a
240+
// The "all" jar exposes its bundled SLF4j dependency as an export - but, like the
241+
// default jar, it *also* imports it ("self-wiring"), which allows the bundle to use a
220242
// higher version if one is provided by another bundle.
221-
addOsgiManifest(project.tasks.shadowJarAll, [ project.configurations.shadow ], [ project.configurations.shadow ])
243+
addOsgiManifest(project.tasks.shadowJarAll, [ project.configurations.imports ], [ project.configurations.imports ])
222244
}
223245

224246
doLast {
@@ -370,14 +392,18 @@ def addOsgiManifest(jarTask, List<Configuration> importConfigs, List<Configurati
370392
// Since we're not currently able to use bnd or the Gradle OSGi plugin, we're not discovering
371393
// imports by looking at the actual code; instead, we're just importing whatever packages each
372394
// dependency is exporting (if it has an OSGi manifest) or every package in the dependency (if
373-
// it doesn't). We also always add *optional* imports for Gson, so that GsonTypeAdapters will
374-
// work *if* Gson is present externally.
395+
// it doesn't).
375396
def imports = forEachArtifactAndVisiblePackage(importConfigs, { a, p ->
376397
bundleImport(p, a.moduleVersion.id.version, nextMajorVersion(a.moduleVersion.id.version))
377398
}) + systemPackageImports
378-
imports += "com.google.gson;resolution:=optional"
379-
imports += "com.google.gson.reflect;resolution:=optional"
380-
imports += "com.google.gson.stream;resolution:=optional"
399+
400+
// We also always add *optional* imports for Gson and Jackson, so that GsonTypeAdapters and
401+
// JacksonTypeAdapters will work *if* Gson or Jackson is present externally. Currently we
402+
// are hard-coding the Gson packages but there is probably a better way.
403+
def optImports = [ "com.google.gson", "com.google.gson.reflect", "com.google.gson.stream" ]
404+
forEachArtifactAndVisiblePackage([ configurations.optional ]) { a, p -> optImports += p }
405+
imports += (optImports.join(";") + ";resolution:=optional" )
406+
381407
attributes("Import-Package": imports.join(","))
382408

383409
// Similarly, we're adding package exports for every package in whatever libraries we're
@@ -391,9 +417,7 @@ def addOsgiManifest(jarTask, List<Configuration> importConfigs, List<Configurati
391417
}
392418

393419
def bundleImport(packageName, importVersion, versionLimit) {
394-
def optional = packageName.startsWith("com.fasterxml.jackson")
395-
packageName + ";version=\"[" + importVersion + "," + versionLimit + ")\"" +
396-
(optional ? ";resolution:=optional" : "")
420+
packageName + ";version=\"[" + importVersion + "," + versionLimit + ")\""
397421
}
398422

399423
def bundleExport(packageName, exportVersion) {

packaging-test/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ clean:
6767
# SECONDEXPANSION is needed so we can use "$@" inside a variable in the prerequisite list of the test targets
6868
.SECONDEXPANSION:
6969

70-
test-all-jar test-default-jar test-thin-jar: $$@-classes get-sdk-dependencies $$(RUN_JARS_$$@) $(TEST_APP_JAR) $(FELIX_DIR)
70+
test-all-jar test-default-jar test-thin-jar: $$@-classes $(TEST_APP_JAR) get-sdk-dependencies $$(RUN_JARS_$$@) $(FELIX_DIR)
7171
@$(call caption,$@)
7272
@./run-non-osgi-test.sh $(RUN_JARS_$@)
7373
@./run-osgi-test.sh $(RUN_JARS_$@)
@@ -113,6 +113,7 @@ $(SDK_THIN_JAR):
113113
cd .. && ./gradlew jar
114114

115115
$(TEST_APP_JAR): $(SDK_THIN_JAR) $(TEST_APP_SOURCES) | $(TEMP_DIR)
116+
mkdir -p $(TEMP_DIR)/dependencies-app
116117
cd test-app && ../../gradlew jar
117118
cp $(BASE_DIR)/test-app/build/libs/test-app-*.jar $@
118119

@@ -121,6 +122,7 @@ get-sdk-dependencies: $(TEMP_DIR)/dependencies-all $(TEMP_DIR)/dependencies-exte
121122
$(TEMP_DIR)/dependencies-all: | $(TEMP_DIR)
122123
[ -d $@ ] || mkdir -p $@
123124
cd .. && ./gradlew exportDependencies
125+
cp $(TEMP_DIR)/dependencies-app/*.jar $@
124126

125127
$(TEMP_DIR)/dependencies-external: $(TEMP_DIR)/dependencies-all
126128
[ -d $@ ] || mkdir -p $@
@@ -132,6 +134,7 @@ $(TEMP_DIR)/dependencies-internal: $(TEMP_DIR)/dependencies-all
132134
[ -d $@ ] || mkdir -p $@
133135
cp $(TEMP_DIR)/dependencies-all/*.jar $@
134136
rm $@/slf4j*.jar
137+
rm $@/jackson*.jar
135138

136139
$(FELIX_JAR): $(FELIX_DIR)
137140

packaging-test/test-app/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ dependencies {
4141
osgiRuntime "org.slf4j:slf4j-simple:1.7.22"
4242
}
4343

44+
task exportDependencies(type: Copy, dependsOn: compileJava) {
45+
into "../temp/dependencies-app"
46+
from configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts.collect { it.file }
47+
}
48+
4449
jar {
4550
bnd(
4651
// This consumer-policy directive completely turns off version checking for the test app's
@@ -57,6 +62,8 @@ jar {
5762
',com.google.gson;resolution:=optional' +
5863
',com.fasterxml.jackson.*;resolution:=optional'
5964
)
65+
66+
finalizedBy(exportDependencies)
6067
}
6168

6269
runOsgi {

src/main/java/com/launchdarkly/sdk/server/DataModel.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,23 @@ void setPreprocessed(EvaluatorPreprocessing.ClauseExtra preprocessed) {
344344
static final class Rollout {
345345
private List<WeightedVariation> variations;
346346
private UserAttribute bucketBy;
347+
private RolloutKind kind;
348+
private Integer seed;
347349

348350
Rollout() {}
349351

350-
Rollout(List<WeightedVariation> variations, UserAttribute bucketBy) {
352+
Rollout(List<WeightedVariation> variations, UserAttribute bucketBy, RolloutKind kind) {
351353
this.variations = variations;
352354
this.bucketBy = bucketBy;
355+
this.kind = kind;
356+
this.seed = null;
357+
}
358+
359+
Rollout(List<WeightedVariation> variations, UserAttribute bucketBy, RolloutKind kind, Integer seed) {
360+
this.variations = variations;
361+
this.bucketBy = bucketBy;
362+
this.kind = kind;
363+
this.seed = seed;
353364
}
354365

355366
// Guaranteed non-null
@@ -360,6 +371,18 @@ List<WeightedVariation> getVariations() {
360371
UserAttribute getBucketBy() {
361372
return bucketBy;
362373
}
374+
375+
RolloutKind getKind() {
376+
return this.kind;
377+
}
378+
379+
Integer getSeed() {
380+
return this.seed;
381+
}
382+
383+
boolean isExperiment() {
384+
return kind == RolloutKind.experiment;
385+
}
363386
}
364387

365388
/**
@@ -389,12 +412,14 @@ Rollout getRollout() {
389412
static final class WeightedVariation {
390413
private int variation;
391414
private int weight;
415+
private boolean untracked;
392416

393417
WeightedVariation() {}
394418

395-
WeightedVariation(int variation, int weight) {
419+
WeightedVariation(int variation, int weight, boolean untracked) {
396420
this.variation = variation;
397421
this.weight = weight;
422+
this.untracked = untracked;
398423
}
399424

400425
int getVariation() {
@@ -404,6 +429,10 @@ int getVariation() {
404429
int getWeight() {
405430
return weight;
406431
}
432+
433+
boolean isUntracked() {
434+
return untracked;
435+
}
407436
}
408437

409438
@JsonAdapter(JsonHelpers.PostProcessingDeserializableTypeAdapterFactory.class)
@@ -511,4 +540,13 @@ static enum Operator {
511540
semVerGreaterThan,
512541
segmentMatch
513542
}
543+
544+
/**
545+
* This enum is all lowercase so that when it is automatically deserialized from JSON,
546+
* the lowercase properties properly map to these enumerations.
547+
*/
548+
static enum RolloutKind {
549+
rollout,
550+
experiment
551+
}
514552
}

src/main/java/com/launchdarkly/sdk/server/Evaluator.java

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.launchdarkly.sdk.LDUser;
77
import com.launchdarkly.sdk.LDValue;
88
import com.launchdarkly.sdk.LDValueType;
9+
import com.launchdarkly.sdk.EvaluationReason.Kind;
10+
import com.launchdarkly.sdk.server.DataModel.WeightedVariation;
911
import com.launchdarkly.sdk.server.interfaces.Event;
1012

1113
import org.slf4j.Logger;
@@ -15,6 +17,7 @@
1517
import java.util.Set;
1618

1719
import static com.launchdarkly.sdk.EvaluationDetail.NO_VARIATION;
20+
import static com.launchdarkly.sdk.server.EvaluatorBucketing.bucketUser;
1821

1922
/**
2023
* Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
@@ -221,13 +224,52 @@ private EvalResult getOffValue(DataModel.FeatureFlag flag, EvaluationReason reas
221224
}
222225

223226
private EvalResult getValueForVariationOrRollout(DataModel.FeatureFlag flag, DataModel.VariationOrRollout vr, LDUser user, EvaluationReason reason) {
224-
Integer index = EvaluatorBucketing.variationIndexForUser(vr, user, flag.getKey(), flag.getSalt());
225-
if (index == null) {
227+
int variation = -1;
228+
boolean inExperiment = false;
229+
Integer maybeVariation = vr.getVariation();
230+
if (maybeVariation != null) {
231+
variation = maybeVariation.intValue();
232+
} else {
233+
DataModel.Rollout rollout = vr.getRollout();
234+
if (rollout != null && !rollout.getVariations().isEmpty()) {
235+
float bucket = bucketUser(rollout.getSeed(), user, flag.getKey(), rollout.getBucketBy(), flag.getSalt());
236+
float sum = 0F;
237+
for (DataModel.WeightedVariation wv : rollout.getVariations()) {
238+
sum += (float) wv.getWeight() / 100000F;
239+
if (bucket < sum) {
240+
variation = wv.getVariation();
241+
inExperiment = vr.getRollout().isExperiment() && !wv.isUntracked();
242+
break;
243+
}
244+
}
245+
if (variation < 0) {
246+
// The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
247+
// to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
248+
// data could contain buckets that don't actually add up to 100000. Rather than returning an error in
249+
// this case (or changing the scaling, which would potentially change the results for *all* users), we
250+
// will simply put the user in the last bucket.
251+
WeightedVariation lastVariation = rollout.getVariations().get(rollout.getVariations().size() - 1);
252+
variation = lastVariation.getVariation();
253+
inExperiment = vr.getRollout().isExperiment() && !lastVariation.isUntracked();
254+
}
255+
}
256+
}
257+
258+
if (variation < 0) {
226259
logger.error("Data inconsistency in feature flag \"{}\": variation/rollout object with no variation or rollout", flag.getKey());
227260
return EvalResult.error(EvaluationReason.ErrorKind.MALFORMED_FLAG);
228261
} else {
229-
return getVariation(flag, index, reason);
262+
return getVariation(flag, variation, inExperiment ? experimentize(reason) : reason);
263+
}
264+
}
265+
266+
private EvaluationReason experimentize(EvaluationReason reason) {
267+
if (reason.getKind() == Kind.FALLTHROUGH) {
268+
return EvaluationReason.fallthrough(true);
269+
} else if (reason.getKind() == Kind.RULE_MATCH) {
270+
return EvaluationReason.ruleMatch(reason.getRuleIndex(), reason.getRuleId(), true);
230271
}
272+
return reason;
231273
}
232274

233275
private boolean ruleMatchesUser(DataModel.FeatureFlag flag, DataModel.Rule rule, LDUser user) {
@@ -344,7 +386,7 @@ private boolean segmentRuleMatchesUser(DataModel.SegmentRule segmentRule, LDUser
344386
}
345387

346388
// All of the clauses are met. See if the user buckets in
347-
double bucket = EvaluatorBucketing.bucketUser(user, segmentKey, segmentRule.getBucketBy(), salt);
389+
double bucket = EvaluatorBucketing.bucketUser(null, user, segmentKey, segmentRule.getBucketBy(), salt);
348390
double weight = (double)segmentRule.getWeight() / 100000.0;
349391
return bucket < weight;
350392
}

0 commit comments

Comments
 (0)