Skip to content

Commit d8f6943

Browse files
committed
adding http cache
Signed-off-by: liran2000 <[email protected]>
1 parent f93014e commit d8f6943

File tree

6 files changed

+150
-10
lines changed

6 files changed

+150
-10
lines changed

providers/flagd/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ This can be used to enrich evaluations with such data.
5555
If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map.
5656

5757
#### Http Connector
58-
HttpConnector is responsible for polling data from a specified URL at regular intervals.
58+
HttpConnector is responsible for polling data from a specified URL at regular intervals.
59+
It is implementing Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response, reducing traffic and
60+
changes updates. Can be enabled via useHttpCache option.
61+
One of its benefits is to reduce infrastructure/devops work, without additional containers needed.
5962
The implementation is using Java HttpClient.
6063

6164
##### What happens if the Http source is down when application is starting ?
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync;
2+
3+
import java.net.http.HttpClient;
4+
import java.net.http.HttpRequest;
5+
import java.net.http.HttpResponse;
6+
import lombok.SneakyThrows;
7+
import lombok.extern.slf4j.Slf4j;
8+
9+
/**
10+
* Fetches content from a given HTTP endpoint using caching headers to optimize network usage.
11+
* If cached ETag or Last-Modified values are available, they are included in the request headers
12+
* to potentially receive a 304 Not Modified response, reducing data transfer.
13+
* Updates the cached ETag and Last-Modified values upon receiving a 200 OK response.
14+
* It does not store the cached response, assuming not needed after first successful fetching.
15+
*
16+
* @param httpClient the HTTP client used to send the request
17+
* @param httpRequestBuilder the builder for constructing the HTTP request
18+
* @return the HTTP response received from the server
19+
*/
20+
@Slf4j
21+
public class HttpCacheFetcher {
22+
private static String cachedETag = null;
23+
private static String cachedLastModified = null;
24+
25+
@SneakyThrows
26+
public HttpResponse<String> fetchContent(HttpClient httpClient, HttpRequest.Builder httpRequestBuilder) {
27+
if (cachedETag != null) {
28+
httpRequestBuilder.header("If-None-Match", cachedETag);
29+
}
30+
if (cachedLastModified != null) {
31+
httpRequestBuilder.header("If-Modified-Since", cachedLastModified);
32+
}
33+
34+
HttpRequest request = httpRequestBuilder.build();
35+
HttpResponse<String> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
36+
37+
if (httpResponse.statusCode() == 200) {
38+
cachedETag = httpResponse.headers().firstValue("ETag").orElse(null);
39+
cachedLastModified = httpResponse.headers().firstValue("Last-Modified").orElse(null);
40+
log.debug("fetched new content");
41+
} else if (httpResponse.statusCode() == 304) {
42+
log.debug("got 304 Not Modified");
43+
}
44+
return httpResponse;
45+
}
46+
}

providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnector.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public class HttpConnector implements QueueSource {
5858
private Map<String, String> headers;
5959
private PayloadCacheWrapper payloadCacheWrapper;
6060
private PayloadCache payloadCache;
61+
private HttpCacheFetcher httpCacheFetcher;
6162

6263
@NonNull
6364
private String url;
@@ -66,7 +67,7 @@ public class HttpConnector implements QueueSource {
6667
public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCapacity,
6768
Integer scheduledThreadPoolSize, Integer requestTimeoutSeconds, Integer connectTimeoutSeconds, String url,
6869
Map<String, String> headers, ExecutorService httpClientExecutor, String proxyHost, Integer proxyPort,
69-
PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache) {
70+
PayloadCacheOptions payloadCacheOptions, PayloadCache payloadCache, Boolean useHttpCache) {
7071
validate(url, pollIntervalSeconds, linkedBlockingQueueCapacity, scheduledThreadPoolSize, requestTimeoutSeconds,
7172
connectTimeoutSeconds, proxyHost, proxyPort, payloadCacheOptions, payloadCache);
7273
this.pollIntervalSeconds = pollIntervalSeconds == null ? DEFAULT_POLL_INTERVAL_SECONDS : pollIntervalSeconds;
@@ -100,6 +101,9 @@ public HttpConnector(Integer pollIntervalSeconds, Integer linkedBlockingQueueCap
100101
.payloadCacheOptions(payloadCacheOptions)
101102
.build();
102103
}
104+
if (Boolean.TRUE.equals(useHttpCache)) {
105+
httpCacheFetcher = new HttpCacheFetcher();
106+
}
103107
}
104108

105109
@SneakyThrows
@@ -180,13 +184,11 @@ private boolean fetchAndUpdate() {
180184
.timeout(Duration.ofSeconds(requestTimeoutSeconds))
181185
.GET();
182186
headers.forEach(requestBuilder::header);
183-
HttpRequest request = requestBuilder
184-
.build();
185187

186188
HttpResponse<String> response;
187189
try {
188190
log.debug("fetching response");
189-
response = execute(request);
191+
response = execute(requestBuilder);
190192
} catch (IOException e) {
191193
log.info("could not fetch", e);
192194
return false;
@@ -196,14 +198,18 @@ private boolean fetchAndUpdate() {
196198
}
197199
log.debug("fetched response");
198200
String payload = response.body();
199-
if (response.statusCode() != 200) {
201+
if (!isSuccessful(response)) {
200202
log.info("received non-successful status code: {} {}", response.statusCode(), payload);
201203
return false;
204+
} else if (response.statusCode() == 304) {
205+
log.debug("got 304 Not Modified, skipping update");
206+
return false;
202207
}
203208
if (payload == null) {
204209
log.debug("payload is null");
205210
return false;
206211
}
212+
log.debug("adding payload to queue");
207213
if (!this.queue.offer(new QueuePayload(QueuePayloadType.DATA, payload))) {
208214
log.warn("Unable to offer file content to queue: queue is full");
209215
return false;
@@ -217,8 +223,15 @@ private boolean fetchAndUpdate() {
217223
return payload != null;
218224
}
219225

220-
protected HttpResponse<String> execute(HttpRequest request) throws IOException, InterruptedException {
221-
return client.send(request, HttpResponse.BodyHandlers.ofString());
226+
private static boolean isSuccessful(HttpResponse<String> response) {
227+
return response.statusCode() == 200 || response.statusCode() == 304;
228+
}
229+
230+
protected HttpResponse<String> execute(HttpRequest.Builder requestBuilder) throws IOException, InterruptedException {
231+
if (httpCacheFetcher != null) {
232+
return httpCacheFetcher.fetchContent(client, requestBuilder);
233+
}
234+
return client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());
222235
}
223236

224237
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync;
2+
3+
import static dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.sync.HttpConnectorTest.delay;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertFalse;
6+
import static org.junit.jupiter.api.Assertions.assertNotNull;
7+
import static org.junit.jupiter.api.Assertions.assertThrows;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
10+
import static org.mockito.ArgumentMatchers.any;
11+
import static org.mockito.Mockito.doReturn;
12+
import static org.mockito.Mockito.mock;
13+
import static org.mockito.Mockito.spy;
14+
import static org.mockito.Mockito.when;
15+
16+
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayload;
17+
import dev.openfeature.contrib.providers.flagd.resolver.process.storage.connector.QueuePayloadType;
18+
import java.io.IOException;
19+
import java.lang.reflect.Field;
20+
import java.net.MalformedURLException;
21+
import java.net.ProxySelector;
22+
import java.net.http.HttpClient;
23+
import java.net.http.HttpRequest;
24+
import java.net.http.HttpResponse;
25+
import java.util.HashMap;
26+
import java.util.Map;
27+
import java.util.Optional;
28+
import java.util.concurrent.BlockingQueue;
29+
import java.util.concurrent.ExecutorService;
30+
import java.util.concurrent.Executors;
31+
import java.util.concurrent.ScheduledExecutorService;
32+
import java.util.concurrent.TimeUnit;
33+
import lombok.SneakyThrows;
34+
import lombok.extern.slf4j.Slf4j;
35+
import org.junit.jupiter.api.Test;
36+
import org.mockito.Mockito;
37+
38+
/**
39+
* Integration test for the HttpConnector class, specifically testing the ability to fetch
40+
* raw content from a GitHub URL. This test assumes that integration tests are enabled
41+
* and verifies that the HttpConnector can successfully enqueue data from the specified URL.
42+
* The test initializes the HttpConnector with specific configurations, waits for data
43+
* to be enqueued, and asserts the expected queue size. The connector is shut down
44+
* gracefully after the test execution.
45+
* As this integration test using external request, it is disabled by default, and not part of the CI build.
46+
*/
47+
@Slf4j
48+
class HttpConnectorIntegrationTest {
49+
50+
@SneakyThrows
51+
@Test
52+
void testGithubRawContent() {
53+
assumeTrue(parseBoolean("integrationTestsEnabled"));
54+
HttpConnector connector = null;
55+
try {
56+
String testUrl = "https://raw.githubusercontent.com/open-feature/java-sdk-contrib/58fe5da7d4e2f6f4ae2c1caf3411a01e84a1dc1a/providers/flagd/version.txt";
57+
connector = HttpConnector.builder()
58+
.url(testUrl)
59+
.connectTimeoutSeconds(10)
60+
.requestTimeoutSeconds(10)
61+
.useHttpCache(true)
62+
.pollIntervalSeconds(5)
63+
.build();
64+
BlockingQueue<QueuePayload> queue = connector.getStreamQueue();
65+
delay(20000);
66+
assertEquals(1, queue.size());
67+
} finally {
68+
if (connector != null) {
69+
connector.shutdown();
70+
}
71+
}
72+
}
73+
74+
public static boolean parseBoolean(String key) {
75+
return Boolean.parseBoolean(System.getProperty(key, System.getenv(key)));
76+
}
77+
78+
}

providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/resolver/process/storage/connector/sync/HttpConnectorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ void testQueuePayloadTypeSetToDataOnSuccess() {
579579
}
580580

581581
@SneakyThrows
582-
private static void delay(long ms) {
582+
protected static void delay(long ms) {
583583
Thread.sleep(ms);
584584
}
585585

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
org.org.slf4j.simpleLogger.defaultLogLevel=debug
1+
org.slf4j.simpleLogger.defaultLogLevel=debug
22
org.slf4j.simpleLogger.showDateTime=
33

44
io.grpc.level=trace

0 commit comments

Comments
 (0)