From 70835860090175209b6cec1cab6443d0bc4784fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matheus=20Ver=C3=ADssimo?= <42448661+matheusverissimo@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:57:02 -0300 Subject: [PATCH] feat!: changing cache provider to caffeine over guava (#1065) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matheus VerĂ­ssimo Signed-off-by: Matheus VerĂ­ssimo --- providers/go-feature-flag/README.md | 56 ++++++++++++++++++- providers/go-feature-flag/pom.xml | 6 +- .../GoFeatureFlagProviderOptions.java | 21 +++++-- .../controller/CacheController.java | 15 +++-- .../controller/GoFeatureFlagController.java | 22 +++++--- .../GoFeatureFlagProviderTest.java | 45 +++++++-------- 6 files changed, 114 insertions(+), 51 deletions(-) diff --git a/providers/go-feature-flag/README.md b/providers/go-feature-flag/README.md index ad42714f9..e4d12d2b4 100644 --- a/providers/go-feature-flag/README.md +++ b/providers/go-feature-flag/README.md @@ -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.
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. \ No newline at end of file diff --git a/providers/go-feature-flag/pom.xml b/providers/go-feature-flag/pom.xml index 0b824f4aa..b94d04c1e 100644 --- a/providers/go-feature-flag/pom.xml +++ b/providers/go-feature-flag/pom.xml @@ -57,9 +57,9 @@ - com.google.guava - guava - 33.3.1-jre + com.github.ben-manes.caffeine + caffeine + 2.9.3 diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java index c8d38fc30..552713038 100644 --- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderOptions.java @@ -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; @@ -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: + *
+     * GoFeatureFlagProviderOptions.builder()
+     *   .caffeineConfig(
+     *      Caffeine.newBuilder()
+     *          .initialCapacity(100)
+     *          .maximumSize(100000)
+     *          .expireAfterWrite(Duration.ofMillis(5L * 60L * 1000L))
+     *          .build()
+     *    )
+     *   .build();
+     * 
+     * 
* Default: * CACHE_TTL_MS: 5min - * CACHE_CONCURRENCY_LEVEL: 1 * CACHE_INITIAL_CAPACITY: 100 * CACHE_MAXIMUM_SIZE: 100000 */ - private CacheBuilder> cacheBuilder; + private Caffeine> cacheConfig; /** * (optional) enable cache value. diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/CacheController.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/CacheController.java index d3ca23f2f..450e69517 100644 --- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/CacheController.java +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/CacheController.java @@ -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> 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> 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)) diff --git a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java index 5c98577be..648af2d69 100644 --- a/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java +++ b/providers/go-feature-flag/src/main/java/dev/openfeature/contrib/providers/gofeatureflag/controller/GoFeatureFlagController.java @@ -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; @@ -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. */ @@ -137,13 +141,13 @@ public EvaluationResponse 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()) { @@ -216,13 +220,13 @@ public void sendEventToDataCollector(List 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()) { @@ -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()) { @@ -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; diff --git a/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java b/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java index ed8c0df3f..64f2c7f07 100644 --- a/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java +++ b/providers/go-feature-flag/src/test/java/dev/openfeature/contrib/providers/gofeatureflag/GoFeatureFlagProviderTest.java @@ -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; @@ -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 { @@ -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); @@ -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