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

Commit 1392dea

Browse files
LaunchDarklyReleaseBoteli-darklyLaunchDarklyCIgwhelanLDssrm
authored
prepare 5.9.1 release (#268)
* (5.0) add HTTP default headers method + some component refactoring * don't need to pass the whole config object to describeConfiguration() * simplify test logic for HTTP headers * (5.0) final test coverage improvements, for now, with enforcement * re-simplify DataBuilder * increase timeouts * misc fixes * rm unnecessary override * indents * update benchmark code for API change * support loading file data from a classpath resource * update metadata so Releaser knows about 4.x branch * minor test fixes * make class final * rm beta changelog items * test data source * more info about coverage in CONTRIBUTING.md * misc fixes/tests * use java-sdk-common 1.0.0 * use okhttp-eventsource 2.3.0 * use okhttp-eventsource 2.3.1 for thread fix * fix flaky tests due to change in EventSource error reporting * remove support for indirect put and indirect patch * fix typo in javadoc example code * clean up polling logic, fix status updating after an outage, don't reinit store unnecessarily (#256) * slightly change semantics of boolean setters, improve tests, misc cleanup * avoid NPEs if LDUser was deserialized by Gson (#257) * avoid NPEs if LDUser was deserialized by Gson * add test * fix release metadata * prepare 4.14.1 release (#200) * Releasing version 4.14.1 * exclude Kotlin metadata from jar + fix misc Gradle problems * update CI and Gradle to test with newer JDKs (#259) * update okhttp to 3.14.9 (fixes incompatibility with OpenJDK 8.0.252) * prepare 4.14.2 release (#205) * Releasing version 4.14.2 * update okhttp to 4.8.1 (fixes incompatibility with OpenJDK 8.0.252) * gitignore * Bump SnakeYAML from 1.19 to 1.26 to address CVE-2017-18640 * prepare 4.14.3 release (#209) * Releasing version 4.14.3 * comments * only log initialization message once in polling mode * [ch89935] Correct some logging call format strings (#264) Also adds debug logs for full exception information in a couple locations. * [ch90109] Remove outdated trackMetric comment from before service support. (#265) * Fix compatibility with Java 7. * Remove import that is no longer used. * add Java 7 build (#267) * prepare 4.14.4 release (#214) * Releasing version 4.14.4 * add and use getSocketFactory * alignment * add socketFactory to builder * test socket factory builder * preserve dummy CI config file when pushing to gh-pages (#271) * fix concatenation when base URI has a context path (#270) * fix shaded jar builds to exclude Jackson classes and not modify Jackson return types (#268) * add test httpClientCanUseCustomSocketFactory for DefaultFeatureRequestor * add httpClientCanUseCustomSocketFactory() test for DefaultEventSenderTest * add httpClientCanUseCustomSocketFactory() test to StreamProcessorTest * pass URI to in customSocketFactory event test * make test less ambiguous * copy rules to new FlagBuilder instances (#273) * Bump guava version (#274) * Removed the guides link * increment versions when loading file data, so FlagTracker will work (#275) * increment versions when loading file data, so FlagTracker will work * update doc comment about flag change events with file data * add ability to ignore duplicate keys in file data (#276) * add alias events (#278) * add alias events and function * update tests for new functionality * update javadoc strings * add validation of javadoc build to CI * update commons-codec to 1.15 (#279) * Add support for experiment rollouts * add tests and use seed for allocating user to partition * test serialization and add check for isExperiment * fix PollingProcessorTest test race condition + other test issues (#282) * use launchdarkly-java-sdk-common 1.1.0-alpha-expalloc.2 * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes <[email protected]> * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes <[email protected]> * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes <[email protected]> * Update src/test/java/com/launchdarkly/sdk/server/EvaluatorTest.java Co-authored-by: Sam Stokes <[email protected]> * changes per code review comments * Please enter the commit message for your changes. Lines starting * fix null pointer exception * address code review comments * address more comments * missed a ! for isUntracked() * fix default boolean for json * make untracked FALSE by default * refactoring of bucketing logic to remove the need for an extra result object (#283) * add comment to enum * various JSON fixes, update common-sdk (#284) * simlpify the logic and make it match node/.Net sdks * Update src/main/java/com/launchdarkly/sdk/server/EventFactory.java Co-authored-by: Sam Stokes <[email protected]> * add the same comment as the Node SDK * Remove outdated/meaningless doc comment. (#286) * protect against NPEs if flag/segment JSON contains a null value * use java-sdk-common 1.2.0 * fix Jackson-related build issues (again) (#288) * update to okhttp-eventsource patch for stream retry bug, improve tests (#289) * update to okhttp-eventsource patch for stream retry bug, improve test * add test for appropriate stream retry * add public builder for FeatureFlagsState (#290) * add public builder for FeatureFlagsState * javadoc fixes * clarify FileData doc comment to say you shouldn't use offline mode (#291) * improve validation of SDK key so we won't throw an exception that contains the key (#293) * fix javadoc link in FileData comment (#294) * fix PollingProcessor 401 behavior and use new HTTP test helpers (#292) * re-fix metadata to remove Jackson dependencies, also remove Class-Path from manifest (#295) * make FeatureFlagsState.Builder.build() public (#297) * clean up tests using java-test-helpers 1.1.0 (#296) * use Releaser v2 config + newer CI images (#298) * [ch123129] Fix `PollingDataSourceBuilder` example. (#299) * Updates docs URLs * always use US locale when parsing HTTP dates * use Gson 2.8.9 * don't try to send more diagnostic events after an unrecoverable HTTP error * ensure module-info file isn't copied into our jars during build * use Gradle 7 * update build for benchmarks * more Gradle 7 compatibility changes for benchmark job * test with Java 17 in CI (#307) * test with Java 17 in CI * also test in Java 17 for Windows * fix choco install command * do date comparisons as absolute times, regardless of time zone (#310) * fix suppression of nulls in JSON representations (#311) * fix suppression of nulls in JSON representations * distinguish between situations where we do or do not want to suppress nulls * fix identify/track null user key check, also don't create index event for alias * use latest java-sdk-common * fix setting of trackEvents/trackReason in allFlagsState data when there's an experiment * implement contract tests (#314) * Merge Big Segments feature branch for 5.7.0 release (#316) Includes Big Segments implementation and contract test support for the new behavior. * Fix for pom including SDK common library as a dependency. (#317) * Upload JUnit XML to CircleCI on failure (#320) Fix a bug in the CircleCI config that was only uploading JUnit XML on _success_, not failure. * Add application tag support (#319) * Enforce 64 character limit on application tag values (#323) * fix "wrong type" logic in evaluations when default value is null * Rename master to main in .ldrelease/config.yml (#325) * Simpler way of setting base URIs in Java (#322) Now supports the `ServiceEndpoints` config for setting custom URIs for endpoints in a single place * make BigSegmentStoreWrapper.pollingDetectsStaleStatus test less timing-sensitive * make LDEndToEndClientTest.test____SpecialHttpConfigurations less timing-sensitive * make data source status tests less timing-sensitive * use streaming JSON parsing for incoming LD data * fix tests * rm unused * rm unused * use okhttp-eventsource 2.6.0 * update eventsource to 2.6.1 to fix pom/manifest problem * increase efficiency of summary event data structures (#335) Co-authored-by: Eli Bishop <[email protected]> Co-authored-by: LaunchDarklyCI <[email protected]> Co-authored-by: LaunchDarklyCI <[email protected]> Co-authored-by: Gavin Whelan <[email protected]> Co-authored-by: ssrm <[email protected]> Co-authored-by: Harpo Roeder <[email protected]> Co-authored-by: Ben Woskow <[email protected]> Co-authored-by: Elliot <[email protected]> Co-authored-by: Robert J. Neal <[email protected]> Co-authored-by: Robert J. Neal <[email protected]> Co-authored-by: Sam Stokes <[email protected]> Co-authored-by: LaunchDarklyReleaseBot <[email protected]> Co-authored-by: Ember Stevens <[email protected]> Co-authored-by: ember-stevens <[email protected]> Co-authored-by: Alex Engelberg <[email protected]> Co-authored-by: Alex Engelberg <[email protected]>
1 parent 04422de commit 1392dea

19 files changed

+1057
-409
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ ext.versions = [
7474
"jackson": "2.11.2",
7575
"launchdarklyJavaSdkCommon": "1.3.0",
7676
"okhttp": "4.8.1", // specify this for the SDK build instead of relying on the transitive dependency from okhttp-eventsource
77-
"okhttpEventsource": "2.3.2",
77+
"okhttpEventsource": "2.6.1",
7878
"slf4j": "1.7.21",
7979
"snakeyaml": "1.26",
8080
"jedis": "2.9.0"
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.launchdarkly.sdk.server;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import com.google.common.collect.ImmutableMap;
5+
import com.google.gson.JsonElement;
6+
import com.google.gson.stream.JsonReader;
7+
import com.google.gson.stream.JsonToken;
8+
import com.launchdarkly.sdk.server.DataModel.FeatureFlag;
9+
import com.launchdarkly.sdk.server.DataModel.Segment;
10+
import com.launchdarkly.sdk.server.DataModel.VersionedData;
11+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.DataKind;
12+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet;
13+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor;
14+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.KeyedItems;
15+
import com.launchdarkly.sdk.server.interfaces.SerializationException;
16+
17+
import java.io.IOException;
18+
import java.util.AbstractMap;
19+
import java.util.Map;
20+
21+
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
22+
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
23+
import static com.launchdarkly.sdk.server.JsonHelpers.gsonInstance;
24+
25+
/**
26+
* JSON conversion logic specifically for our data model types.
27+
* <p>
28+
* More general JSON helpers are in JsonHelpers.
29+
*/
30+
abstract class DataModelSerialization {
31+
/**
32+
* Deserializes a data model object from JSON that was already parsed by Gson.
33+
* <p>
34+
* For built-in data model classes, our usual abstraction for deserializing from a string is inefficient in
35+
* this case, because Gson has already parsed the original JSON and then we would have to convert the
36+
* JsonElement back into a string and parse it again. So it's best to call Gson directly instead of going
37+
* through our abstraction in that case, but it's also best to implement that special-casing just once here
38+
* instead of scattered throughout the SDK.
39+
*
40+
* @param kind the data kind
41+
* @param parsedJson the parsed JSON
42+
* @return the deserialized item
43+
*/
44+
static VersionedData deserializeFromParsedJson(DataKind kind, JsonElement parsedJson) throws SerializationException {
45+
VersionedData item;
46+
try {
47+
if (kind == FEATURES) {
48+
item = gsonInstance().fromJson(parsedJson, FeatureFlag.class);
49+
} else if (kind == SEGMENTS) {
50+
item = gsonInstance().fromJson(parsedJson, Segment.class);
51+
} else {
52+
// This shouldn't happen since we only use this method internally with our predefined data kinds
53+
throw new IllegalArgumentException("unknown data kind");
54+
}
55+
} catch (RuntimeException e) {
56+
// A variety of unchecked exceptions can be thrown from JSON parsing; treat them all the same
57+
throw new SerializationException(e);
58+
}
59+
return item;
60+
}
61+
62+
/**
63+
* Deserializes a data model object from a Gson reader.
64+
*
65+
* @param kind the data kind
66+
* @param jr the JSON reader
67+
* @return the deserialized item
68+
*/
69+
static VersionedData deserializeFromJsonReader(DataKind kind, JsonReader jr) throws SerializationException {
70+
VersionedData item;
71+
try {
72+
if (kind == FEATURES) {
73+
item = gsonInstance().fromJson(jr, FeatureFlag.class);
74+
} else if (kind == SEGMENTS) {
75+
item = gsonInstance().fromJson(jr, Segment.class);
76+
} else {
77+
// This shouldn't happen since we only use this method internally with our predefined data kinds
78+
throw new IllegalArgumentException("unknown data kind");
79+
}
80+
} catch (RuntimeException e) {
81+
// A variety of unchecked exceptions can be thrown from JSON parsing; treat them all the same
82+
throw new SerializationException(e);
83+
}
84+
return item;
85+
}
86+
87+
/**
88+
* Deserializes a full set of flag/segment data from a standard JSON object representation
89+
* in the form {"flags": ..., "segments": ...} (which is used in both streaming and polling
90+
* responses).
91+
*
92+
* @param jr the JSON reader
93+
* @return the deserialized data
94+
*/
95+
static FullDataSet<ItemDescriptor> parseFullDataSet(JsonReader jr) throws SerializationException {
96+
ImmutableList.Builder<Map.Entry<String, ItemDescriptor>> flags = ImmutableList.builder();
97+
ImmutableList.Builder<Map.Entry<String, ItemDescriptor>> segments = ImmutableList.builder();
98+
99+
try {
100+
jr.beginObject();
101+
while (jr.peek() != JsonToken.END_OBJECT) {
102+
String kindName = jr.nextName();
103+
Class<?> itemClass;
104+
ImmutableList.Builder<Map.Entry<String, ItemDescriptor>> listBuilder;
105+
switch (kindName) {
106+
case "flags":
107+
itemClass = DataModel.FeatureFlag.class;
108+
listBuilder = flags;
109+
break;
110+
case "segments":
111+
itemClass = DataModel.Segment.class;
112+
listBuilder = segments;
113+
break;
114+
default:
115+
jr.skipValue();
116+
continue;
117+
}
118+
jr.beginObject();
119+
while (jr.peek() != JsonToken.END_OBJECT) {
120+
String key = jr.nextName();
121+
@SuppressWarnings("unchecked")
122+
Object item = JsonHelpers.deserialize(jr, (Class<Object>)itemClass);
123+
listBuilder.add(new AbstractMap.SimpleEntry<>(key,
124+
new ItemDescriptor(((VersionedData)item).getVersion(), item)));
125+
}
126+
jr.endObject();
127+
}
128+
jr.endObject();
129+
130+
return new FullDataSet<ItemDescriptor>(ImmutableMap.of(
131+
FEATURES, new KeyedItems<>(flags.build()),
132+
SEGMENTS, new KeyedItems<>(segments.build())
133+
).entrySet());
134+
} catch (IOException e) {
135+
throw new SerializationException(e);
136+
} catch (RuntimeException e) {
137+
// A variety of unchecked exceptions can be thrown from JSON parsing; treat them all the same
138+
throw new SerializationException(e);
139+
}
140+
}
141+
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ private void triggerFlush(EventBuffer outbox, BlockingQueue<FlushPayload> payloa
460460
} else {
461461
logger.debug("Skipped flushing because all workers are busy");
462462
// All the workers are busy so we can't flush now; keep the events in our state
463+
outbox.summarizer.restoreTo(payload.summary);
463464
synchronized(busyFlushWorkersCount) {
464465
busyFlushWorkersCount.decrementAndGet();
465466
busyFlushWorkersCount.notify();
@@ -506,7 +507,7 @@ void addToSummary(Event e) {
506507
}
507508

508509
boolean isEmpty() {
509-
return events.isEmpty() && summarizer.snapshot().isEmpty();
510+
return events.isEmpty() && summarizer.isEmpty();
510511
}
511512

512513
long getAndClearDroppedCount() {
@@ -517,7 +518,7 @@ long getAndClearDroppedCount() {
517518

518519
FlushPayload getPayload() {
519520
Event[] eventsOut = events.toArray(new Event[events.size()]);
520-
EventSummarizer.EventSummary summary = summarizer.snapshot();
521+
EventSummarizer.EventSummary summary = summarizer.getSummaryAndReset();
521522
return new FlushPayload(eventsOut, summary);
522523
}
523524

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.launchdarkly.sdk.server;
22

33
import com.google.common.annotations.VisibleForTesting;
4+
import com.google.gson.stream.JsonReader;
5+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.FullDataSet;
6+
import com.launchdarkly.sdk.server.interfaces.DataStoreTypes.ItemDescriptor;
47
import com.launchdarkly.sdk.server.interfaces.HttpConfiguration;
58
import com.launchdarkly.sdk.server.interfaces.SerializationException;
69

@@ -11,6 +14,7 @@
1114
import java.nio.file.Files;
1215
import java.nio.file.Path;
1316

17+
import static com.launchdarkly.sdk.server.DataModelSerialization.parseFullDataSet;
1418
import static com.launchdarkly.sdk.server.Util.concatenateUriPath;
1519
import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder;
1620
import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor;
@@ -23,7 +27,7 @@
2327
import okhttp3.Response;
2428

2529
/**
26-
* Implementation of getting flag data via a polling request. Used by both streaming and polling components.
30+
* Implementation of getting flag data via a polling request.
2731
*/
2832
final class DefaultFeatureRequestor implements FeatureRequestor {
2933
private static final Logger logger = Loggers.DATA_SOURCE;
@@ -59,7 +63,8 @@ public void close() {
5963
Util.deleteDirectory(cacheDir);
6064
}
6165

62-
public AllData getAllData(boolean returnDataEvenIfCached) throws IOException, HttpErrorException, SerializationException {
66+
public FullDataSet<ItemDescriptor> getAllData(boolean returnDataEvenIfCached)
67+
throws IOException, HttpErrorException, SerializationException {
6368
Request request = new Request.Builder()
6469
.url(pollingUri.toURL())
6570
.headers(headers)
@@ -75,18 +80,18 @@ public AllData getAllData(boolean returnDataEvenIfCached) throws IOException, Ht
7580
logger.debug("Cache hit count: " + httpClient.cache().hitCount() + " Cache network Count: " + httpClient.cache().networkCount());
7681
return null;
7782
}
78-
79-
String body = response.body().string();
8083

81-
if (!response.isSuccessful()) {
82-
throw new HttpErrorException(response.code());
83-
}
84-
logger.debug("Get flag(s) response: " + response.toString() + " with body: " + body);
84+
logger.debug("Get flag(s) response: " + response.toString());
8585
logger.debug("Network response: " + response.networkResponse());
8686
logger.debug("Cache hit count: " + httpClient.cache().hitCount() + " Cache network Count: " + httpClient.cache().networkCount());
8787
logger.debug("Cache response: " + response.cacheResponse());
88-
89-
return JsonHelpers.deserialize(body, AllData.class);
88+
89+
if (!response.isSuccessful()) {
90+
throw new HttpErrorException(response.code());
91+
}
92+
93+
JsonReader jr = new JsonReader(response.body().charStream());
94+
return parseFullDataSet(jr);
9095
}
9196
}
9297
}

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

Lines changed: 31 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import com.launchdarkly.sdk.EvaluationReason;
66
import com.launchdarkly.sdk.LDUser;
77
import com.launchdarkly.sdk.LDValue;
8-
import com.launchdarkly.sdk.server.EventSummarizer.CounterKey;
98
import com.launchdarkly.sdk.server.EventSummarizer.CounterValue;
9+
import com.launchdarkly.sdk.server.EventSummarizer.FlagInfo;
10+
import com.launchdarkly.sdk.server.EventSummarizer.SimpleIntKeyedMap;
1011
import com.launchdarkly.sdk.server.interfaces.Event;
1112

1213
import java.io.IOException;
1314
import java.io.Writer;
15+
import java.util.Map;
1416

1517
/**
1618
* Transforms analytics events and summary data into the JSON format that we send to LaunchDarkly.
@@ -28,6 +30,7 @@ final class EventOutputFormatter {
2830
this.gson = JsonHelpers.gsonInstanceForEventsSerialization(config);
2931
}
3032

33+
@SuppressWarnings("resource")
3134
final int writeOutputEvents(Event[] events, EventSummarizer.EventSummary summary, Writer writer) throws IOException {
3235
int count = events.length;
3336
try (JsonWriter jsonWriter = new JsonWriter(writer)) {
@@ -112,59 +115,48 @@ private final void writeSummaryEvent(EventSummarizer.EventSummary summary, JsonW
112115
jw.name("features");
113116
jw.beginObject();
114117

115-
CounterKey[] unprocessedKeys = summary.counters.keySet().toArray(new CounterKey[summary.counters.size()]);
116-
for (int i = 0; i < unprocessedKeys.length; i++) {
117-
if (unprocessedKeys[i] == null) {
118-
continue;
119-
}
120-
CounterKey key = unprocessedKeys[i];
121-
String flagKey = key.key;
122-
CounterValue firstValue = summary.counters.get(key);
118+
for (Map.Entry<String, FlagInfo> flag: summary.counters.entrySet()) {
119+
String flagKey = flag.getKey();
120+
FlagInfo flagInfo = flag.getValue();
123121

124122
jw.name(flagKey);
125123
jw.beginObject();
126124

127-
writeLDValue("default", firstValue.defaultVal, jw);
125+
writeLDValue("default", flagInfo.defaultVal, jw);
128126

129127
jw.name("counters");
130128
jw.beginArray();
131129

132-
for (int j = i; j < unprocessedKeys.length; j++) {
133-
CounterKey keyForThisFlag = unprocessedKeys[j];
134-
if (j != i && (keyForThisFlag == null || !keyForThisFlag.key.equals(flagKey))) {
135-
continue;
136-
}
137-
CounterValue value = keyForThisFlag == key ? firstValue : summary.counters.get(keyForThisFlag);
138-
unprocessedKeys[j] = null;
139-
140-
jw.beginObject();
141-
142-
if (keyForThisFlag.variation >= 0) {
143-
jw.name("variation");
144-
jw.value(keyForThisFlag.variation);
130+
for (int i = 0; i < flagInfo.versionsAndVariations.size(); i++) {
131+
int version = flagInfo.versionsAndVariations.keyAt(i);
132+
SimpleIntKeyedMap<CounterValue> variations = flagInfo.versionsAndVariations.valueAt(i);
133+
for (int j = 0; j < variations.size(); j++) {
134+
int variation = variations.keyAt(j);
135+
CounterValue counter = variations.valueAt(j);
136+
137+
jw.beginObject();
138+
139+
if (variation >= 0) {
140+
jw.name("variation").value(variation);
141+
}
142+
if (version >= 0) {
143+
jw.name("version").value(version);
144+
} else {
145+
jw.name("unknown").value(true);
146+
}
147+
writeLDValue("value", counter.flagValue, jw);
148+
jw.name("count").value(counter.count);
149+
150+
jw.endObject();
145151
}
146-
if (keyForThisFlag.version >= 0) {
147-
jw.name("version");
148-
jw.value(keyForThisFlag.version);
149-
} else {
150-
jw.name("unknown");
151-
jw.value(true);
152-
}
153-
writeLDValue("value", value.flagValue, jw);
154-
jw.name("count");
155-
jw.value(value.count);
156-
157-
jw.endObject(); // end of this counter
158152
}
159-
153+
160154
jw.endArray(); // end of "counters" array
161-
162155
jw.endObject(); // end of this flag
163156
}
164157

165158
jw.endObject(); // end of "features"
166-
167-
jw.endObject();
159+
jw.endObject(); // end of summary event object
168160
}
169161

170162
private final void startEvent(Event event, String kind, String key, JsonWriter jw) throws IOException {

0 commit comments

Comments
 (0)