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());
+ }
+}