Skip to content

Commit 7688523

Browse files
liran2000chrfwowaepfli
authored
feat(flagd): add http connector for In-process resolver (#1299)
Signed-off-by: liran2000 <[email protected]> Signed-off-by: Liran M <[email protected]> Co-authored-by: chrfwow <[email protected]> Co-authored-by: Simon Schrottner <[email protected]>
1 parent f22e9e4 commit 7688523

File tree

19 files changed

+2637
-0
lines changed

19 files changed

+2637
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<module>providers/configcat</module>
4141
<module>providers/statsig</module>
4242
<module>providers/multiprovider</module>
43+
<module>tools/flagd-http-connector</module>
4344
</modules>
4445

4546
<scm>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Changelog

tools/flagd-http-connector/README.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Http Connector
2+
3+
## Introduction
4+
Http Connector is a tool for [flagd](https://github.com/open-feature/flagd) in-process resolver.
5+
6+
This mode performs flag evaluations locally (in-process).
7+
Flag configurations for evaluation are obtained via Http.
8+
9+
## Http Connector functionality
10+
11+
HttpConnector is responsible for polling data from a specified URL at regular intervals.
12+
It is leveraging Http cache mechanism with 'ETag' header, then when receiving 304 Not Modified response,
13+
reducing traffic, reducing rate limits effects and changes updates. Can be enabled via useHttpCache option.
14+
The implementation is using Java HttpClient.
15+
16+
## Use cases and benefits
17+
* flagd installation is not required, the Http Connector works independently.
18+
Minimizing infrastructure and DevOps overhead - no extra containers required.
19+
* Low latency by fetching data directly in-process.
20+
* Decreased external network traffic from the HTTP source, even without a standalone flagd container or proxy,
21+
when using polling cache.
22+
* Can serve as an additional provider for fallback or internal backup scenarios using a multi-provider setup.
23+
24+
### What happens if the Http source is down during application startup?
25+
26+
Http Connector supports optional resilient fail-safe initialization using a cache.
27+
If the initial fetch fails due to source unavailability, it can load the initial payload from the cache instead of
28+
falling back to default values.
29+
This ensures smoother startup behavior until the source becomes available again. To be effective, the TTL of the
30+
fallback cache should be longer than the expected duration of the source downtime during initialization.
31+
32+
### Polling cache
33+
The polling cache is used to store the payload fetched from the URL.
34+
Used when usePollingCache is configured as true.
35+
A key advantage of this cache is that it enables a single microservice within a cluster to handle the polling of a
36+
URL, effectively acting as a flagd/proxy while all other services leverage the shared cache.
37+
This approach optimizes resource usage by preventing redundant polling across services.
38+
39+
### Sample flows demonstrating the architecture
40+
41+
#### Basic Simple Configuration
42+
43+
This example demonstrates a simple flow using:
44+
- GitHub as the source for flag payload.
45+
46+
```mermaid
47+
sequenceDiagram
48+
participant service
49+
participant Github
50+
51+
service->>Github: fetch
52+
Github->>service: payload
53+
Note right of service: polling interval passed
54+
service->>Github: fetch
55+
Github->>service: payload
56+
```
57+
58+
#### A More Scalable Configuration Utilizing Fail-Safe and Polling Caching Mechanisms
59+
60+
This configuration aim to reduce network requests to the source URL, to improve performance and to improve the
61+
application's resilience to source downtime.
62+
63+
This example demonstrates a micro-services architectural flow using:
64+
- GitHub as the source for flag payload.
65+
- Redis serving as both the fail-safe initialization cache and the polling cache.
66+
67+
Example initialization flow during GitHub downtime,
68+
demonstrates how the application continues to access flag values from the cache even when GitHub is unavailable.
69+
In this setup, multiple microservices share the same cache, with only one service responsible for polling the source
70+
URL.
71+
72+
```mermaid
73+
sequenceDiagram
74+
box Cluster
75+
participant micro-service-1
76+
participant micro-service-2
77+
participant micro-service-3
78+
participant Redis
79+
end
80+
participant Github
81+
82+
break source downtime
83+
micro-service-1->>Github: initialize
84+
Github->>micro-service-1: failure
85+
end
86+
micro-service-1->>Redis: fetch
87+
Redis->>micro-service-1: failsafe payload
88+
Note right of micro-service-1: polling interval passed
89+
micro-service-1->>Github: fetch
90+
Github->>micro-service-1: payload
91+
micro-service-2->>Redis: fetch
92+
Redis->>micro-service-2: payload
93+
micro-service-3->>Redis: fetch
94+
Redis->>micro-service-3: payload
95+
96+
```
97+
98+
## Usage
99+
100+
### Installation
101+
<!-- x-release-please-start-version -->
102+
```xml
103+
<dependency>
104+
<groupId>dev.openfeature.contrib.tools</groupId>
105+
<artifactId>flagd-http-connector</artifactId>
106+
<version>0.0.1</version>
107+
</dependency>
108+
```
109+
<!-- x-release-please-end-version -->
110+
111+
### Usage example
112+
113+
```java
114+
HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder()
115+
.url("http://example.com/flags")
116+
.build();
117+
HttpConnector connector = HttpConnector.builder()
118+
.httpConnectorOptions(httpConnectorOptions)
119+
.build();
120+
121+
FlagdOptions options =
122+
FlagdOptions.builder()
123+
.resolverType(Config.Resolver.IN_PROCESS)
124+
.customConnector(connector)
125+
.build();
126+
127+
FlagdProvider flagdProvider = new FlagdProvider(options);
128+
```
129+
130+
#### HttpConnector using fail-safe cache and polling cache
131+
132+
```java
133+
PayloadCache payloadCache = new PayloadCache() {
134+
135+
@Override
136+
public void put(String key, String payload) {
137+
// implement put in cache
138+
}
139+
140+
@Override
141+
public void put(String key, String payload, int ttlSeconds) {
142+
// implement put in cache with TTL
143+
}
144+
145+
@Override
146+
public String get(String key) {
147+
// implement get from cache and return
148+
}
149+
};
150+
151+
HttpConnectorOptions httpConnectorOptions = HttpConnectorOptions.builder()
152+
.url(testUrl)
153+
.useHttpCache(true)
154+
.payloadCache(payloadCache)
155+
.payloadCacheOptions(PayloadCacheOptions.builder().build())
156+
.useFailsafeCache(true)
157+
.pollIntervalSeconds(10)
158+
.usePollingCache(true)
159+
.build();
160+
161+
HttpConnector connector = HttpConnector.builder()
162+
.httpConnectorOptions(httpConnectorOptions)
163+
.build();
164+
```
165+
166+
### Configuration
167+
The Http Connector can be configured using the following properties in the `HttpConnectorOptions` class.:
168+
169+
| Property Name | Type | Description |
170+
|-------------------------------------------|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
171+
| url | String | The URL to poll for updates. This is a required field. |
172+
| pollIntervalSeconds | Integer | The interval in seconds at which the connector will poll the URL for updates. Default is 60 seconds. |
173+
| connectTimeoutSeconds | Integer | The timeout in seconds for establishing a connection to the URL. Default is 10 seconds. |
174+
| requestTimeoutSeconds | Integer | The timeout in seconds for the request to complete. Default is 10 seconds. |
175+
| linkedBlockingQueueCapacity | Integer | The capacity of the linked blocking queue used for processing requests. Default is 100. |
176+
| scheduledThreadPoolSize | Integer | The size of the scheduled thread pool used for processing requests. Default is 2. |
177+
| headers | Map<String, String> | A map of headers to be included in the request. Default is an empty map. |
178+
| httpClientExecutor | ExecutorService | The executor service used for making HTTP requests. Default is a fixed thread pool with 1 thread. |
179+
| proxyHost | String | The host of the proxy server to use for requests. Default is null. |
180+
| proxyPort | Integer | The port of the proxy server to use for requests. Default is null. |
181+
| payloadCacheOptions | PayloadCacheOptions | Options for configuring the payload cache. Default is null. |
182+
| payloadCache | PayloadCache | The payload cache to use for caching responses. Default is null. |
183+
| useHttpCache | Boolean | Whether to use HTTP caching for the requests. Default is false. |
184+
| useFailsafeCache | Boolean | Whether to use a failsafe cache for initialization. Default is false. |
185+
| usePollingCache | Boolean | Whether to use a polling cache for initialization. Default is false. |
186+
| PayloadCacheOptions.updateIntervalSeconds | Integer | The interval, in seconds, at which the cache is updated. By default, this is set to 30 minutes. The goal is to avoid overloading fallback cache writes, since the cache serves only as a fallback mechanism. Typically, this value can be tuned to be shorter than the cache's TTL, balancing the need to minimize unnecessary updates while still handling edge cases effectively. |

tools/flagd-http-connector/pom.xml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>dev.openfeature.contrib</groupId>
7+
<artifactId>parent</artifactId>
8+
<version>[0.2,)</version>
9+
<relativePath>../../pom.xml</relativePath>
10+
</parent>
11+
<groupId>dev.openfeature.contrib.tools</groupId>
12+
<artifactId>flagdhttpconnector</artifactId>
13+
<version>0.0.1</version> <!--x-release-please-version -->
14+
15+
<name>flagd-http-connector</name>
16+
<description>Flagd Http Connector</description>
17+
<url>https://openfeature.dev</url>
18+
19+
<developers>
20+
<developer>
21+
<id>liran2000</id>
22+
<name>Liran Mendelovich</name>
23+
<organization>OpenFeature</organization>
24+
<url>https://openfeature.dev/</url>
25+
</developer>
26+
</developers>
27+
28+
<dependencies>
29+
<dependency>
30+
<groupId>dev.openfeature.contrib.providers</groupId>
31+
<artifactId>flagd</artifactId>
32+
<version>0.11.8</version>
33+
</dependency>
34+
35+
<dependency>
36+
<groupId>org.apache.commons</groupId>
37+
<artifactId>commons-lang3</artifactId>
38+
<version>3.17.0</version>
39+
</dependency>
40+
41+
<dependency>
42+
<groupId>com.google.code.findbugs</groupId>
43+
<artifactId>annotations</artifactId>
44+
<version>3.0.1</version>
45+
<scope>test</scope>
46+
</dependency>
47+
48+
</dependencies>
49+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package dev.openfeature.contrib.tools.flagd.resolver.process.storage.connector.sync.http;
2+
3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4+
import lombok.Builder;
5+
import lombok.extern.slf4j.Slf4j;
6+
7+
/**
8+
* A wrapper class for managing a payload cache with a specified update interval.
9+
* This class ensures that the cache is only updated if the specified time interval
10+
* has passed since the last update. It logs debug messages when updates are skipped
11+
* and error messages if the update process fails.
12+
* Not thread-safe.
13+
*
14+
* <p>Usage involves creating an instance with {@link PayloadCacheOptions} to set
15+
* the update interval, and then using {@link #updatePayloadIfNeeded(String)} to
16+
* conditionally update the cache and {@link #get()} to retrieve the cached payload.</p>
17+
*/
18+
@SuppressFBWarnings(
19+
value = {"EI_EXPOSE_REP2", "CT_CONSTRUCTOR_THROW"},
20+
justification = "builder validations")
21+
@Slf4j
22+
public class FailSafeCache {
23+
public static final String FAILSAFE_PAYLOAD_CACHE_KEY = FailSafeCache.class.getSimpleName() + ".failsafe-payload";
24+
private long lastUpdateTimeMs;
25+
private long updateIntervalMs;
26+
private PayloadCache payloadCache;
27+
28+
/**
29+
* Constructor for FailSafeCache.
30+
*
31+
* @param payloadCache the payload cache to be used
32+
* @param payloadCacheOptions the options for configuring the cache
33+
*/
34+
@Builder
35+
public FailSafeCache(PayloadCache payloadCache, PayloadCacheOptions payloadCacheOptions) {
36+
validate(payloadCacheOptions);
37+
this.updateIntervalMs = payloadCacheOptions.getUpdateIntervalSeconds() * 1000L;
38+
this.payloadCache = payloadCache;
39+
}
40+
41+
private static void validate(PayloadCacheOptions payloadCacheOptions) {
42+
if (payloadCacheOptions.getUpdateIntervalSeconds() < 1) {
43+
throw new IllegalArgumentException("pollIntervalSeconds must be larger than 0");
44+
}
45+
}
46+
47+
/**
48+
* Updates the payload in the cache if the specified update interval has passed.
49+
*
50+
* @param payload the payload to be cached
51+
*/
52+
public void updatePayloadIfNeeded(String payload) {
53+
if ((getCurrentTimeMillis() - lastUpdateTimeMs) < updateIntervalMs) {
54+
log.debug("not updating payload, updateIntervalMs not reached");
55+
return;
56+
}
57+
58+
try {
59+
log.debug("updating payload");
60+
payloadCache.put(FAILSAFE_PAYLOAD_CACHE_KEY, payload);
61+
lastUpdateTimeMs = getCurrentTimeMillis();
62+
} catch (Exception e) {
63+
log.error("failed updating cache", e);
64+
}
65+
}
66+
67+
protected long getCurrentTimeMillis() {
68+
return System.currentTimeMillis();
69+
}
70+
71+
/**
72+
* Retrieves the cached payload.
73+
*
74+
* @return the cached payload
75+
*/
76+
public String get() {
77+
try {
78+
return payloadCache.get(FAILSAFE_PAYLOAD_CACHE_KEY);
79+
} catch (Exception e) {
80+
log.error("failed getting from cache", e);
81+
return null;
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)