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

Commit 37af42c

Browse files
prepare 5.0.2 release (#198)
1 parent e098c4b commit 37af42c

File tree

14 files changed

+294
-465
lines changed

14 files changed

+294
-465
lines changed

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

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -139,24 +139,9 @@ public DataSource createDataSource(ClientContext context, DataSourceUpdates data
139139
Loggers.DATA_SOURCE.info("Enabling streaming API");
140140

141141
URI streamUri = baseURI == null ? LDConfig.DEFAULT_STREAM_URI : baseURI;
142-
URI pollUri;
143-
if (pollingBaseURI != null) {
144-
pollUri = pollingBaseURI;
145-
} else {
146-
// If they have set a custom base URI, and they did *not* set a custom polling URI, then we can
147-
// assume they're using Relay in which case both of those values are the same.
148-
pollUri = baseURI == null ? LDConfig.DEFAULT_BASE_URI : baseURI;
149-
}
150-
151-
DefaultFeatureRequestor requestor = new DefaultFeatureRequestor(
152-
context.getHttp(),
153-
pollUri,
154-
false
155-
);
156142

157143
return new StreamProcessor(
158144
context.getHttp(),
159-
requestor,
160145
dataSourceUpdates,
161146
null,
162147
context.getBasic().getThreadPriority(),
@@ -170,9 +155,7 @@ public DataSource createDataSource(ClientContext context, DataSourceUpdates data
170155
public LDValue describeConfiguration(BasicConfiguration basicConfiguration) {
171156
return LDValue.buildObject()
172157
.put(ConfigProperty.STREAMING_DISABLED.name, false)
173-
.put(ConfigProperty.CUSTOM_BASE_URI.name,
174-
(pollingBaseURI != null && !pollingBaseURI.equals(LDConfig.DEFAULT_BASE_URI)) ||
175-
(pollingBaseURI == null && baseURI != null && !baseURI.equals(LDConfig.DEFAULT_STREAM_URI)))
158+
.put(ConfigProperty.CUSTOM_BASE_URI.name, false)
176159
.put(ConfigProperty.CUSTOM_STREAM_URI.name,
177160
baseURI != null && !baseURI.equals(LDConfig.DEFAULT_STREAM_URI))
178161
.put(ConfigProperty.RECONNECT_TIME_MILLIS.name, initialReconnectDelay.toMillis())
@@ -191,8 +174,7 @@ public DataSource createDataSource(ClientContext context, DataSourceUpdates data
191174

192175
DefaultFeatureRequestor requestor = new DefaultFeatureRequestor(
193176
context.getHttp(),
194-
baseURI == null ? LDConfig.DEFAULT_BASE_URI : baseURI,
195-
true
177+
baseURI == null ? LDConfig.DEFAULT_BASE_URI : baseURI
196178
);
197179
return new PollingProcessor(
198180
requestor,
Lines changed: 27 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package com.launchdarkly.sdk.server;
22

33
import com.google.common.annotations.VisibleForTesting;
4-
import com.google.common.io.Files;
54
import com.launchdarkly.sdk.server.interfaces.HttpConfiguration;
65
import com.launchdarkly.sdk.server.interfaces.SerializationException;
76

87
import org.slf4j.Logger;
98

10-
import java.io.File;
119
import java.io.IOException;
1210
import java.net.URI;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
1313

1414
import static com.launchdarkly.sdk.server.Util.configureHttpClientBuilder;
1515
import static com.launchdarkly.sdk.server.Util.getHeadersBuilderFor;
@@ -26,77 +26,67 @@
2626
*/
2727
final class DefaultFeatureRequestor implements FeatureRequestor {
2828
private static final Logger logger = Loggers.DATA_SOURCE;
29-
private static final String GET_LATEST_FLAGS_PATH = "/sdk/latest-flags";
30-
private static final String GET_LATEST_SEGMENTS_PATH = "/sdk/latest-segments";
3129
private static final String GET_LATEST_ALL_PATH = "/sdk/latest-all";
3230
private static final long MAX_HTTP_CACHE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
3331

3432
@VisibleForTesting final URI baseUri;
3533
private final OkHttpClient httpClient;
34+
private final URI pollingUri;
3635
private final Headers headers;
37-
private final boolean useCache;
36+
private final Path cacheDir;
3837

39-
DefaultFeatureRequestor(HttpConfiguration httpConfig, URI baseUri, boolean useCache) {
38+
DefaultFeatureRequestor(HttpConfiguration httpConfig, URI baseUri) {
4039
this.baseUri = baseUri;
41-
this.useCache = useCache;
40+
this.pollingUri = baseUri.resolve(GET_LATEST_ALL_PATH);
4241

4342
OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
4443
configureHttpClientBuilder(httpConfig, httpBuilder);
4544
this.headers = getHeadersBuilderFor(httpConfig).build();
4645

47-
// HTTP caching is used only for FeatureRequestor. However, when streaming is enabled, HTTP GETs
48-
// made by FeatureRequester will always guarantee a new flag state, so we disable the cache.
49-
if (useCache) {
50-
File cacheDir = Files.createTempDir();
51-
Cache cache = new Cache(cacheDir, MAX_HTTP_CACHE_SIZE_BYTES);
52-
httpBuilder.cache(cache);
46+
try {
47+
cacheDir = Files.createTempDirectory("LaunchDarklySDK");
48+
} catch (IOException e) {
49+
throw new RuntimeException("unable to create cache directory for polling", e);
5350
}
51+
Cache cache = new Cache(cacheDir.toFile(), MAX_HTTP_CACHE_SIZE_BYTES);
52+
httpBuilder.cache(cache);
5453

5554
httpClient = httpBuilder.build();
5655
}
5756

5857
public void close() {
5958
shutdownHttpClient(httpClient);
59+
Util.deleteDirectory(cacheDir);
6060
}
6161

62-
public DataModel.FeatureFlag getFlag(String featureKey) throws IOException, HttpErrorException, SerializationException {
63-
String body = get(GET_LATEST_FLAGS_PATH + "/" + featureKey);
64-
return JsonHelpers.deserialize(body, DataModel.FeatureFlag.class);
65-
}
66-
67-
public DataModel.Segment getSegment(String segmentKey) throws IOException, HttpErrorException, SerializationException {
68-
String body = get(GET_LATEST_SEGMENTS_PATH + "/" + segmentKey);
69-
return JsonHelpers.deserialize(body, DataModel.Segment.class);
70-
}
71-
72-
public AllData getAllData() throws IOException, HttpErrorException, SerializationException {
73-
String body = get(GET_LATEST_ALL_PATH);
74-
return JsonHelpers.deserialize(body, AllData.class);
75-
}
76-
77-
private String get(String path) throws IOException, HttpErrorException {
62+
public AllData getAllData(boolean returnDataEvenIfCached) throws IOException, HttpErrorException, SerializationException {
7863
Request request = new Request.Builder()
79-
.url(baseUri.resolve(path).toURL())
64+
.url(pollingUri.toURL())
8065
.headers(headers)
8166
.get()
8267
.build();
8368

8469
logger.debug("Making request: " + request);
8570

8671
try (Response response = httpClient.newCall(request).execute()) {
72+
boolean wasCached = response.networkResponse() == null || response.networkResponse().code() == 304;
73+
if (wasCached && !returnDataEvenIfCached) {
74+
logger.debug("Get flag(s) got cached response, will not parse");
75+
logger.debug("Cache hit count: " + httpClient.cache().hitCount() + " Cache network Count: " + httpClient.cache().networkCount());
76+
return null;
77+
}
78+
8779
String body = response.body().string();
8880

8981
if (!response.isSuccessful()) {
9082
throw new HttpErrorException(response.code());
9183
}
9284
logger.debug("Get flag(s) response: " + response.toString() + " with body: " + body);
9385
logger.debug("Network response: " + response.networkResponse());
94-
if (useCache) {
95-
logger.debug("Cache hit count: " + httpClient.cache().hitCount() + " Cache network Count: " + httpClient.cache().networkCount());
96-
logger.debug("Cache response: " + response.cacheResponse());
97-
}
98-
99-
return body;
86+
logger.debug("Cache hit count: " + httpClient.cache().hitCount() + " Cache network Count: " + httpClient.cache().networkCount());
87+
logger.debug("Cache response: " + response.cacheResponse());
88+
89+
return JsonHelpers.deserialize(body, AllData.class);
10090
}
10191
}
102-
}
92+
}

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,22 @@
1616
import static com.launchdarkly.sdk.server.DataModel.FEATURES;
1717
import static com.launchdarkly.sdk.server.DataModel.SEGMENTS;
1818

19+
/**
20+
* Internal abstraction for polling requests. Currently this is only used by PollingProcessor, and
21+
* the only implementation is DefaultFeatureRequestor, but using an interface allows us to mock out
22+
* the HTTP behavior and test the rest of PollingProcessor separately.
23+
*/
1924
interface FeatureRequestor extends Closeable {
20-
DataModel.FeatureFlag getFlag(String featureKey) throws IOException, HttpErrorException;
21-
22-
DataModel.Segment getSegment(String segmentKey) throws IOException, HttpErrorException;
23-
24-
AllData getAllData() throws IOException, HttpErrorException;
25+
/**
26+
* Makes a request to the LaunchDarkly server-side SDK polling endpoint,
27+
*
28+
* @param returnDataEvenIfCached true if the method should return non-nil data no matter what;
29+
* false if it should return {@code null} when the latest data is already in the cache
30+
* @return the data, or {@code null} as above
31+
* @throws IOException for network errors
32+
* @throws HttpErrorException for HTTP error responses
33+
*/
34+
AllData getAllData(boolean returnDataEvenIfCached) throws IOException, HttpErrorException;
2535

2636
static class AllData {
2737
final Map<String, DataModel.FeatureFlag> flags;

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

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,23 @@ public Future<Void> start() {
8484
}
8585

8686
private void poll() {
87-
FeatureRequestor.AllData allData = null;
88-
8987
try {
90-
allData = requestor.getAllData();
88+
// If we already obtained data earlier, and the poll request returns a cached response, then we don't
89+
// want to bother parsing the data or reinitializing the data store. But if we never succeeded in
90+
// storing any data, then we would still want to parse and try to store it even if it's cached.
91+
boolean alreadyInited = initialized.get();
92+
FeatureRequestor.AllData allData = requestor.getAllData(!alreadyInited);
93+
if (allData == null) {
94+
// This means it was cached, and alreadyInited was true
95+
dataSourceUpdates.updateStatus(State.VALID, null);
96+
} else {
97+
if (dataSourceUpdates.init(allData.toFullDataSet())) {
98+
dataSourceUpdates.updateStatus(State.VALID, null);
99+
logger.info("Initialized LaunchDarkly client.");
100+
initialized.getAndSet(true);
101+
initFuture.complete(null);
102+
}
103+
}
91104
} catch (HttpErrorException e) {
92105
ErrorInfo errorInfo = ErrorInfo.fromHttpError(e.getStatus());
93106
boolean recoverable = checkIfErrorIsRecoverableAndLog(logger, httpErrorDescription(e.getStatus()),
@@ -109,13 +122,5 @@ private void poll() {
109122
logger.debug(e.toString(), e);
110123
dataSourceUpdates.updateStatus(State.INTERRUPTED, ErrorInfo.fromException(ErrorKind.UNKNOWN, e));
111124
}
112-
113-
if (allData != null && dataSourceUpdates.init(allData.toFullDataSet())) {
114-
if (!initialized.getAndSet(true)) {
115-
logger.info("Initialized LaunchDarkly client.");
116-
dataSourceUpdates.updateStatus(State.VALID, null);
117-
initFuture.complete(null);
118-
}
119-
}
120125
}
121126
}

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

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ final class StreamProcessor implements DataSource {
7171
private static final String PUT = "put";
7272
private static final String PATCH = "patch";
7373
private static final String DELETE = "delete";
74-
private static final String INDIRECT_PUT = "indirect/put";
75-
private static final String INDIRECT_PATCH = "indirect/patch";
7674
private static final Logger logger = Loggers.DATA_SOURCE;
7775
private static final Duration DEAD_CONNECTION_INTERVAL = Duration.ofSeconds(300);
7876
private static final String ERROR_CONTEXT_MESSAGE = "in stream connection";
@@ -83,7 +81,6 @@ final class StreamProcessor implements DataSource {
8381
private final Headers headers;
8482
@VisibleForTesting final URI streamUri;
8583
@VisibleForTesting final Duration initialReconnectDelay;
86-
@VisibleForTesting final FeatureRequestor requestor;
8784
private final DiagnosticAccumulator diagnosticAccumulator;
8885
private final EventSourceCreator eventSourceCreator;
8986
private final int threadPriority;
@@ -121,7 +118,6 @@ static interface EventSourceCreator {
121118

122119
StreamProcessor(
123120
HttpConfiguration httpConfig,
124-
FeatureRequestor requestor,
125121
DataSourceUpdates dataSourceUpdates,
126122
EventSourceCreator eventSourceCreator,
127123
int threadPriority,
@@ -131,7 +127,6 @@ static interface EventSourceCreator {
131127
) {
132128
this.dataSourceUpdates = dataSourceUpdates;
133129
this.httpConfig = httpConfig;
134-
this.requestor = requestor;
135130
this.diagnosticAccumulator = diagnosticAccumulator;
136131
this.eventSourceCreator = eventSourceCreator != null ? eventSourceCreator : this::defaultEventSourceCreator;
137132
this.threadPriority = threadPriority;
@@ -232,7 +227,6 @@ public void close() throws IOException {
232227
if (es != null) {
233228
es.close();
234229
}
235-
requestor.close();
236230
dataSourceUpdates.updateStatus(State.OFF, null);
237231
}
238232

@@ -271,14 +265,6 @@ public void onMessage(String name, MessageEvent event) throws Exception {
271265
case DELETE:
272266
handleDelete(event.getData());
273267
break;
274-
275-
case INDIRECT_PUT:
276-
handleIndirectPut();
277-
break;
278-
279-
case INDIRECT_PATCH:
280-
handleIndirectPatch(event.getData());
281-
break;
282268

283269
default:
284270
logger.warn("Unexpected event found in stream: " + name);
@@ -356,41 +342,6 @@ private void handleDelete(String eventData) throws StreamInputException, StreamS
356342
}
357343
}
358344

359-
private void handleIndirectPut() throws StreamInputException, StreamStoreException {
360-
FeatureRequestor.AllData putData;
361-
try {
362-
putData = requestor.getAllData();
363-
} catch (Exception e) {
364-
throw new StreamInputException(e);
365-
}
366-
FullDataSet<ItemDescriptor> allData = putData.toFullDataSet();
367-
if (!dataSourceUpdates.init(allData)) {
368-
throw new StreamStoreException();
369-
}
370-
if (!initialized.getAndSet(true)) {
371-
initFuture.complete(null);
372-
logger.info("Initialized LaunchDarkly client.");
373-
}
374-
}
375-
376-
private void handleIndirectPatch(String path) throws StreamInputException, StreamStoreException {
377-
Map.Entry<DataKind, String> kindAndKey = getKindAndKeyFromStreamApiPath(path);
378-
DataKind kind = kindAndKey.getKey();
379-
String key = kindAndKey.getValue();
380-
VersionedData item;
381-
try {
382-
item = kind == SEGMENTS ? requestor.getSegment(key) : requestor.getFlag(key);
383-
} catch (Exception e) {
384-
throw new StreamInputException(e);
385-
// In this case, StreamInputException doesn't necessarily represent malformed data from the service - it
386-
// could be that the request to the polling endpoint failed in some other way. But either way, we must
387-
// assume that we did not get valid data from LD so we have missed an update.
388-
}
389-
if (!dataSourceUpdates.upsert(kind, key, new ItemDescriptor(item.getVersion(), item))) {
390-
throw new StreamStoreException();
391-
}
392-
}
393-
394345
@Override
395346
public void onComment(String comment) {
396347
logger.debug("Received a heartbeat");

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
import org.slf4j.Logger;
77

88
import java.io.IOException;
9+
import java.nio.file.FileVisitResult;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.SimpleFileVisitor;
13+
import java.nio.file.attribute.BasicFileAttributes;
914
import java.time.Duration;
1015
import java.util.Map;
1116
import java.util.concurrent.TimeUnit;
@@ -150,4 +155,26 @@ static String describeDuration(Duration d) {
150155
}
151156
return d.toMillis() + " milliseconds";
152157
}
158+
159+
static void deleteDirectory(Path path) {
160+
try {
161+
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
162+
@Override
163+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
164+
try {
165+
Files.delete(file);
166+
} catch (IOException e) {}
167+
return FileVisitResult.CONTINUE;
168+
}
169+
170+
@Override
171+
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
172+
try {
173+
Files.delete(dir);
174+
} catch (IOException e) {}
175+
return FileVisitResult.CONTINUE;
176+
}
177+
});
178+
} catch (IOException e) {}
179+
}
153180
}

0 commit comments

Comments
 (0)