Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add initialize and shutdown behavior #456

Merged
merged 21 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5125e03
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 27, 2023
1c3ea04
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 28, 2023
56b4f02
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 28, 2023
11fc987
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 28, 2023
f490efe
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 28, 2023
2581635
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 28, 2023
c4631e3
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 31, 2023
b324e10
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 31, 2023
ce84058
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz May 31, 2023
8ca85c8
chore(deps): update github/codeql-action digest to 9d2dd7c (#457)
renovate[bot] May 31, 2023
a3b6b1f
Merge branch 'main' into java-sdk-449
lopitz May 31, 2023
4d50752
Merge branch 'main' into java-sdk-449
lopitz Jun 1, 2023
a313750
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz Jun 1, 2023
9475cac
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz Jun 1, 2023
d0fa55d
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz Jun 2, 2023
75491eb
Merge branch 'main' into java-sdk-449
lopitz Jun 2, 2023
076d970
Merge branch 'main' into java-sdk-449
lopitz Jun 3, 2023
a650655
java-sdk-449 Implement Initialize/Shutdown on provider registration
lopitz Jun 3, 2023
c09b814
Merge branch 'main' into java-sdk-449
lopitz Jun 6, 2023
4437b89
- addresses review comments
lopitz Jun 6, 2023
24b7e98
Merge branch 'main' into java-sdk-449
toddbaert Jun 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,13 @@
<version>0.5.10</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/dev/openfeature/sdk/FeatureProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,29 @@ default List<Hook> getProviderHooks() {
ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx);

ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx);

/**
* This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
* if they have special initialization needed prior being called for flag evaluation.
* <p>
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
* caught and logged.
* </p>
*/
default void initialize() {
// Intentionally left blank
}

/**
* This method is called when a new provider is about to be used to evaluate flags.
lopitz marked this conversation as resolved.
Show resolved Hide resolved
* Providers can overwrite this method, if they have special shutdown actions needed.
* <p>
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
* caught and logged.
* </p>
*/
default void shutdown() {
// Intentionally left blank
}

}
71 changes: 63 additions & 8 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
package dev.openfeature.sdk;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

import javax.annotation.Nullable;

import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import dev.openfeature.sdk.internal.*;
import lombok.extern.slf4j.Slf4j;

