Skip to content

Commit

Permalink
feat!: changing cache provider to caffeine over guava (#1065)
Browse files Browse the repository at this point in the history
Signed-off-by: Matheus Veríssimo <[email protected]>
Signed-off-by: Matheus Veríssimo <[email protected]>
  • Loading branch information
matheusverissimo authored Nov 6, 2024
1 parent 2331fec commit 7083586
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 51 deletions.
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

0 comments on commit 7083586

Please sign in to comment.