Skip to content

feat!: changing cache provider to caffeine over guava #1065

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion providers/go-feature-flag/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,62 @@ You will have a new instance ready to be used with your `open-feature` java SDK.
| **`keepAliveDuration`** | `false` | keepAliveDuration is the time in millisecond we keep the connexion open. _(default: 7200000 (2 hours))_ |
| **`apiKey`** | `false` | If the relay proxy is configured to authenticate the requests, you should provide an API Key to the provider. Please ask the administrator of the relay proxy to provide an API Key. (This feature is available only if you are using GO Feature Flag relay proxy v1.7.0 or above). _(default: null)_ |
| **`enableCache`** | `false` | enable cache value. _(default: true)_ |
| **`cacheBuilder`** | `false` | If cache custom configuration is wanted, you should provide a cache builder. _(default: null)_ |
| **`cacheConfig`** | `false` | If cache custom configuration is wanted, you should provide a [Caffeine](https://github.com/ben-manes/caffeine) configuration object. _(default: null)_ |
| **`flushIntervalMs`** | `false` | interval time we publish statistics collection data to the proxy. The parameter is used only if the cache is enabled, otherwise the collection of the data is done directly when calling the evaluation API. _(default: 1000 ms)_ |
| **`maxPendingEvents`** | `false` | max pending events aggregated before publishing for collection data to the proxy. When event is added while events collection is full, event is omitted. _(default: 10000)_ |
| **`flagChangePollingIntervalMs`** | `false` | interval time we poll the proxy to check if the configuration has changed.<br/>If the cache is enabled, we will poll the relay-proxy every X milliseconds to check if the configuration has changed. _(default: 120000)_ |
| **`disableDataCollection`** | `false` | set to true if you don't want to collect the usage of flags retrieved in the cache. _(default: false)_ |

## Breaking changes

### 0.4.0 - Cache Implementation Change: Guava to Caffeine

In this release, we have updated the cache implementation from Guava to Caffeine. This change was made because Caffeine is now the recommended caching solution by the maintainers of Guava due to its performance improvements and enhanced features.

Because of this, the cache configuration on `GoFeatureFlagProviderOptions` that used Guava's `CacheBuilder` is now handled by `Caffeine`.

#### How to migrate

Configuration cache with Guava used to be like this:

```java
import com.google.common.cache.CacheBuilder;
// ...
CacheBuilder guavaCacheBuilder = CacheBuilder.newBuilder()
.initialCapacity(100)
.maximumSize(2000);

FeatureProvider provider = new GoFeatureFlagProvider(
GoFeatureFlagProviderOptions
.builder()
.endpoint("https://my-gofeatureflag-instance.org")
.cacheBuilder(guavaCacheBuilder)
.build());

OpenFeatureAPI.getInstance().setProviderAndWait(provider);

// ...
```

Now with Caffeine it should be like this:

```java
import com.github.benmanes.caffeine.cache.Caffeine;
// ...
Caffeine caffeineCacheConfig = Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(2000);

FeatureProvider provider = new GoFeatureFlagProvider(
GoFeatureFlagProviderOptions
.builder()
.endpoint("https://my-gofeatureflag-instance.org")
.cacheConfig(caffeineCacheConfig)
.build());

OpenFeatureAPI.getInstance().setProviderAndWait(provider);

// ...
```

For a complete list of customizations options available in Caffeine, please refer to the [Caffeine documentation](https://github.com/ben-manes/caffeine/wiki) for more details.
6 changes: 3 additions & 3 deletions providers/go-feature-flag/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,9 @@
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.3.1-jre</version>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.9.3</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.openfeature.contrib.providers.gofeatureflag;

import com.google.common.cache.CacheBuilder;
import com.github.benmanes.caffeine.cache.Caffeine;

import dev.openfeature.sdk.ProviderEvaluation;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -49,14 +50,26 @@ public class GoFeatureFlagProviderOptions {

/**
* (optional) If cache custom configuration is wanted, you should provide
* a cache builder.
* a cache configuration caffeine object.
* Example:
* <pre>
* <code>GoFeatureFlagProviderOptions.builder()
* .caffeineConfig(
* Caffeine.newBuilder()
* .initialCapacity(100)
* .maximumSize(100000)
* .expireAfterWrite(Duration.ofMillis(5L * 60L * 1000L))
* .build()
* )
* .build();
* </code>
* </pre>
* Default:
* CACHE_TTL_MS: 5min
* CACHE_CONCURRENCY_LEVEL: 1
* CACHE_INITIAL_CAPACITY: 100
* CACHE_MAXIMUM_SIZE: 100000
*/
private CacheBuilder<String, ProviderEvaluation<?>> cacheBuilder;
private Caffeine<String, ProviderEvaluation<?>> cacheConfig;

/**
* (optional) enable cache value.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
package dev.openfeature.contrib.providers.gofeatureflag.controller;

import java.time.Duration;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

import dev.openfeature.contrib.providers.gofeatureflag.GoFeatureFlagProviderOptions;
import dev.openfeature.contrib.providers.gofeatureflag.bean.BeanUtils;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.ProviderEvaluation;
import lombok.Builder;

import java.time.Duration;

/**
* CacheController is a controller to manage the cache of the provider.
*/
public class CacheController {
public static final long DEFAULT_CACHE_TTL_MS = 5L * 60L * 1000L;
public static final int DEFAULT_CACHE_CONCURRENCY_LEVEL = 1;
public static final int DEFAULT_CACHE_INITIAL_CAPACITY = 100;
public static final int DEFAULT_CACHE_MAXIMUM_SIZE = 100000;
private final Cache<String, ProviderEvaluation<?>> cache;

@Builder
public CacheController(GoFeatureFlagProviderOptions options) {
this.cache = options.getCacheBuilder() != null ? options.getCacheBuilder().build() : buildDefaultCache();
this.cache = options.getCacheConfig() != null ? options.getCacheConfig().build() : buildDefaultCache();
}

private Cache<String, ProviderEvaluation<?>> buildDefaultCache() {
return CacheBuilder.newBuilder()
.concurrencyLevel(DEFAULT_CACHE_CONCURRENCY_LEVEL)
return Caffeine.newBuilder()
.initialCapacity(DEFAULT_CACHE_INITIAL_CAPACITY)
.maximumSize(DEFAULT_CACHE_MAXIMUM_SIZE)
.expireAfterWrite(Duration.ofMillis(DEFAULT_CACHE_TTL_MS))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.common.net.HttpHeaders;
import dev.openfeature.contrib.providers.gofeatureflag.EvaluationResponse;
import dev.openfeature.contrib.providers.gofeatureflag.GoFeatureFlagProviderOptions;
import dev.openfeature.contrib.providers.gofeatureflag.bean.ConfigurationChange;
Expand Down Expand Up @@ -60,6 +59,11 @@ public class GoFeatureFlagController {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private static final String BEARER_TOKEN = "Bearer ";

private static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type";
private static final String HTTP_HEADER_AUTHORIZATION = "Authorization";
private static final String HTTP_HEADER_ETAG = "ETag";
private static final String HTTP_HEADER_IF_NONE_MATCH = "If-None-Match";

/**
* apiKey contains the token to use while calling GO Feature Flag relay proxy.
*/
Expand Down Expand Up @@ -137,13 +141,13 @@ public <T> EvaluationResponse<T> evaluateFlag(

Request.Builder reqBuilder = new Request.Builder()
.url(url)
.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
.addHeader(HTTP_HEADER_CONTENT_TYPE, APPLICATION_JSON)
.post(RequestBody.create(
requestMapper.writeValueAsBytes(goffRequest),
MediaType.get("application/json; charset=utf-8")));

if (this.apiKey != null && !this.apiKey.isEmpty()) {
reqBuilder.addHeader(HttpHeaders.AUTHORIZATION, BEARER_TOKEN + this.apiKey);
reqBuilder.addHeader(HTTP_HEADER_AUTHORIZATION, BEARER_TOKEN + this.apiKey);
}

try (Response response = this.httpClient.newCall(reqBuilder.build()).execute()) {
Expand Down Expand Up @@ -216,13 +220,13 @@ public void sendEventToDataCollector(List<Event> eventsList) {

Request.Builder reqBuilder = new Request.Builder()
.url(url)
.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
.addHeader(HTTP_HEADER_CONTENT_TYPE, APPLICATION_JSON)
.post(RequestBody.create(
requestMapper.writeValueAsBytes(events),
MediaType.get("application/json; charset=utf-8")));

if (this.apiKey != null && !this.apiKey.isEmpty()) {
reqBuilder.addHeader(HttpHeaders.AUTHORIZATION, BEARER_TOKEN + this.apiKey);
reqBuilder.addHeader(HTTP_HEADER_AUTHORIZATION, BEARER_TOKEN + this.apiKey);
}

try (Response response = this.httpClient.newCall(reqBuilder.build()).execute()) {
Expand Down Expand Up @@ -259,14 +263,14 @@ public ConfigurationChange configurationHasChanged() throws GoFeatureFlagExcepti

Request.Builder reqBuilder = new Request.Builder()
.url(url)
.addHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON)
.addHeader(HTTP_HEADER_CONTENT_TYPE, APPLICATION_JSON)
.get();

if (this.etag != null && !this.etag.isEmpty()) {
reqBuilder.addHeader(HttpHeaders.IF_NONE_MATCH, this.etag);
reqBuilder.addHeader(HTTP_HEADER_IF_NONE_MATCH, this.etag);
}
if (this.apiKey != null && !this.apiKey.isEmpty()) {
reqBuilder.addHeader(HttpHeaders.AUTHORIZATION, BEARER_TOKEN + this.apiKey);
reqBuilder.addHeader(HTTP_HEADER_AUTHORIZATION, BEARER_TOKEN + this.apiKey);
}

try (Response response = this.httpClient.newCall(reqBuilder.build()).execute()) {
Expand All @@ -283,7 +287,7 @@ public ConfigurationChange configurationHasChanged() throws GoFeatureFlagExcepti
}

boolean isInitialConfiguration = this.etag == null;
this.etag = response.header(HttpHeaders.ETAG);
this.etag = response.header(HTTP_HEADER_ETAG);
return isInitialConfiguration
? ConfigurationChange.FLAG_CONFIGURATION_INITIALIZED
: ConfigurationChange.FLAG_CONFIGURATION_UPDATED;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package dev.openfeature.contrib.providers.gofeatureflag;

import static dev.openfeature.contrib.providers.gofeatureflag.controller.GoFeatureFlagController.requestMapper;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
Expand All @@ -10,42 +15,34 @@
import java.util.List;
import java.util.Map;

import com.google.common.cache.CacheBuilder;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.net.HttpHeaders;

import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidEndpoint;
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidOptions;
import dev.openfeature.sdk.Client;
import dev.openfeature.sdk.ErrorCode;
import dev.openfeature.sdk.EvaluationContext;
import dev.openfeature.sdk.FlagEvaluationDetails;
import dev.openfeature.sdk.ImmutableContext;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.ProviderState;
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import dev.openfeature.sdk.ImmutableMetadata;
import dev.openfeature.sdk.MutableContext;
import dev.openfeature.sdk.MutableStructure;
import dev.openfeature.sdk.OpenFeatureAPI;
import dev.openfeature.sdk.Reason;
import dev.openfeature.sdk.Value;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidEndpoint;
import dev.openfeature.contrib.providers.gofeatureflag.exception.InvalidOptions;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.HttpUrl;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.TestInfo;

import static dev.openfeature.contrib.providers.gofeatureflag.controller.GoFeatureFlagController.requestMapper;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

@Slf4j
class GoFeatureFlagProviderTest {
Expand Down Expand Up @@ -361,9 +358,9 @@ void should_resolve_from_cache() {
@SneakyThrows
@Test
void should_resolve_from_cache_max_size() {
CacheBuilder cacheBuilder = CacheBuilder.newBuilder().maximumSize(1);
Caffeine caffeine = Caffeine.newBuilder().maximumSize(1);
GoFeatureFlagProvider g = new GoFeatureFlagProvider(GoFeatureFlagProviderOptions.builder()
.endpoint(this.baseUrl.toString()).timeout(1000).cacheBuilder(cacheBuilder).build());
.endpoint(this.baseUrl.toString()).timeout(1000).cacheConfig(caffeine).build());
String providerName = this.testName;
OpenFeatureAPI.getInstance().setProviderAndWait(providerName, g);
Client client = OpenFeatureAPI.getInstance().getClient(providerName);
Expand Down Expand Up @@ -406,10 +403,6 @@ void should_resolve_from_cache_max_size() {
.flagMetadata(defaultMetadata)
.build();
assertEquals(wantStr2, gotStr);

// verify that value previously fetch from cache now not fetched from cache since cache max size is 1, and cache is full.
got = client.getBooleanDetails("bool_targeting_match", false, this.evaluationContext);
assertEquals(want, got);
}

@SneakyThrows
Expand Down
Loading