diff --git a/providers/flagd/pom.xml b/providers/flagd/pom.xml index 47073039a..dda9f9783 100644 --- a/providers/flagd/pom.xml +++ b/providers/flagd/pom.xml @@ -19,6 +19,7 @@ 1.79.0 3.25.6 + 1.2.24 flagd @@ -168,6 +169,13 @@ 2.0.17 test + + + com.vmlens + api + ${com.vmlens.version} + test + @@ -281,6 +289,25 @@ dev.openfeature.contrib.- + + com.vmlens + vmlens-maven-plugin + ${com.vmlens.version} + + + test + + test + + + + **/*CTest.java + + true + + + + diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java index fec52f8e6..976870a97 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResources.java @@ -18,7 +18,9 @@ class FlagdProviderSyncResources { @Setter private volatile ProviderEvent previousEvent; + @Setter private volatile boolean isFatal; + private volatile ProviderEventDetails fatalProviderEventDetails; private volatile EvaluationContext enrichedContext = new ImmutableContext(); @@ -36,7 +38,7 @@ public void setEnrichedContext(EvaluationContext context) { * @return true iff this was the first call to {@code initialize()} */ public synchronized boolean initialize() { - if (this.isInitialized) { + if (this.isInitialized || this.isShutDown || this.isFatal) { return false; } this.isInitialized = true; diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java index ba69b3ad7..d6bf31318 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/InProcessResolver.java @@ -216,7 +216,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " not found") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -225,7 +225,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC return ProviderEvaluation.builder() .errorMessage("flag: " + key + " is disabled") .errorCode(ErrorCode.FLAG_NOT_FOUND) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -260,7 +260,7 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC .reason(Reason.ERROR.toString()) .errorCode(ErrorCode.FLAG_NOT_FOUND) .errorMessage("Flag '" + key + "' has no default variant defined, will use code default") - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } @@ -285,11 +285,11 @@ private ProviderEvaluation resolve(Class type, String key, EvaluationC .value((T) value) .variant(resolvedVariant) .reason(reason) - .flagMetadata(getFlagMetadata(storageQueryResult)) + .flagMetadata(getFlagMetadata(storageQueryResult, scope)) .build(); } - private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) { + private static ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult, String scope) { ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder = ImmutableMetadata.builder(); for (Map.Entry entry : storageQueryResult.getFlagSetMetadata().entrySet()) { @@ -310,7 +310,7 @@ private ImmutableMetadata getFlagMetadata(StorageQueryResult storageQueryResult) return metadataBuilder.build(); } - private void addEntryToMetadataBuilder( + private static void addEntryToMetadataBuilder( ImmutableMetadata.ImmutableMetadataBuilder metadataBuilder, String key, Object value) { if (value instanceof Number) { if (value instanceof Long) { diff --git a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java index 2eaf2ed87..b9b549f57 100644 --- a/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java +++ b/providers/flagd/src/main/java/dev/openfeature/contrib/providers/flagd/resolver/process/model/FeatureFlag.java @@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -39,7 +39,7 @@ public FeatureFlag( this.variants = variants; this.targeting = targeting; if (metadata == null) { - this.metadata = new HashMap<>(); + this.metadata = Collections.emptyMap(); } else { this.metadata = metadata; } @@ -51,7 +51,7 @@ public FeatureFlag(String state, String defaultVariant, Map vari this.defaultVariant = defaultVariant; this.variants = variants; this.targeting = targeting; - this.metadata = new HashMap<>(); + this.metadata = Collections.emptyMap(); } /** Get targeting rule of the flag. */ diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCTest.java new file mode 100644 index 000000000..3b9536876 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderCTest.java @@ -0,0 +1,107 @@ +package dev.openfeature.contrib.providers.flagd; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.vmlens.api.AllInterleavings; +import com.vmlens.api.Runner; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.sdk.ImmutableContext; +import dev.openfeature.sdk.OpenFeatureAPI; +import dev.openfeature.sdk.Value; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class FlagdProviderCTest { + private FlagdProvider provider; + + @BeforeEach + void setup() throws Exception { + provider = FlagdTestUtils.createInProcessProvider(Map.of( + "flag", + new FeatureFlag( + "ENABLED", + "a", + Map.of("a", "a", "b", "b", "c", "c"), + "{\n" + + " \"if\": [\n" + + " {\n" + + " \"ends_with\": [\n" + + " {\n" + + " \"var\": \"email\"\n" + + " },\n" + + " \"@openfeature.dev\"\n" + + " ]\n" + + " },\n" + + " \"b\",\n" + + " \"c\"\n" + + " ]\n" + + " }", + null))); + provider.initialize(ImmutableContext.EMPTY); + } + + @Test + @Disabled( + "There is a race condition in the JsonLogic library, but it does not affect us as we don't add operators after program startup") + void concurrentFlagEvaluationsWork() { + var invocationContext = ImmutableContext.EMPTY; + + try (var interleavings = new AllInterleavings("Concurrent Flag evaluations")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> assertEquals( + "c", + provider.getStringEvaluation("flag", "z", invocationContext) + .getValue()), + () -> assertEquals( + "c", + provider.getStringEvaluation("flag", "z", invocationContext) + .getValue())); + } + } + } + + @Test + void flagEvaluationsWhileSettingContextWork() { + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + var client = OpenFeatureAPI.getInstance().getClient(); + + var invocationContext = ImmutableContext.EMPTY; + var clientContext = new ImmutableContext(Map.of("email", new Value("someone@openfeature.dev"))); + + try (var interleavings = new AllInterleavings("Concurrently setting client context and evaluating a Flag")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> assertTrue(List.of("b", "c") + .contains(provider.getStringEvaluation("flag", "z", invocationContext) + .getValue())), + () -> client.setEvaluationContext(clientContext)); + } + } + } + + @Test + void settingDifferentContextsWorks() { + OpenFeatureAPI.getInstance().setProviderAndWait(provider); + var client = OpenFeatureAPI.getInstance().getClient(); + + var invocationContext = ImmutableContext.EMPTY; + var clientContext = new ImmutableContext(Map.of("email", new Value("someone@openfeature.dev"))); + var apiContext = new ImmutableContext(Map.of("email", new Value("someone.else@test.com"))); + + try (var interleavings = new AllInterleavings("Concurrently setting client and api context")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> client.setEvaluationContext(clientContext), + () -> OpenFeatureAPI.getInstance().setEvaluationContext(apiContext), + () -> assertTrue(List.of("b", "c") + .contains(provider.getStringEvaluation("flag", "z", invocationContext) + .getValue()))); + } + } + } +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java new file mode 100644 index 000000000..f8c4d0977 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesCTest.java @@ -0,0 +1,381 @@ +package dev.openfeature.contrib.providers.flagd; + +import com.vmlens.api.AllInterleavings; +import com.vmlens.api.Runner; +import dev.openfeature.sdk.ErrorCode; +import dev.openfeature.sdk.ProviderEventDetails; +import dev.openfeature.sdk.exceptions.FatalError; +import dev.openfeature.sdk.exceptions.GeneralError; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +class FlagdProviderSyncResourcesCTest { + private static final long MAX_TIME_TOLERANCE = 20; + + private FlagdProviderSyncResources flagdProviderSyncResources; + + @BeforeEach + void setUp() { + flagdProviderSyncResources = new FlagdProviderSyncResources(); + } + + @Timeout(2) + @Test + void waitForInitialization_failsWhenDeadlineElapses() { + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(2)); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + } + + @Timeout(2) + @Test + void waitForInitialization_waitsApproxForDeadline() { + final AtomicLong start = new AtomicLong(); + final AtomicLong end = new AtomicLong(); + final long deadline = 45; + + start.set(System.currentTimeMillis()); + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); + end.set(System.currentTimeMillis()); + + final long elapsed = end.get() - start.get(); + // should wait at least for the deadline + Assertions.assertTrue(elapsed >= deadline); + // should not wait much longer than the deadline + Assertions.assertTrue( + elapsed < deadline + MAX_TIME_TOLERANCE, + () -> "elapsed time: " + elapsed + " deadline: " + deadline + " max tolerance: " + MAX_TIME_TOLERANCE); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + } + + @Timeout(2) + @Test + void interruptingWaitingThread_isIgnored() throws InterruptedException { + try (var interleavings = new AllInterleavings("calling interrupt on a waiting thread is ignored")) { + final long deadline = 10; + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + var waitingThread = new Thread(() -> { + startTime.set(System.currentTimeMillis()); + Assertions.assertThrows( + GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); + endTime.set(System.currentTimeMillis()); + }); + waitingThread.start(); + + waitingThread.interrupt(); + + waitingThread.join(); + + long duration = endTime.get() - startTime.get(); + // even though thread was interrupted, it still waited for the deadline + Assertions.assertTrue(duration >= deadline); + Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + } + } + } + + @Timeout(5) + @Test + void callingInitialize_wakesUpWaitingThread() { + try (var interleavings = new AllInterleavings("calling initialize() wakes up waiting thread")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + flagdProviderSyncResources.waitForInitialization(10000); + endTime.set(System.currentTimeMillis()); + Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.initialize(); + }); + + Assertions.assertTrue( + endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, + () -> "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms"); + } + } + } + + @Timeout(5) + @Test + void callingShutdown_wakesUpWaitingThreadWithException() { + try (var interleavings = new AllInterleavings("calling shutdown() wakes up waiting thread with exception")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + Assertions.assertThrows( + GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + endTime.set(System.currentTimeMillis()); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isFatal()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.shutdown(); + }); + + Assertions.assertTrue( + endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, + () -> "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms"); + } + } + } + + @Timeout(5) + @Test + void callingFatalError_wakesUpWaitingThreadWithException() { + try (var interleavings = + new AllInterleavings("calling setFatal(true) wakes up waiting thread with exception")) { + while (interleavings.hasNext()) { + final var startTime = new AtomicLong(); + final var endTime = new AtomicLong(); + Runner.runParallel( + () -> { + Assertions.assertThrows( + FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + endTime.set(System.currentTimeMillis()); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + Assertions.assertTrue(flagdProviderSyncResources.isFatal()); + }, + () -> { + startTime.set(System.currentTimeMillis()); + flagdProviderSyncResources.setFatal(true); + }); + + Assertions.assertTrue( + endTime.get() - startTime.get() <= MAX_TIME_TOLERANCE, + () -> "Expected waiting thread to be released shortly after initialization, but waited for " + + (endTime.get() - startTime.get()) + "ms"); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializesWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.initialize()); + Assertions.assertTrue(flagdProviderSyncResources.isInitialized()); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializeAndShutdownShutsDownWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() and shutdown() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.shutdown()); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializeAndShutdownAndSetFatalShutsDownWork() { + try (var interleavings = + new AllInterleavings("concurrent initialize() and shutdown() and fatal() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), + () -> flagdProviderSyncResources.shutdown(), + () -> flagdProviderSyncResources.setFatal(true)); + Assertions.assertFalse(flagdProviderSyncResources.isInitialized()); + Assertions.assertTrue(flagdProviderSyncResources.isShutDown()); + Assertions.assertTrue(flagdProviderSyncResources.isFatal()); + } + } + } + + @Timeout(5) + @Test + void concurrentInitializeAndSetFatalShutsDownWork() { + try (var interleavings = new AllInterleavings("concurrent initialize() and fatal() calls work")) { + while (interleavings.hasNext()) { + Runner.runParallel( + () -> flagdProviderSyncResources.initialize(), () -> flagdProviderSyncResources.setFatal(true)); + Assertions.assertFalse(flagdProviderSyncResources.isShutDown()); + Assertions.assertTrue(flagdProviderSyncResources.isFatal()); + } + } + } + + @Timeout(2) + @Test + void waitForInitializationAfterCallingInitialize_returnsInstantly() { + flagdProviderSyncResources.initialize(); + long start = System.currentTimeMillis(); + flagdProviderSyncResources.waitForInitialization(10000); + long end = System.currentTimeMillis(); + // do not use MAX_TIME_TOLERANCE here, this should happen faster than that + Assertions.assertTrue(start + 1 >= end); + } + + @Timeout(2) + @Test + void waitForInitializationAfterCallingShutdown_returnsInstantly() { + flagdProviderSyncResources.shutdown(); + long start = System.currentTimeMillis(); + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + long end = System.currentTimeMillis(); + // do not use MAX_TIME_TOLERANCE here, this should happen faster than that + Assertions.assertTrue(start + 10 >= end); + } + + @Timeout(2) + @Test + void waitForInitializationAfterCallingFatal_returnsInstantly() { + flagdProviderSyncResources.setFatal(true); + long start = System.currentTimeMillis(); + Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + long end = System.currentTimeMillis(); + // for some reason, throwing the exception takes very long + Assertions.assertTrue(start + MAX_TIME_TOLERANCE >= end); + } + + @Timeout(2) + @Test + void initializeAfterFatalReturnsFalse() { + flagdProviderSyncResources.setFatal(true); + Assertions.assertFalse(flagdProviderSyncResources.initialize()); + } + + @Timeout(2) + @Test + void initializeAfterShutdownReturnsFalse() { + flagdProviderSyncResources.shutdown(); + Assertions.assertFalse(flagdProviderSyncResources.initialize()); + } + + @Timeout(2) + @Test + void initializeAfterInitializeReturnsFalse() { + flagdProviderSyncResources.initialize(); + Assertions.assertFalse(flagdProviderSyncResources.initialize()); + } + + @Timeout(2) + @Test + void firstInitializeReturnsTrue() { + Assertions.assertTrue(flagdProviderSyncResources.initialize()); + } + + @Timeout(2) + @Test + void fatalHasPrecedenceOverInitAndShutdown() { + flagdProviderSyncResources.fatalError(null); + flagdProviderSyncResources.initialize(); + flagdProviderSyncResources.shutdown(); + + Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + } + + @Timeout(2) + @Test + void fatalAbortsInit() throws InterruptedException { + final AtomicBoolean isWaiting = new AtomicBoolean(); + final AtomicLong waitTime = new AtomicLong(Long.MAX_VALUE); + final AtomicReference fatalException = new AtomicReference<>(); + + Thread waitingThread = new Thread(() -> { + long start = System.currentTimeMillis(); + isWaiting.set(true); + try { + flagdProviderSyncResources.waitForInitialization(10000); + } catch (Exception e) { + fatalException.set(e); + } + long end = System.currentTimeMillis(); + long duration = end - start; + waitTime.set(duration); + }); + waitingThread.start(); + + while (!isWaiting.get()) { + Thread.yield(); + } + + Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime + + var fatalEvent = ProviderEventDetails.builder() + .errorCode(ErrorCode.PROVIDER_FATAL) + .message("Some message") + .build(); + flagdProviderSyncResources.fatalError(fatalEvent); + + waitingThread.join(); + + var wait = MAX_TIME_TOLERANCE * 3; + + Assertions.assertTrue( + waitTime.get() < wait, + () -> "Wakeup should be almost instant, but took " + waitTime.get() + + " ms, which is more than the max of" + + wait + " ms"); + Assertions.assertNotNull(fatalException.get()); + Assertions.assertInstanceOf(FatalError.class, fatalException.get()); + Assertions.assertEquals( + "Initialization failed due to a fatal error: " + fatalEvent.getMessage(), + fatalException.get().getMessage()); + } + + @Timeout(2) + @Test + void callingShutdownWithPreviousNonFatal_wakesUpWaitingThread_WithGeneralException() throws InterruptedException { + final AtomicBoolean isWaiting = new AtomicBoolean(); + final AtomicBoolean successfulTest = new AtomicBoolean(); + + Thread waitingThread = new Thread(() -> { + long start = System.currentTimeMillis(); + isWaiting.set(true); + Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); + + long end = System.currentTimeMillis(); + long duration = end - start; + var wait = MAX_TIME_TOLERANCE * 3; + successfulTest.set(duration < wait); + }); + waitingThread.start(); + + while (!isWaiting.get()) { + Thread.yield(); + } + + Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime + + flagdProviderSyncResources.shutdown(); + + waitingThread.join(); + + Assertions.assertTrue(successfulTest.get()); + } +} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java deleted file mode 100644 index dd5dbe73a..000000000 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderSyncResourcesTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package dev.openfeature.contrib.providers.flagd; - -import dev.openfeature.sdk.ErrorCode; -import dev.openfeature.sdk.ProviderEventDetails; -import dev.openfeature.sdk.exceptions.FatalError; -import dev.openfeature.sdk.exceptions.GeneralError; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -class FlagdProviderSyncResourcesTest { - private static final long MAX_TIME_TOLERANCE = 20; - - private FlagdProviderSyncResources flagdProviderSyncResources; - - @BeforeEach - void setUp() { - flagdProviderSyncResources = new FlagdProviderSyncResources(); - } - - @Timeout(2) - @Test - void waitForInitialization_failsWhenDeadlineElapses() { - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(2)); - } - - @Timeout(2) - @Test - void waitForInitialization_waitsApproxForDeadline() { - final AtomicLong start = new AtomicLong(); - final AtomicLong end = new AtomicLong(); - final long deadline = 45; - - start.set(System.currentTimeMillis()); - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); - end.set(System.currentTimeMillis()); - - final long elapsed = end.get() - start.get(); - // should wait at least for the deadline - Assertions.assertTrue(elapsed >= deadline); - // should not wait much longer than the deadline - Assertions.assertTrue(elapsed < deadline + MAX_TIME_TOLERANCE); - } - - @Timeout(2) - @Test - void interruptingWaitingThread_isIgnored() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final long deadline = 500; - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows( - GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(deadline)); - - long end = System.currentTimeMillis(); - long duration = end - start; - // even though thread was interrupted, it still waited for the deadline - Assertions.assertTrue(duration >= deadline); - Assertions.assertTrue(duration < deadline + MAX_TIME_TOLERANCE); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - for (int i = 0; i < 50; i++) { - waitingThread.interrupt(); - Thread.sleep(10); - } - - waitingThread.join(); - } - - @Timeout(2) - @Test - void callingInitialize_wakesUpWaitingThread() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicLong waitTime = new AtomicLong(Long.MAX_VALUE); - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - flagdProviderSyncResources.waitForInitialization(10000); - long end = System.currentTimeMillis(); - long duration = end - start; - waitTime.set(duration); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - flagdProviderSyncResources.initialize(); - - waitingThread.join(); - - var wait = MAX_TIME_TOLERANCE * 3; - - Assertions.assertTrue( - waitTime.get() < wait, - () -> "Wakeup should be almost instant, but took " + waitTime.get() - + " ms, which is more than the max of" - + wait + " ms"); - } - - @Timeout(2) - @Test - void callingShutdownWithPreviousNonFatal_wakesUpWaitingThread_WithGeneralException() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicBoolean successfulTest = new AtomicBoolean(); - - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows(GeneralError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); - - long end = System.currentTimeMillis(); - long duration = end - start; - var wait = MAX_TIME_TOLERANCE * 3; - successfulTest.set(duration < wait); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - flagdProviderSyncResources.shutdown(); - - waitingThread.join(); - - Assertions.assertTrue(successfulTest.get()); - } - - @Timeout(2) - @Test - void callingShutdownWithPreviousFatal_wakesUpWaitingThread_WithFatalException() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicBoolean successfulTest = new AtomicBoolean(); - flagdProviderSyncResources.fatalError(null); - - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); - - long end = System.currentTimeMillis(); - long duration = end - start; - var wait = MAX_TIME_TOLERANCE * 3; - successfulTest.set(duration < wait); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - flagdProviderSyncResources.shutdown(); - - waitingThread.join(); - - Assertions.assertTrue(successfulTest.get()); - } - - @Timeout(2) - @Test - void waitForInitializationAfterCallingInitialize_returnsInstantly() { - flagdProviderSyncResources.initialize(); - long start = System.currentTimeMillis(); - flagdProviderSyncResources.waitForInitialization(10000); - long end = System.currentTimeMillis(); - // do not use MAX_TIME_TOLERANCE here, this should happen faster than that - Assertions.assertTrue(start + 1 >= end); - } - - @Timeout(2) - @Test - void fatalHasPrecedenceOverInitAndShutdown() { - flagdProviderSyncResources.fatalError(null); - flagdProviderSyncResources.initialize(); - flagdProviderSyncResources.shutdown(); - - Assertions.assertThrows(FatalError.class, () -> flagdProviderSyncResources.waitForInitialization(10000)); - } - - @Timeout(2) - @Test - void fatalAbortsInit() throws InterruptedException { - final AtomicBoolean isWaiting = new AtomicBoolean(); - final AtomicLong waitTime = new AtomicLong(Long.MAX_VALUE); - final AtomicReference fatalException = new AtomicReference<>(); - - Thread waitingThread = new Thread(() -> { - long start = System.currentTimeMillis(); - isWaiting.set(true); - try { - flagdProviderSyncResources.waitForInitialization(10000); - } catch (Exception e) { - fatalException.set(e); - } - long end = System.currentTimeMillis(); - long duration = end - start; - waitTime.set(duration); - }); - waitingThread.start(); - - while (!isWaiting.get()) { - Thread.yield(); - } - - Thread.sleep(MAX_TIME_TOLERANCE); // waitingThread should have started waiting in the meantime - - var fatalEvent = ProviderEventDetails.builder() - .errorCode(ErrorCode.PROVIDER_FATAL) - .message("Some message") - .build(); - flagdProviderSyncResources.fatalError(fatalEvent); - - waitingThread.join(); - - var wait = MAX_TIME_TOLERANCE * 3; - - Assertions.assertTrue( - waitTime.get() < wait, - () -> "Wakeup should be almost instant, but took " + waitTime.get() - + " ms, which is more than the max of" - + wait + " ms"); - Assertions.assertNotNull(fatalException.get()); - Assertions.assertInstanceOf(FatalError.class, fatalException.get()); - Assertions.assertEquals( - "Initialization failed due to a fatal error: " + fatalEvent.getMessage(), - fatalException.get().getMessage()); - } -} diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java index ad38fe15d..cf6cba722 100644 --- a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdProviderTest.java @@ -20,12 +20,6 @@ import dev.openfeature.contrib.providers.flagd.resolver.Resolver; import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelConnector; import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver; -import dev.openfeature.contrib.providers.flagd.resolver.process.MockStorage; -import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; -import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; -import dev.openfeature.contrib.providers.flagd.resolver.rpc.RpcResolver; -import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.Cache; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanRequest; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveBooleanResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveFloatResponse; @@ -33,7 +27,6 @@ import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveObjectResponse; import dev.openfeature.flagd.grpc.evaluation.Evaluation.ResolveStringResponse; import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceBlockingStub; -import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc.ServiceStub; import dev.openfeature.sdk.ErrorCode; import dev.openfeature.sdk.EvaluationContext; import dev.openfeature.sdk.FlagEvaluationDetails; @@ -54,13 +47,11 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; @@ -156,7 +147,7 @@ void resolvers_call_grpc_service_and_return_details() { .thenReturn(objectResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertTrue(booleanDetails.getValue()); @@ -234,7 +225,7 @@ void zero_value() { ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); assertEquals(false, booleanDetails.getValue()); @@ -298,7 +289,7 @@ void test_metadata_from_grpc_response() { .thenReturn(booleanResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); // when FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY_BOOLEAN, false); @@ -380,7 +371,7 @@ void context_is_parsed_and_passed_to_grpc_service() { .thenReturn(booleanResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); final MutableContext context = new MutableContext("MY_TARGETING_KEY"); context.add(BOOLEAN_ATTR_KEY, BOOLEAN_ATTR_VALUE); @@ -419,7 +410,7 @@ void null_context_handling() { .build()); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); // then final Boolean evaluation = api.getClient().getBooleanValue(flagA, defaultVariant, context); @@ -443,7 +434,7 @@ void reason_mapped_correctly_if_unknown() { .thenReturn(badReasonResponse); ChannelConnector grpc = mock(ChannelConnector.class); - OpenFeatureAPI.getInstance().setProviderAndWait(createProvider(grpc, serviceBlockingStubMock)); + OpenFeatureAPI.getInstance().setProviderAndWait(FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock)); FlagEvaluationDetails booleanDetails = api.getClient().getBooleanDetails(FLAG_KEY, false, new MutableContext()); @@ -502,7 +493,7 @@ private void doResolversCacheResponses(String reason, Boolean eventStreamAlive, .thenReturn(objectResponse); ChannelConnector grpc = mock(ChannelConnector.class); - FlagdProvider provider = createProvider(grpc, serviceBlockingStubMock); + FlagdProvider provider = FlagdTestUtils.createProvider(grpc, serviceBlockingStubMock); // provider.setState(eventStreamAlive); // caching only available when event // stream is alive @@ -711,66 +702,4 @@ void initAfterFatalPropagatesErrorEvent() { Assertions.assertEquals(ErrorCode.PROVIDER_FATAL, error.getErrorCode()); } } - - // test helper - // create provider with given grpc provider and state supplier - private FlagdProvider createProvider(ChannelConnector connector, ServiceBlockingStub mockBlockingStub) { - final Cache cache = new Cache("lru", 5); - final ServiceStub mockStub = mock(ServiceStub.class); - - return createProvider(connector, cache, mockStub, mockBlockingStub); - } - - // create provider with given grpc provider, cache and state supplier - private FlagdProvider createProvider( - ChannelConnector connector, Cache cache, ServiceStub mockStub, ServiceBlockingStub mockBlockingStub) { - final FlagdOptions flagdOptions = FlagdOptions.builder().build(); - final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (event, details, metadata) -> {}); - - try { - Field resolver = RpcResolver.class.getDeclaredField("connector"); - resolver.setAccessible(true); - resolver.set(grpcResolver, connector); - - Field stub = RpcResolver.class.getDeclaredField("stub"); - stub.setAccessible(true); - stub.set(grpcResolver, mockStub); - - Field blockingStub = RpcResolver.class.getDeclaredField("blockingStub"); - blockingStub.setAccessible(true); - blockingStub.set(grpcResolver, mockBlockingStub); - - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - final FlagdProvider provider = new FlagdProvider(grpcResolver, true); - return provider; - } - - // Create an in process provider - private FlagdProvider createInProcessProvider() { - - final FlagdOptions flagdOptions = FlagdOptions.builder() - .resolverType(Config.Resolver.IN_PROCESS) - .deadline(1000) - .build(); - final FlagdProvider provider = new FlagdProvider(flagdOptions); - final MockStorage mockStorage = new MockStorage( - new HashMap(), - new LinkedBlockingQueue(Arrays.asList(new StorageStateChange(StorageState.OK)))); - - try { - final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); - flagResolver.setAccessible(true); - final Resolver resolver = (Resolver) flagResolver.get(provider); - - final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); - flagStore.setAccessible(true); - flagStore.set(resolver, mockStorage); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new RuntimeException(e); - } - - return provider; - } } diff --git a/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java new file mode 100644 index 000000000..752a29222 --- /dev/null +++ b/providers/flagd/src/test/java/dev/openfeature/contrib/providers/flagd/FlagdTestUtils.java @@ -0,0 +1,88 @@ +package dev.openfeature.contrib.providers.flagd; + +import static org.mockito.Mockito.mock; + +import dev.openfeature.contrib.providers.flagd.resolver.Resolver; +import dev.openfeature.contrib.providers.flagd.resolver.common.ChannelConnector; +import dev.openfeature.contrib.providers.flagd.resolver.process.InProcessResolver; +import dev.openfeature.contrib.providers.flagd.resolver.process.MockStorage; +import dev.openfeature.contrib.providers.flagd.resolver.process.model.FeatureFlag; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageState; +import dev.openfeature.contrib.providers.flagd.resolver.process.storage.StorageStateChange; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.RpcResolver; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.Cache; +import dev.openfeature.contrib.providers.flagd.resolver.rpc.cache.CacheType; +import dev.openfeature.flagd.grpc.evaluation.ServiceGrpc; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +class FlagdTestUtils { + // test helper + // create provider with given grpc provider and state supplier + static FlagdProvider createProvider(ChannelConnector connector, ServiceGrpc.ServiceBlockingStub mockBlockingStub) { + final Cache cache = new Cache(CacheType.LRU.getValue(), 5); + final ServiceGrpc.ServiceStub mockStub = mock(ServiceGrpc.ServiceStub.class); + + return createProvider(connector, cache, mockStub, mockBlockingStub); + } + + // create provider with given grpc provider, cache and state supplier + static FlagdProvider createProvider( + ChannelConnector connector, + Cache cache, + ServiceGrpc.ServiceStub mockStub, + ServiceGrpc.ServiceBlockingStub mockBlockingStub) { + final FlagdOptions flagdOptions = FlagdOptions.builder().build(); + final RpcResolver grpcResolver = new RpcResolver(flagdOptions, cache, (event, details, data) -> {}); + + try { + Field resolver = RpcResolver.class.getDeclaredField("connector"); + resolver.setAccessible(true); + resolver.set(grpcResolver, connector); + + Field stub = RpcResolver.class.getDeclaredField("stub"); + stub.setAccessible(true); + stub.set(grpcResolver, mockStub); + + Field blockingStub = RpcResolver.class.getDeclaredField("blockingStub"); + blockingStub.setAccessible(true); + blockingStub.set(grpcResolver, mockBlockingStub); + + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + return new FlagdProvider(grpcResolver, true); + } + + static FlagdProvider createInProcessProvider(Map mockFlags) { + final FlagdOptions flagdOptions = FlagdOptions.builder() + .resolverType(Config.Resolver.IN_PROCESS) + .offlineFlagSourcePath("") // this is new + .deadline(1000) + .build(); + final FlagdProvider provider = new FlagdProvider(flagdOptions); + final MockStorage mockStorage = + new MockStorage(mockFlags, new LinkedBlockingQueue<>(List.of(new StorageStateChange(StorageState.OK)))); + + try { + final Field flagResolver = FlagdProvider.class.getDeclaredField("flagResolver"); + flagResolver.setAccessible(true); + final Resolver resolver = (Resolver) flagResolver.get(provider); + + final Field flagStore = InProcessResolver.class.getDeclaredField("flagStore"); + flagStore.setAccessible(true); + flagStore.set(resolver, mockStorage); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + return provider; + } + + static FlagdProvider createInProcessProvider() { + return createInProcessProvider(Collections.emptyMap()); + } +}