Skip to content

Commit 5f173ff

Browse files
lopitztoddbaert
andauthored
feat: add initialize and shutdown behavior (#456)
Signed-off-by: Lars Opitz <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent 24f0923 commit 5f173ff

16 files changed

+1032
-90
lines changed

pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@
145145
<version>0.5.10</version>
146146
<scope>test</scope>
147147
</dependency>
148+
149+
<dependency>
150+
<groupId>org.awaitility</groupId>
151+
<artifactId>awaitility</artifactId>
152+
<version>4.2.0</version>
153+
<scope>test</scope>
154+
</dependency>
148155
</dependencies>
149156

150157
<dependencyManagement>

src/main/java/dev/openfeature/sdk/FeatureProvider.java

+25
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,29 @@ default List<Hook> getProviderHooks() {
2222
ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx);
2323

2424
ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx);
25+
26+
/**
27+
* This method is called before a provider is used to evaluate flags. Providers can overwrite this method,
28+
* if they have special initialization needed prior being called for flag evaluation.
29+
* <p>
30+
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
31+
* caught and logged.
32+
* </p>
33+
*/
34+
default void initialize() {
35+
// Intentionally left blank
36+
}
37+
38+
/**
39+
* This method is called when a new provider is about to be used to evaluate flags, or the SDK is shut down.
40+
* Providers can overwrite this method, if they have special shutdown actions needed.
41+
* <p>
42+
* It is ok, if the method is expensive as it is executed in the background. All runtime exceptions will be
43+
* caught and logged.
44+
* </p>
45+
*/
46+
default void shutdown() {
47+
// Intentionally left blank
48+
}
49+
2550
}

src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java

+32-22
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,30 @@
11
package dev.openfeature.sdk;
22

3-
import java.util.*;
4-
import java.util.concurrent.ConcurrentHashMap;
5-
6-
import javax.annotation.Nullable;
7-
83
import dev.openfeature.sdk.internal.AutoCloseableLock;
94
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
5+
import lombok.extern.slf4j.Slf4j;
6+
7+
import javax.annotation.Nullable;
8+
import java.util.ArrayList;
9+
import java.util.Arrays;
10+
import java.util.List;
1011