/**
* A global singleton which holds base configuration for the OpenFeature library.
* Configuration here will be shared across all {@link Client}s.
*/
@Slf4j
public class OpenFeatureAPI {
// package-private multi-read/single-write lock
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
private EvaluationContext evaluationContext;

private final List<Hook> apiHooks;
private FeatureProvider defaultProvider = new NoOpProvider();
private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();
private final ExecutorService taskExecutor = Executors.newCachedThreadPool();
private final AtomicReference<FeatureProvider> initializingDefaultProvider = new AtomicReference<>();
private final Map<String, FeatureProvider> initializingNamedProviders = new ConcurrentHashMap<>();
lopitz marked this conversation as resolved.
Show resolved Hide resolved

private FeatureProvider defaultProvider = new NoOpProvider();
private EvaluationContext evaluationContext;

private OpenFeatureAPI() {
this.apiHooks = new ArrayList<>();
Expand All @@ -31,6 +39,7 @@ private static class SingletonHolder {

/**
* Provisions the {@link OpenFeatureAPI} singleton (if needed) and returns it.
*
* @return The singleton instance.
*/
public static OpenFeatureAPI getInstance() {
Expand Down Expand Up @@ -82,7 +91,8 @@ public void setProvider(FeatureProvider provider) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
defaultProvider = provider;
shutdownProvider(this.defaultProvider);
initializeProvider(provider);
toddbaert marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -94,7 +104,53 @@ public void setProvider(String clientName, FeatureProvider provider) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
this.providers.put(clientName, provider);
shutdownProvider(clientName);
initializeProvider(clientName, provider);
}

private void shutdownProvider(FeatureProvider provider) {
if (provider == null) {
return;
}
taskExecutor.submit(() -> {
try {
provider.shutdown();
} catch (Exception e) {
log.error("Exception when shutting down feature provider {}", provider.getClass().getName(), e);
}
});
}

private void shutdownProvider(String clientName) {
shutdownProvider(providers.get(clientName));
}

private void initializeProvider(FeatureProvider provider) {
initializingDefaultProvider.set(provider);
initializeProvider(provider,
newProvider -> Optional
.ofNullable(initializingDefaultProvider.get())
.filter(initializingProvider -> initializingProvider == newProvider)
.ifPresent(initializedProvider -> defaultProvider = initializedProvider));
}

private void initializeProvider(String clientName, FeatureProvider provider) {
initializingNamedProviders.put(clientName, provider);
initializeProvider(provider, newProvider -> Optional
.ofNullable(initializingNamedProviders.get(clientName))
.filter(initializingProvider -> initializingProvider == newProvider)
.ifPresent(initializedProvider -> this.providers.put(clientName, initializedProvider)));
}

private void initializeProvider(FeatureProvider provider, Consumer<FeatureProvider> afterInitialization) {
taskExecutor.submit(() -> {
try {
provider.initialize();
afterInitialization.accept(provider);
} catch (Exception e) {
log.error("Exception when initializing feature provider {}", provider.getClass().getName(), e);
}
});
}

/**
Expand All @@ -113,7 +169,6 @@ public FeatureProvider getProvider(String name) {
return Optional.ofNullable(name).map(this.providers::get).orElse(defaultProvider);
}


/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package dev.openfeature.sdk;

import io.cucumber.java.eo.Do;
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;

class ClientProviderMappingTest {

public class ClientProviderMappingTest {
@Test
void clientProviderTest() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();

api.setProvider("client1", new DoSomethingProvider());
api.setProvider("client2", new NoOpProvider());
FeatureProviderTestUtils.setFeatureProvider("client1", new DoSomethingProvider());
FeatureProviderTestUtils.setFeatureProvider("client2", new NoOpProvider());

Client c1 = api.getClient("client1");
Client c2 = api.getClient("client2");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Map;
import java.util.Optional;

import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
import org.junit.jupiter.api.Test;

import dev.openfeature.sdk.fixtures.HookFixtures;
Expand Down Expand Up @@ -77,7 +78,7 @@ class DeveloperExperienceTest implements HookFixtures {

@Test void brokenProvider() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new AlwaysBrokenProvider());
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client client = api.getClient();
FlagEvaluationDetails<Boolean> retval = client.getBooleanDetails(flagKey, false);
assertEquals(ErrorCode.FLAG_NOT_FOUND, retval.getErrorCode());
Expand All @@ -87,22 +88,22 @@ class DeveloperExperienceTest implements HookFixtures {
}

@Test
void providerLockedPerTransaction() throws InterruptedException {
void providerLockedPerTransaction() {

class MutatingHook implements Hook {

@Override
// change the provider during a before hook - this should not impact the evaluation in progress
public Optional before(HookContext ctx, Map hints) {
OpenFeatureAPI.getInstance().setProvider(new NoOpProvider());
FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider());
return Optional.empty();
}
}

final String defaultValue = "string-value";
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
final Client client = api.getClient();
api.setProvider(new DoSomethingProvider());
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
api.addHooks(new MutatingHook());

// if provider is changed during an evaluation transaction it should proceed with the original provider
Expand Down
53 changes: 22 additions & 31 deletions src/test/java/dev/openfeature/sdk/FlagEvaluationSpecTest.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.openfeature.sdk;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.optional;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
Expand All @@ -12,14 +11,12 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


import dev.openfeature.sdk.exceptions.FlagNotFoundError;
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -33,15 +30,19 @@
class FlagEvaluationSpecTest implements HookFixtures {

private Logger logger;
private OpenFeatureAPI api;

private Client _client() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new NoOpProvider());
FeatureProviderTestUtils.setFeatureProvider(new NoOpProvider());
return api.getClient();
}

@BeforeEach
void getApiInstance() {
api = OpenFeatureAPI.getInstance();
}

@AfterEach void reset_ctx() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setEvaluationContext(null);
}

Expand All @@ -61,24 +62,21 @@ private Client _client() {

@Specification(number="1.1.2", text="The API MUST provide a function to set the global provider singleton, which accepts an API-conformant provider implementation.")
@Test void provider() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
FeatureProvider mockProvider = mock(FeatureProvider.class);
api.setProvider(mockProvider);
assertEquals(mockProvider, api.getProvider());
FeatureProviderTestUtils.setFeatureProvider(mockProvider);
assertThat(api.getProvider()).isEqualTo(mockProvider);
}

@Specification(number="1.1.4", text="The API MUST provide a function for retrieving the metadata field of the configured provider.")
@Test void provider_metadata() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new DoSomethingProvider());
assertEquals(DoSomethingProvider.name, api.getProviderMetadata().getName());
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
assertThat(api.getProviderMetadata().getName()).isEqualTo(DoSomethingProvider.name);
}

@Specification(number="1.1.3", text="The API MUST provide a function to add hooks which accepts one or more API-conformant hooks, and appends them to the collection of any previously added hooks. When new hooks are added, previously added hooks are not removed.")
@Test void hook_addition() {
Hook h1 = mock(Hook.class);
Hook h2 = mock(Hook.class);
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.addHooks(h1);

assertEquals(1, api.getHooks().size());
Expand All @@ -91,8 +89,7 @@ private Client _client() {

@Specification(number="1.1.5", text="The API MUST provide a function for creating a client which accepts the following options: - name (optional): A logical string identifier for the client.")
@Test void namedClient() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
Client c = api.getClient("Sir Calls-a-lot");
assertThatCode(() -> api.getClient("Sir Calls-a-lot")).doesNotThrowAnyException();
// TODO: Doesn't say that you can *get* the client name.. which seems useful?
}

Expand All @@ -112,8 +109,8 @@ private Client _client() {
@Specification(number="1.3.1", text="The client MUST provide methods for typed flag evaluation, including boolean, numeric, string, and structure, with parameters flag key (string, required), default value (boolean | number | string | structure, required), evaluation context (optional), and evaluation options (optional), which returns the flag value.")
@Specification(number="1.3.2.1", text="The client SHOULD provide functions for floating-point numbers and integers, consistent with language idioms.")
@Test void value_flags() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new DoSomethingProvider());
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());

Client c = api.getClient();
String key = "key";

Expand Down Expand Up @@ -145,8 +142,7 @@ private Client _client() {
@Specification(number="1.4.5", text="In cases of normal execution, the evaluation details structure's variant field MUST contain the value of the variant field in the flag resolution structure returned by the configured provider, if the field is set.")
@Specification(number="1.4.6", text="In cases of normal execution, the evaluation details structure's reason field MUST contain the value of the reason field in the flag resolution structure returned by the configured provider, if the field is set.")
@Test void detail_flags() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new DoSomethingProvider());
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
Client c = api.getClient();
String key = "key";

Expand Down Expand Up @@ -204,8 +200,7 @@ private Client _client() {
@Specification(number="1.4.7", text="In cases of abnormal execution, the `evaluation details` structure's `error code` field **MUST** contain an `error code`.")
@Specification(number="1.4.12", text="In cases of abnormal execution, the `evaluation details` structure's `error message` field **MAY** contain a string containing additional details about the nature of the error.")
@Test void broken_provider() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new AlwaysBrokenProvider());
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
assertFalse(c.getBooleanValue("key", false));
FlagEvaluationDetails<Boolean> details = c.getBooleanDetails("key", false);
Expand All @@ -215,8 +210,7 @@ private Client _client() {

@Specification(number="1.4.10", text="In the case of abnormal execution, the client SHOULD log an informative error message.")
@Test void log_on_error() throws NotImplementedException {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new AlwaysBrokenProvider());
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);

Expand All @@ -232,16 +226,14 @@ private Client _client() {
Client c = _client();
assertNull(c.getMetadata().getName());

OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new AlwaysBrokenProvider());
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c2 = api.getClient("test");
assertEquals("test", c2.getMetadata().getName());
}

@Specification(number="1.4.8", text="In cases of abnormal execution (network failure, unhandled error, etc) the reason field in the evaluation details SHOULD indicate an error.")
@Test void reason_is_error_when_there_are_errors() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
api.setProvider(new AlwaysBrokenProvider());
FeatureProviderTestUtils.setFeatureProvider(new AlwaysBrokenProvider());
Client c = api.getClient();
FlagEvaluationDetails<Boolean> result = c.getBooleanDetails("test", false);
assertEquals(Reason.ERROR.toString(), result.getReason());
Expand All @@ -250,9 +242,8 @@ private Client _client() {
@Specification(number="3.2.1", text="The API, Client and invocation MUST have a method for supplying evaluation context.")
@Specification(number="3.2.2", text="Evaluation context MUST be merged in the order: API (global; lowest precedence) - client - invocation - before hooks (highest precedence), with duplicate values being overwritten.")
@Test void multi_layer_context_merges_correctly() {
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
DoSomethingProvider provider = new DoSomethingProvider();
api.setProvider(provider);
FeatureProviderTestUtils.setFeatureProvider(provider);

Map<String, Value> attributes = new HashMap<>();
attributes.put("common", new Value("1"));
Expand Down
Loading