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 18 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, or the SDK is shut down.
* 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
}

}
41 changes: 20 additions & 21 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
package dev.openfeature.sdk;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import javax.annotation.Nullable;

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

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
* 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 ProviderRepository providerRepository = new ProviderRepository();
private final List<Hook> apiHooks;
private FeatureProvider defaultProvider = new NoOpProvider();
private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();

private EvaluationContext evaluationContext;

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

/**
* Provisions the {@link OpenFeatureAPI} singleton (if needed) and returns it.
*
* @return The singleton instance.
*/
public static OpenFeatureAPI getInstance() {
return SingletonHolder.INSTANCE;
}

public Metadata getProviderMetadata() {
return defaultProvider.getMetadata();
return getProvider().getMetadata();
}

public Metadata getProviderMetadata(String clientName) {
Expand Down Expand Up @@ -79,41 +83,36 @@ public EvaluationContext getEvaluationContext() {
* Set the default provider.
*/
public void setProvider(FeatureProvider provider) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
defaultProvider = provider;
providerRepository.setProvider(provider);
}

/**
* Add a provider for a named client.
*
* @param clientName The name of the client.
* @param provider The provider to set.
* @param provider The provider to set.
*/
public void setProvider(String clientName, FeatureProvider provider) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
this.providers.put(clientName, provider);
providerRepository.setProvider(clientName, provider);
}

/**
* Return the default provider.
*/
public FeatureProvider getProvider() {
return defaultProvider;
return providerRepository.getProvider();
}

/**
* Fetch a provider for a named client. If not found, return the default.
*
* @param name The client name to look for.
* @return A named {@link FeatureProvider}
*/
public FeatureProvider getProvider(String name) {
return Optional.ofNullable(name).map(this.providers::get).orElse(defaultProvider);
return providerRepository.getProvider(name);
}


/**
* {@inheritDoc}
*/
Expand Down
156 changes: 156 additions & 0 deletions src/main/java/dev/openfeature/sdk/ProviderRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package dev.openfeature.sdk;

import lombok.extern.slf4j.Slf4j;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Stream;

@Slf4j
class ProviderRepository {

private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();
private final ExecutorService taskExecutor = Executors.newCachedThreadPool();
private final Map<String, FeatureProvider> initializingNamedProviders = new ConcurrentHashMap<>();
private final AtomicReference<FeatureProvider> defaultProvider = new AtomicReference<>(new NoOpProvider());
private FeatureProvider initializingDefaultProvider;

/**
* Return the default provider.
*/
public FeatureProvider getProvider() {
return defaultProvider.get();
}

/**
* Fetch a provider for a named client. If not found, return the default.
*
* @param name The client name to look for.
* @return A named {@link FeatureProvider}
*/
public FeatureProvider getProvider(String name) {
return Optional.ofNullable(name).map(this.providers::get).orElse(this.defaultProvider.get());
}

/**
* Set the default provider.
*/
public void setProvider(FeatureProvider provider) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
initializeProvider(provider);
}

/**
* Add a provider for a named client.
*
* @param clientName The name of the client.
* @param provider The provider to set.
*/
public void setProvider(String clientName, FeatureProvider provider) {
if (provider == null) {
throw new IllegalArgumentException("Provider cannot be null");
}
if (clientName == null) {
throw new IllegalArgumentException("clientName cannot be null");
}
initializeProvider(clientName, provider);
}

private void initializeProvider(FeatureProvider provider) {
initializingDefaultProvider = provider;
initializeProvider(provider, this::updateDefaultProviderAfterInitialization);
}

private void initializeProvider(String clientName, FeatureProvider provider) {
initializingNamedProviders.put(clientName, provider);
initializeProvider(provider, newProvider -> updateProviderAfterInit(clientName, newProvider));
}

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

private void updateProviderAfterInit(String clientName, FeatureProvider newProvider) {
Optional
.ofNullable(initializingNamedProviders.get(clientName))
.filter(initializingProvider -> initializingProvider.equals(newProvider))
.ifPresent(provider -> updateNamedProviderAfterInitialization(clientName, provider));
}

private void updateDefaultProviderAfterInitialization(FeatureProvider initializedProvider) {
Optional
.ofNullable(this.initializingDefaultProvider)
.filter(initializingProvider -> initializingProvider.equals(initializedProvider))
.ifPresent(this::replaceDefaultProvider);
}

private void replaceDefaultProvider(FeatureProvider provider) {
FeatureProvider oldProvider = this.defaultProvider.getAndSet(provider);
if (isOldProviderNotBoundByName(oldProvider)) {
shutdownProvider(oldProvider);
}
}

private boolean isOldProviderNotBoundByName(FeatureProvider oldProvider) {
return !this.providers.containsValue(oldProvider);
}

private void updateNamedProviderAfterInitialization(String clientName, FeatureProvider initializedProvider) {
Optional
.ofNullable(this.initializingNamedProviders.get(clientName))
Kavindu-Dodan marked this conversation as resolved.
Show resolved Hide resolved
.filter(initializingProvider -> initializingProvider.equals(initializedProvider))
.ifPresent(provider -> replaceNamedProviderAndShutdownOldOne(clientName, provider));
}

private void replaceNamedProviderAndShutdownOldOne(String clientName, FeatureProvider provider) {
FeatureProvider oldProvider = this.providers.put(clientName, provider);
this.initializingNamedProviders.remove(clientName, provider);
if (isProviderNotBoundMultipleTimes(oldProvider)) {
shutdownProvider(oldProvider);
}
}

private boolean isProviderNotBoundMultipleTimes(FeatureProvider oldProvider) {
lopitz marked this conversation as resolved.
Show resolved Hide resolved
return !(this.providers.containsValue(oldProvider) || this.defaultProvider.get().equals(oldProvider));
}

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

/**
* Shutdowns this repository which includes shutting down all FeatureProviders that are registered,
* including the default feature provider.
*/
public void shutdown() {
Stream
.concat(Stream.of(this.defaultProvider.get()), this.providers.values().stream())
.distinct()
.forEach(this::shutdownProvider);
setProvider(new NoOpProvider());
this.providers.clear();
taskExecutor.shutdown();
}
}
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
Loading