1112
/**
1213
* A global singleton which holds base configuration for the OpenFeature library.
1314
* Configuration here will be shared across all {@link Client}s.
1415
*/
16+
@Slf4j
1517
public class OpenFeatureAPI {
1618
// package-private multi-read/single-write lock
1719
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
1820
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
19-
private EvaluationContext evaluationContext;
21+
2022
private final List<Hook> apiHooks;
21-
private FeatureProvider defaultProvider = new NoOpProvider();
22-
private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();
2323

24-
private OpenFeatureAPI() {
24+
private ProviderRepository providerRepository = new ProviderRepository();
25+
private EvaluationContext evaluationContext;
26+
27+
protected OpenFeatureAPI() {
2528
this.apiHooks = new ArrayList<>();
2629
}
2730

@@ -31,14 +34,15 @@ private static class SingletonHolder {
3134

3235
/**
3336
* Provisions the {@link OpenFeatureAPI} singleton (if needed) and returns it.
37+
*
3438
* @return The singleton instance.
3539
*/
3640
public static OpenFeatureAPI getInstance() {
3741
return SingletonHolder.INSTANCE;
3842
}
3943

4044
public Metadata getProviderMetadata() {
41-
return defaultProvider.getMetadata();
45+
return getProvider().getMetadata();
4246
}
4347

4448
public Metadata getProviderMetadata(String clientName) {
@@ -79,41 +83,36 @@ public EvaluationContext getEvaluationContext() {
7983
* Set the default provider.
8084
*/
8185
public void setProvider(FeatureProvider provider) {
82-
if (provider == null) {
83-
throw new IllegalArgumentException("Provider cannot be null");
84-
}
85-
defaultProvider = provider;
86+
providerRepository.setProvider(provider);
8687
}
8788

8889
/**
8990
* Add a provider for a named client.
91+
*
9092
* @param clientName The name of the client.
91-
* @param provider The provider to set.
93+
* @param provider The provider to set.
9294
*/
9395
public void setProvider(String clientName, FeatureProvider provider) {
94-
if (provider == null) {
95-
throw new IllegalArgumentException("Provider cannot be null");
96-
}
97-
this.providers.put(clientName, provider);
96+
providerRepository.setProvider(clientName, provider);
9897
}
9998

10099
/**
101100
* Return the default provider.
102101
*/
103102
public FeatureProvider getProvider() {
104-
return defaultProvider;
103+
return providerRepository.getProvider();
105104
}
106105

107106
/**
108107
* Fetch a provider for a named client. If not found, return the default.
108+
*
109109
* @param name The client name to look for.
110110
* @return A named {@link FeatureProvider}
111111
*/
112112
public FeatureProvider getProvider(String name) {
113-
return Optional.ofNullable(name).map(this.providers::get).orElse(defaultProvider);
113+
return providerRepository.getProvider(name);
114114
}
115115

116-
117116
/**
118117
* {@inheritDoc}
119118
*/
@@ -140,4 +139,15 @@ public void clearHooks() {
140139
this.apiHooks.clear();
141140
}
142141
}
142+
143+
public void shutdown() {
144+
providerRepository.shutdown();
145+
}
146+
147+
/**
148+
* This method is only here for testing as otherwise all tests after the API shutdown test would fail.
149+
*/
150+
final void resetProviderRepository() {
151+
providerRepository = new ProviderRepository();
152+
}
143153
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package dev.openfeature.sdk;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
import java.util.Map;
6+
import java.util.Optional;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
import java.util.concurrent.ExecutorService;
9+
import java.util.concurrent.Executors;
10+
import java.util.concurrent.atomic.AtomicReference;
11+
import java.util.function.Consumer;
12+
import java.util.stream.Stream;
13+
14+
@Slf4j
15+
class ProviderRepository {
16+
17+
private final Map<String, FeatureProvider> providers = new ConcurrentHashMap<>();
18+
private final ExecutorService taskExecutor = Executors.newCachedThreadPool();
19+
private final Map<String, FeatureProvider> initializingNamedProviders = new ConcurrentHashMap<>();
20+
private final AtomicReference<FeatureProvider> defaultProvider = new AtomicReference<>(new NoOpProvider());
21+
private FeatureProvider initializingDefaultProvider;
22+
23+
/**
24+
* Return the default provider.
25+
*/
26+
public FeatureProvider getProvider() {
27+
return defaultProvider.get();
28+
}
29+
30+
/**
31+
* Fetch a provider for a named client. If not found, return the default.
32+
*
33+
* @param name The client name to look for.
34+
* @return A named {@link FeatureProvider}
35+
*/
36+
public FeatureProvider getProvider(String name) {
37+
return Optional.ofNullable(name).map(this.providers::get).orElse(this.defaultProvider.get());
38+
}
39+
40+
/**
41+
* Set the default provider.
42+
*/
43+
public void setProvider(FeatureProvider provider) {
44+
if (provider == null) {
45+
throw new IllegalArgumentException("Provider cannot be null");
46+
}
47+
initializeProvider(provider);
48+
}
49+
50+
/**
51+
* Add a provider for a named client.
52+
*
53+
* @param clientName The name of the client.
54+
* @param provider The provider to set.
55+
*/
56+
public void setProvider(String clientName, FeatureProvider provider) {
57+
if (provider == null) {
58+
throw new IllegalArgumentException("Provider cannot be null");
59+
}
60+
if (clientName == null) {
61+
throw new IllegalArgumentException("clientName cannot be null");
62+
}
63+
initializeProvider(clientName, provider);
64+
}
65+
66+
private void initializeProvider(FeatureProvider provider) {
67+
initializingDefaultProvider = provider;
68+
initializeProvider(provider, this::updateDefaultProviderAfterInitialization);
69+
}
70+
71+
private void initializeProvider(String clientName, FeatureProvider provider) {
72+
initializingNamedProviders.put(clientName, provider);
73+
initializeProvider(provider, newProvider -> updateProviderAfterInit(clientName, newProvider));
74+
}
75+
76+
private void initializeProvider(FeatureProvider provider, Consumer<FeatureProvider> afterInitialization) {
77+
taskExecutor.submit(() -> {
78+
try {
79+
if (!isProviderRegistered(provider)) {
80+
provider.initialize();
81+
}
82+
afterInitialization.accept(provider);
83+
} catch (Exception e) {
84+
log.error("Exception when initializing feature provider {}", provider.getClass().getName(), e);
85+
}
86+
});
87+
}
88+
89+
private void updateProviderAfterInit(String clientName, FeatureProvider newProvider) {
90+
Optional
91+
.ofNullable(initializingNamedProviders.get(clientName))
92+
.filter(initializingProvider -> initializingProvider.equals(newProvider))
93+
.ifPresent(provider -> updateNamedProviderAfterInitialization(clientName, provider));
94+
}
95+
96+
private void updateDefaultProviderAfterInitialization(FeatureProvider initializedProvider) {
97+
Optional
98+
.ofNullable(this.initializingDefaultProvider)
99+
.filter(initializingProvider -> initializingProvider.equals(initializedProvider))
100+
.ifPresent(this::replaceDefaultProvider);
101+
}
102+
103+
private void replaceDefaultProvider(FeatureProvider provider) {
104+
FeatureProvider oldProvider = this.defaultProvider.getAndSet(provider);
105+
if (isOldProviderNotBoundByName(oldProvider)) {
106+
shutdownProvider(oldProvider);
107+
}
108+
}
109+
110+
private boolean isOldProviderNotBoundByName(FeatureProvider oldProvider) {
111+
return !this.providers.containsValue(oldProvider);
112+
}
113+
114+
private void updateNamedProviderAfterInitialization(String clientName, FeatureProvider initializedProvider) {
115+
Optional
116+
.ofNullable(this.initializingNamedProviders.get(clientName))
117+
.filter(initializingProvider -> initializingProvider.equals(initializedProvider))
118+
.ifPresent(provider -> replaceNamedProviderAndShutdownOldOne(clientName, provider));
119+
}
120+
121+
private void replaceNamedProviderAndShutdownOldOne(String clientName, FeatureProvider provider) {
122+
FeatureProvider oldProvider = this.providers.put(clientName, provider);
123+
this.initializingNamedProviders.remove(clientName, provider);
124+
if (!isProviderRegistered(oldProvider)) {
125+
shutdownProvider(oldProvider);
126+
}
127+
}
128+
129+
private boolean isProviderRegistered(FeatureProvider oldProvider) {
130+
return this.providers.containsValue(oldProvider) || this.defaultProvider.get().equals(oldProvider);
131+
}
132+
133+
private void shutdownProvider(FeatureProvider provider) {
134+
taskExecutor.submit(() -> {
135+
try {
136+
provider.shutdown();
137+
} catch (Exception e) {
138+
log.error("Exception when shutting down feature provider {}", provider.getClass().getName(), e);
139+
}
140+
});
141+
}
142+
143+
/**
144+
* Shutdowns this repository which includes shutting down all FeatureProviders that are registered,
145+
* including the default feature provider.
146+
*/
147+
public void shutdown() {
148+
Stream
149+
.concat(Stream.of(this.defaultProvider.get()), this.providers.values().stream())
150+
.distinct()
151+
.forEach(this::shutdownProvider);
152+
setProvider(new NoOpProvider());
153+
this.providers.clear();
154+
taskExecutor.shutdown();
155+
}
156+
}

src/test/java/dev/openfeature/sdk/ClientProviderMappingTest.java

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package dev.openfeature.sdk;
22

3-
import io.cucumber.java.eo.Do;
3+
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
44
import org.junit.jupiter.api.Test;
55

6-
import static org.junit.jupiter.api.Assertions.assertFalse;
7-
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
import static org.junit.jupiter.api.Assertions.*;
7+
8+
class ClientProviderMappingTest {
89

9-
public class ClientProviderMappingTest {
1010
@Test
1111
void clientProviderTest() {
1212
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
1313

14-
api.setProvider("client1", new DoSomethingProvider());
15-
api.setProvider("client2", new NoOpProvider());
14+
FeatureProviderTestUtils.setFeatureProvider("client1", new DoSomethingProvider());
15+
FeatureProviderTestUtils.setFeatureProvider("client2", new NoOpProvider());
1616

1717
Client c1 = api.getClient("client1");
1818
Client c2 = api.getClient("client2");

src/test/java/dev/openfeature/sdk/DeveloperExperienceTest.java

+5-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Map;
1313
import java.util.Optional;
1414

15+
import dev.openfeature.sdk.testutils.FeatureProviderTestUtils;
1516
import org.junit.jupiter.api.Test;
1617

1718
import dev.openfeature.sdk.fixtures.HookFixtures;
@@ -77,7 +78,7 @@ class DeveloperExperienceTest implements HookFixtures {
7778

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

8990
@Test
90-
void providerLockedPerTransaction() throws InterruptedException {
91+
void providerLockedPerTransaction() {
9192

9293
class MutatingHook implements Hook {
9394

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

102103
final String defaultValue = "string-value";
103104
final OpenFeatureAPI api = OpenFeatureAPI.getInstance();
104105
final Client client = api.getClient();
105-
api.setProvider(new DoSomethingProvider());
106+
FeatureProviderTestUtils.setFeatureProvider(new DoSomethingProvider());
106107
api.addHooks(new MutatingHook());
107108

108109
// if provider is changed during an evaluation transaction it should proceed with the original provider

0 commit comments

Comments
 (0)