diff --git a/build.gradle b/build.gradle index 8f6296a8..87aa10de 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,7 @@ plugins { id 'jacoco-report-aggregation' id "com.github.spotbugs" version libs.versions.spotbugs id "org.owasp.dependencycheck" version libs.versions.depcheck + id 'me.champeau.jmh' version '0.6.8' // Added JMH plugin } @@ -55,6 +56,10 @@ dependencies { compileOnly libs.lombok annotationProcessor libs.lombok + + // JMH dependencies + implementation 'org.openjdk.jmh:jmh-core:1.37' + annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' } group = 'io.harness' diff --git a/examples/src/main/java/io/harness/ff/examples/EventExampleWithFeatureSnapshot.java b/examples/src/main/java/io/harness/ff/examples/EventExampleWithFeatureSnapshot.java index 261b1704..dcf25ed3 100644 --- a/examples/src/main/java/io/harness/ff/examples/EventExampleWithFeatureSnapshot.java +++ b/examples/src/main/java/io/harness/ff/examples/EventExampleWithFeatureSnapshot.java @@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit; @Slf4j -public class EventExamplePoC { +public class EventExampleWithFeatureSnapshot { private static final String SDK_KEY = ""; private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); diff --git a/src/main/java/io/harness/cf/client/api/InnerClient.java b/src/main/java/io/harness/cf/client/api/InnerClient.java index 55192fdb..bec1ed88 100644 --- a/src/main/java/io/harness/cf/client/api/InnerClient.java +++ b/src/main/java/io/harness/cf/client/api/InnerClient.java @@ -1,6 +1,5 @@ package io.harness.cf.client.api; -import com.google.gson.Gson; import com.google.gson.JsonObject; import io.harness.cf.client.common.SdkCodes; import io.harness.cf.client.connector.*; @@ -11,7 +10,6 @@ import io.harness.cf.model.Variation; import java.util.LinkedList; import java.util.List; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; @@ -319,7 +317,7 @@ public List getFeatureSnapshots() { } public List getFeatureSnapshots(String prefix) { - if (!options.isEnableFeatureSnapshot()){ + if (!options.isEnableFeatureSnapshot()) { log.debug("FeatureSnapshot disabled, snapshot will contain only current version."); } List identifiers = repository.getAllFeatureIdentifiers(prefix); @@ -334,21 +332,10 @@ public List getFeatureSnapshots(String prefix) { } public FeatureSnapshot getFeatureSnapshot(@NonNull String identifier) { - if (!options.isEnableFeatureSnapshot()){ + if (!options.isEnableFeatureSnapshot()) { log.debug("FeatureSnapshot disabled, snapshot will contain only current version."); } - Optional ofc = repository.getCurrentAndPreviousFeatureConfig(identifier); - FeatureSnapshot result = new FeatureSnapshot(); - if (ofc.isPresent()) { - FeatureConfig[] fc = ofc.get(); - result.setPrevious(fc[0]); - result.setCurrent(fc[1]); - } - // this is here to create a deep copy of the object before its returned. - // this way we protect the cache. - Gson gson = new Gson(); - FeatureSnapshot deepCopySnapshot = gson.fromJson(gson.toJson(result), FeatureSnapshot.class); - return deepCopySnapshot; + return repository.getFeatureSnapshot(identifier); } public void on(@NonNull final Event event, @NonNull final Consumer consumer) { diff --git a/src/main/java/io/harness/cf/client/api/Query.java b/src/main/java/io/harness/cf/client/api/Query.java index ca59d369..b8a09f4d 100644 --- a/src/main/java/io/harness/cf/client/api/Query.java +++ b/src/main/java/io/harness/cf/client/api/Query.java @@ -1,6 +1,7 @@ package io.harness.cf.client.api; import io.harness.cf.model.FeatureConfig; +import io.harness.cf.model.FeatureSnapshot; import io.harness.cf.model.Segment; import java.util.List; import java.util.Optional; @@ -14,7 +15,7 @@ public interface Query { List findFlagsBySegment(@NonNull String identifier); - Optional getCurrentAndPreviousFeatureConfig(@NonNull String identifier); + FeatureSnapshot getFeatureSnapshot(@NonNull String identifier); List getAllFeatureIdentifiers(String prefix); } diff --git a/src/main/java/io/harness/cf/client/api/StorageRepository.java b/src/main/java/io/harness/cf/client/api/StorageRepository.java index cf1a1ece..0b819b1e 100644 --- a/src/main/java/io/harness/cf/client/api/StorageRepository.java +++ b/src/main/java/io/harness/cf/client/api/StorageRepository.java @@ -1,5 +1,6 @@ package io.harness.cf.client.api; +import com.google.gson.Gson; import io.harness.cf.client.common.Cache; import io.harness.cf.client.common.Storage; import io.harness.cf.client.common.Utils; @@ -98,6 +99,37 @@ public Optional getCurrentAndPreviousFeatureConfig(@NonNull Str return Optional.empty(); } + public FeatureSnapshot getFeatureSnapshot(@NonNull String identifier) { + Gson gson = new Gson(); + final String flagKey = formatFlagKey(identifier); + final String pFlagKey = formatPrevFlagKey(identifier); + + FeatureConfig pFlag = (FeatureConfig) cache.get(pFlagKey); + FeatureConfig cFlag = (FeatureConfig) cache.get(flagKey); + + if (cFlag != null) { + FeatureSnapshot deepCopySnapshot = + gson.fromJson(gson.toJson(new FeatureSnapshot(cFlag, pFlag)), FeatureSnapshot.class); + return deepCopySnapshot; + } + // if we don't have it in cache we check the file + if (this.store != null) { + pFlag = (FeatureConfig) store.get(pFlagKey); + cFlag = (FeatureConfig) store.get(flagKey); + if (pFlag != null) { + cache.set(pFlagKey, pFlag); + } + if (cFlag != null) { + cache.set(flagKey, cFlag); + } + + FeatureSnapshot deepCopySnapshot = + gson.fromJson(gson.toJson(new FeatureSnapshot(cFlag, pFlag)), FeatureSnapshot.class); + return deepCopySnapshot; + } + return null; + } + public Optional getSegment(@NonNull String identifier, boolean cacheable) { final String segmentKey = formatSegmentKey(identifier); Segment segment = (Segment) cache.get(segmentKey); diff --git a/src/test/java/io/harness/cf/client/api/StorageRepositoryTest.java b/src/test/java/io/harness/cf/client/api/StorageRepositoryTest.java index 0cbd3ed2..27c77aa3 100644 --- a/src/test/java/io/harness/cf/client/api/StorageRepositoryTest.java +++ b/src/test/java/io/harness/cf/client/api/StorageRepositoryTest.java @@ -4,6 +4,7 @@ import com.google.gson.Gson; import io.harness.cf.model.FeatureConfig; +import io.harness.cf.model.FeatureSnapshot; import java.awt.*; import java.io.File; import java.io.IOException; @@ -12,7 +13,6 @@ import java.nio.file.Paths; import java.util.LinkedList; import java.util.List; -import java.util.Optional; import java.util.stream.Stream; import lombok.NonNull; import org.junit.jupiter.api.Test; @@ -20,9 +20,6 @@ class StorageRepositoryTest { private final Gson gson = new Gson(); - // initialize the config and load the flags. - // load the test - @Test void shouldInitialiseRepo() { final Repository repository = new StorageRepository(new CaffeineCache(10000), null, false); @@ -43,12 +40,10 @@ void shouldStoreCurrentConfig() throws Exception { loadFlags(repository, makeFeatureList(featureConfig)); loadFlags(repository, makeFeatureList(featureConfigUpdated)); - Optional res = - repository.getCurrentAndPreviousFeatureConfig(featureConfigUpdated.getFeature()); - FeatureConfig[] resFc = res.get(); + FeatureSnapshot res = repository.getFeatureSnapshot(featureConfigUpdated.getFeature()); - FeatureConfig previous = resFc[0]; - FeatureConfig current = resFc[1]; + FeatureConfig previous = res.getPrevious(); + FeatureConfig current = res.getCurrent(); // check if previous version is null assertNull(previous); @@ -79,12 +74,10 @@ void shouldStoreCurrentConfigWithFileStore() throws Exception { loadFlags(repository, makeFeatureList(featureConfig)); loadFlags(repository, makeFeatureList(featureConfigUpdated)); - Optional res = - repository.getCurrentAndPreviousFeatureConfig(featureConfigUpdated.getFeature()); - FeatureConfig[] resFc = res.get(); + FeatureSnapshot res = repository.getFeatureSnapshot(featureConfigUpdated.getFeature()); - FeatureConfig previous = resFc[0]; - FeatureConfig current = resFc[1]; + FeatureConfig previous = res.getPrevious(); + FeatureConfig current = res.getCurrent(); // check if previous version is null assertNull(previous); @@ -115,12 +108,10 @@ void shouldStorePreviousAndCurrentConfigWithFileStore() throws Exception { loadFlags(repository, makeFeatureList(featureConfig)); loadFlags(repository, makeFeatureList(featureConfigUpdated)); - Optional res = - repository.getCurrentAndPreviousFeatureConfig(featureConfigUpdated.getFeature()); - FeatureConfig[] resFc = res.get(); + FeatureSnapshot res = repository.getFeatureSnapshot(featureConfigUpdated.getFeature()); - FeatureConfig previous = resFc[0]; - FeatureConfig current = resFc[1]; + FeatureConfig previous = res.getPrevious(); + FeatureConfig current = res.getCurrent(); // check if previous version is null assertNotNull(previous); @@ -145,12 +136,10 @@ void shouldStorePreviousAndCurrentConfig() throws Exception { loadFlags(repository, makeFeatureList(featureConfig)); loadFlags(repository, makeFeatureList(featureConfigUpdated)); - Optional res = - repository.getCurrentAndPreviousFeatureConfig(featureConfigUpdated.getFeature()); - FeatureConfig[] resFc = res.get(); + FeatureSnapshot res = repository.getFeatureSnapshot(featureConfigUpdated.getFeature()); - FeatureConfig previous = resFc[0]; - FeatureConfig current = resFc[1]; + FeatureConfig previous = res.getPrevious(); + FeatureConfig current = res.getCurrent(); // check if previous version is null assertNotNull(previous); @@ -177,12 +166,10 @@ void shouldDeletePreviousAndCurrentConfig() throws Exception { String featureIdentifier = featureConfig.getFeature(); - Optional res = - repository.getCurrentAndPreviousFeatureConfig(featureIdentifier); - FeatureConfig[] resFc = res.get(); + FeatureSnapshot res = repository.getFeatureSnapshot(featureConfigUpdated.getFeature()); - FeatureConfig previous = resFc[0]; - FeatureConfig current = resFc[1]; + FeatureConfig previous = res.getPrevious(); + FeatureConfig current = res.getCurrent(); // check if previous version is null assertNotNull(previous); @@ -194,10 +181,9 @@ void shouldDeletePreviousAndCurrentConfig() throws Exception { // delete config repository.deleteFlag(featureIdentifier); - Optional result = - repository.getCurrentAndPreviousFeatureConfig(featureIdentifier); + FeatureSnapshot result = repository.getFeatureSnapshot(featureConfigUpdated.getFeature()); - assertFalse(result.isPresent(), "The Optional should be empty"); + assertNull(result, "The Optional should be empty"); } @Test @@ -269,4 +255,17 @@ private FeatureConfig GetFeatureConfigFromFile() throws Exception { } return null; } + + private List createBenchmarkData(int flagNumber, int version) throws Exception { + FeatureConfig fg = GetFeatureConfigFromFile(); + List list = new LinkedList(); + for (int i = 1; i <= flagNumber; i++) { + FeatureConfig f = fg; + f.setFeature("simpleBool" + i); + f.setVersion(new Long(version)); + list.add(f); + } + // System.out.println(list); + return list; + } } diff --git a/src/test/java/io/harness/cf/client/api/StoreRepositoryBenchmark.java b/src/test/java/io/harness/cf/client/api/StoreRepositoryBenchmark.java new file mode 100644 index 00000000..f34ccc63 --- /dev/null +++ b/src/test/java/io/harness/cf/client/api/StoreRepositoryBenchmark.java @@ -0,0 +1,178 @@ +package io.harness.cf.client.api; + +import com.google.gson.Gson; +import io.harness.cf.model.FeatureConfig; +import io.harness.cf.model.FeatureSnapshot; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.*; + +/* + How to run it. + ./gradlew clean build + ./gradlew jmh + + some results: + Benchmark Mode Cnt Score Error Units + StoreRepositoryBenchmark.BenchmarkGetFeatureSnapshotsCurrentOnly avgt 5 805.511 ± 105.154 ms/op + StoreRepositoryBenchmark.BenchmarkGetFeatureSnapshotsPreviousAndCurrent avgt 5 860.745 ± 61.876 ms/op + StoreRepositoryBenchmark.BenchmarkLoadFeatureConfigCurrentOnly avgt 5 1.502 ± 0.198 ms/op + StoreRepositoryBenchmark.BenchmarkLoadFeatureConfigPreviousAndCurrent avgt 5 2.071 ± 0.650 ms/op + +*/ + +@State(Scope.Thread) +public class StoreRepositoryBenchmark { + + private Repository setCurrentOnlyRepository; + private Repository setCurrentAndPreviousRepository; + + private Repository getSnapshotCurrentOnlyRepository; + private Repository getSnapshotCurrentAndPreviousRepository; + private List featureConfigs; + private List updatedFeatureConfigs; + private final Gson gson = new Gson(); + private final int FeatureConfigSize = 10000; + private final int CacheSize = FeatureConfigSize * 2; + + @Setup + public void setup() throws Exception { + TestUtils tu = new TestUtils(); + featureConfigs = tu.CreateBenchmarkData(FeatureConfigSize, 1); + updatedFeatureConfigs = tu.CreateBenchmarkData(FeatureConfigSize, 2); + + setupRepoWithCurrentOnlyRepository(); + setupRepoWithCurrentAndPreviousRepository(); + setupRepoWithCurrentOnlyRepositoryForGetSnapshot(); + setupRepoWithCurrentAndPreviousRepositoryForGetSnapshot(); + } + + @Setup + public void setupRepoWithCurrentOnlyRepository() throws Exception { + + TestUtils tu = new TestUtils(); + setCurrentOnlyRepository = new StorageRepository(new CaffeineCache(CacheSize), null, false); + // Initial loading of feature configurations + loadFlags(setCurrentOnlyRepository, featureConfigs); + } + + @Setup + public void setupRepoWithCurrentAndPreviousRepository() throws Exception { + + TestUtils tu = new TestUtils(); + setCurrentAndPreviousRepository = + new StorageRepository(new CaffeineCache(CacheSize), null, true); + // Initial loading of feature configurations + loadFlags(setCurrentAndPreviousRepository, featureConfigs); + } + + @Setup + public void setupRepoWithCurrentOnlyRepositoryForGetSnapshot() throws Exception { + + TestUtils tu = new TestUtils(); + getSnapshotCurrentOnlyRepository = + new StorageRepository(new CaffeineCache(CacheSize), null, false); + // Initial loading of feature configurations + loadFlags(getSnapshotCurrentOnlyRepository, featureConfigs); + loadFlags(getSnapshotCurrentOnlyRepository, updatedFeatureConfigs); + } + + @Setup + public void setupRepoWithCurrentAndPreviousRepositoryForGetSnapshot() throws Exception { + TestUtils tu = new TestUtils(); + getSnapshotCurrentAndPreviousRepository = + new StorageRepository(new CaffeineCache(CacheSize), null, true); + // Initial loading of feature configurations + loadFlags(getSnapshotCurrentAndPreviousRepository, featureConfigs); + loadFlags(getSnapshotCurrentAndPreviousRepository, updatedFeatureConfigs); + } + + @Fork(value = 1, warmups = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void BenchmarkLoadFeatureConfigCurrentOnly() { + // Measure average time taken to load the updated feature configurations while storing only + // current snapshot + loadFlags(setCurrentOnlyRepository, updatedFeatureConfigs); + } + + @Fork(value = 1, warmups = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void BenchmarkLoadFeatureConfigPreviousAndCurrent() { + // Measure average time taken to load the updated feature configurations while storing current + // config as well as keeping previous one. + loadFlags(setCurrentAndPreviousRepository, updatedFeatureConfigs); + } + + @Fork(value = 1, warmups = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void BenchmarkGetFeatureSnapshotsCurrentOnly() { + // Measures time taken to get snapshots + List snapshots = getFeatureSnapshots(getSnapshotCurrentOnlyRepository); + if (snapshots == null) { + throw new IllegalStateException("Snapshots are null"); + } + if (snapshots.size() != FeatureConfigSize) { + throw new IllegalStateException("Snapshots not equal"); + } + + for (int i = 0; i < FeatureConfigSize; i++) { + FeatureSnapshot fs = snapshots.get(i); + if (fs.getPrevious() != null) { + throw new IllegalStateException("Snapshots contains previous"); + } + if (fs.getCurrent() == null) { + throw new IllegalStateException("Snapshots does not contain current"); + } + } + } + + @Fork(value = 1, warmups = 1) + @OutputTimeUnit(TimeUnit.MILLISECONDS) + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public void BenchmarkGetFeatureSnapshotsPreviousAndCurrent() { + // Measures time taken to get snapshots + List snapshots = getFeatureSnapshots(getSnapshotCurrentAndPreviousRepository); + if (snapshots == null) { + throw new IllegalStateException("Snapshots are null"); + } + if (snapshots.size() != FeatureConfigSize) { + throw new IllegalStateException("Snapshots not equal"); + } + + for (int i = 0; i < FeatureConfigSize; i++) { + FeatureSnapshot fs = snapshots.get(i); + if (fs.getPrevious() == null) { + throw new IllegalStateException("Snapshots does not contain previous"); + } + if (fs.getCurrent() == null) { + throw new IllegalStateException("Snapshots does not contain current"); + } + } + } + + private List getFeatureSnapshots(Repository repository) { + List identifiers = repository.getAllFeatureIdentifiers(""); + List snapshots = new LinkedList<>(); + for (String identifier : identifiers) { + FeatureSnapshot snapshot = repository.getFeatureSnapshot(identifier); + snapshots.add(snapshot); + } + return snapshots; + } + + private void loadFlags(Repository repository, List flags) { + if (flags != null) { + for (FeatureConfig nextFlag : flags) { + repository.setFlag(nextFlag.getFeature(), nextFlag); + } + } + } +} diff --git a/src/test/java/io/harness/cf/client/api/TestUtils.java b/src/test/java/io/harness/cf/client/api/TestUtils.java index 98894632..8f406e8c 100644 --- a/src/test/java/io/harness/cf/client/api/TestUtils.java +++ b/src/test/java/io/harness/cf/client/api/TestUtils.java @@ -1,10 +1,15 @@ package io.harness.cf.client.api; +import com.google.gson.Gson; +import io.harness.cf.model.FeatureConfig; +import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.LinkedList; +import java.util.List; public class TestUtils { @@ -28,4 +33,40 @@ public static String getJsonResource(String location) throws IOException, URISyn final Path path = Paths.get(EvaluatorTest.class.getClassLoader().getResource(location).toURI()); return new String(Files.readAllBytes(path)); } + + private final Gson gson = new Gson(); + + public List CreateBenchmarkData(int size, int version) throws Exception { + FeatureConfig fg = GetFeatureConfigFromFile(); + List list = new LinkedList(); + for (int i = 1; i <= size; i++) { + FeatureConfig f = fg; + f.setFeature("simpleBool" + i); + f.setVersion(new Long(version)); + // we are copying objects + FeatureConfig df = gson.fromJson(gson.toJson(f), FeatureConfig.class); + list.add(df); + } + // System.out.println(list); + return list; + } + + public FeatureConfig GetFeatureConfigFromFile() throws Exception { + try { + String relativePath = + "./src/test/resources/local-test-cases/basic_bool_string_for_repository.json"; + // Resolve the absolute path + String filePath = new File(relativePath).getCanonicalPath(); + // Read the content of the file into a String + String jsonString = new String(Files.readAllBytes(Paths.get(filePath))); + + final FeatureConfig featureConfig = gson.fromJson(jsonString, FeatureConfig.class); + return featureConfig; + + } catch (IOException e) { + // Handle exceptions like file not found or read errors + e.printStackTrace(); + } + return null; + } }