Skip to content

Commit 80e2a96

Browse files
committed
fixup: weakmap and throw
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
1 parent cfb2a51 commit 80e2a96

4 files changed

Lines changed: 59 additions & 82 deletions

File tree

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import java.util.ArrayList;
77
import java.util.Arrays;
88
import java.util.Collection;
9+
import java.util.Collections;
910
import java.util.List;
11+
import java.util.Map;
1012
import java.util.Optional;
1113
import java.util.Set;
12-
import java.util.concurrent.ConcurrentHashMap;
14+
import java.util.WeakHashMap;
1315
import java.util.concurrent.ConcurrentLinkedQueue;
1416
import java.util.concurrent.atomic.AtomicReference;
1517
import java.util.function.Consumer;
@@ -38,9 +40,12 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
3840
* Global registry tracking which API instance each provider is currently bound to.
3941
* Used to detect violations of spec requirement 1.8.4 (a provider SHOULD NOT be
4042
* registered with more than one API instance simultaneously).
43+
*
44+
* <p>Backed by a {@link WeakHashMap} so providers that are discarded without explicit
45+
* deregistration do not pin the API instance in memory.
4146
*/
42-
private static final ConcurrentHashMap<FeatureProvider, OpenFeatureAPI> GLOBAL_PROVIDER_REGISTRY =
43-
new ConcurrentHashMap<>();
47+
private static final Map<FeatureProvider, OpenFeatureAPI> GLOBAL_PROVIDER_REGISTRY =
48+
Collections.synchronizedMap(new WeakHashMap<>());
4449

4550
// package-private multi-read/single-write lock (instance-level for isolation)
4651
final AutoCloseableReentrantReadWriteLock lock;
@@ -373,20 +378,23 @@ public void clearHooks() {
373378
}
374379

375380
/**
376-
* Registers a provider with the global registry, warning if it is already
381+
* Registers a provider with the global registry, throwing if it is already
377382
* bound to a different API instance (spec requirement 1.8.4).
383+
*
384+
* @throws IllegalStateException if the provider is already bound to a different API instance
378385
*/
379386
void registerGlobalProvider(FeatureProvider provider) {
380-
GLOBAL_PROVIDER_REGISTRY.compute(provider, (p, existing) -> {
387+
synchronized (GLOBAL_PROVIDER_REGISTRY) {
388+
OpenFeatureAPI existing = GLOBAL_PROVIDER_REGISTRY.get(provider);
381389
if (existing != null && existing != this) {
382-
log.warn("Provider "
390+
throw new IllegalStateException("Provider "
383391
+ provider.getClass().getName()
384392
+ " is already registered with another API instance. "
385393
+ "A provider SHOULD NOT be bound to more than one API instance "
386394
+ "simultaneously (spec requirement 1.8.4).");
387395
}
388-
return this;
389-
});
396+
GLOBAL_PROVIDER_REGISTRY.put(provider, this);
397+
}
390398
}
391399

392400
/**

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,10 @@ private void prepareAndInitializeProvider(
170170
}
171171
FeatureProviderStateManager existing = getExistingStateManagerForProvider(newProvider);
172172
if (existing == null) {
173+
openFeatureAPI.registerGlobalProvider(newProvider);
173174
newStateManager = new FeatureProviderStateManager(newProvider);
174175
// only run afterSet if new provider is not already attached
175176
afterSet.accept(newProvider);
176-
// spec 1.8.4: warn if this provider is already bound to another API instance
177-
openFeatureAPI.registerGlobalProvider(newProvider);
178177
} else {
179178
newStateManager = existing;
180179
}

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

Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,46 @@
11
package dev.openfeature.sdk;
22

33
import static org.assertj.core.api.Assertions.assertThatCode;
4-
import static org.mockito.ArgumentMatchers.contains;
5-
import static org.mockito.Mockito.never;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
65

76
import org.junit.jupiter.api.DisplayName;
87
import org.junit.jupiter.api.Test;
9-
import org.mockito.Mockito;
10-
import org.simplify4u.slf4jmock.LoggerMock;
11-
import org.slf4j.Logger;
128

139
class GlobalProviderRegistryTest {
1410

1511
/**
16-
* Re-registering a provider with the same API instance should not produce a warning.
12+
* Re-registering a provider with the same API instance should not throw.
1713
* This exercises the {@code existing == this} path in registerGlobalProvider.
1814
*/
1915
@Test
20-
@DisplayName("no warning when same API instance re-registers the same provider")
21-
void noWarningWhenSameInstanceReRegisters() {
22-
Logger mockLogger = Mockito.mock(Logger.class);
23-
LoggerMock.setMock(OpenFeatureAPI.class, mockLogger);
24-
try {
25-
OpenFeatureAPI api = new OpenFeatureAPI();
26-
NoOpProvider provider = new NoOpProvider();
27-
28-
api.registerGlobalProvider(provider);
29-
api.registerGlobalProvider(provider); // same instance, second call
16+
@DisplayName("no throw when same API instance re-registers the same provider")
17+
void noThrowWhenSameInstanceReRegisters() {
18+
OpenFeatureAPI api = new OpenFeatureAPI();
19+
NoOpProvider provider = new NoOpProvider();
3020

31-
Mockito.verify(mockLogger, never()).warn(Mockito.anyString());
32-
} finally {
33-
LoggerMock.setMock(OpenFeatureAPI.class, null);
34-
}
21+
assertThatCode(() -> {
22+
api.registerGlobalProvider(provider);
23+
api.registerGlobalProvider(provider); // same instance, second call
24+
})
25+
.doesNotThrowAnyException();
3526
}
3627

3728
/**
3829
* After deregistering a provider, binding it to a different API instance
39-
* should not produce a warning — proving the entry was removed.
30+
* should not throw, proving the entry was removed.
4031
*/
4132
@Test
4233
@DisplayName("deregister removes provider from global registry")
4334
void deregisterRemovesProviderFromRegistry() {
44-
Logger mockLogger = Mockito.mock(Logger.class);
45-
LoggerMock.setMock(OpenFeatureAPI.class, mockLogger);
46-
try {
47-
OpenFeatureAPI api1 = new OpenFeatureAPI();
48-
OpenFeatureAPI api2 = new OpenFeatureAPI();
49-
NoOpProvider provider = new NoOpProvider();
50-
51-
api1.registerGlobalProvider(provider);
52-
api1.deregisterGlobalProvider(provider);
35+
OpenFeatureAPI api1 = new OpenFeatureAPI();
36+
OpenFeatureAPI api2 = new OpenFeatureAPI();
37+
NoOpProvider provider = new NoOpProvider();
5338

54-
// Should not warn because the provider was deregistered
55-
api2.registerGlobalProvider(provider);
39+
api1.registerGlobalProvider(provider);
40+
api1.deregisterGlobalProvider(provider);
5641

57-
Mockito.verify(mockLogger, never()).warn(Mockito.anyString());
58-
} finally {
59-
LoggerMock.setMock(OpenFeatureAPI.class, null);
60-
}
42+
// should not throw because the provider was deregistered
43+
assertThatCode(() -> api2.registerGlobalProvider(provider)).doesNotThrowAnyException();
6144
}
6245

6346
/**
@@ -67,24 +50,19 @@ void deregisterRemovesProviderFromRegistry() {
6750
@Test
6851
@DisplayName("deregister is a no-op when called by non-owner instance")
6952
void deregisterIsNoOpForNonOwner() {
70-
Logger mockLogger = Mockito.mock(Logger.class);
71-
LoggerMock.setMock(OpenFeatureAPI.class, mockLogger);
72-
try {
73-
OpenFeatureAPI api1 = new OpenFeatureAPI();
74-
OpenFeatureAPI api2 = new OpenFeatureAPI();
75-
NoOpProvider provider = new NoOpProvider();
53+
OpenFeatureAPI api1 = new OpenFeatureAPI();
54+
OpenFeatureAPI api2 = new OpenFeatureAPI();
55+
NoOpProvider provider = new NoOpProvider();
7656

77-
api1.registerGlobalProvider(provider);
57+
api1.registerGlobalProvider(provider);
7858

79-
// api2 is not the owner this should be a no-op
80-
api2.deregisterGlobalProvider(provider);
59+
// api2 is not the owner; this should be a no-op
60+
api2.deregisterGlobalProvider(provider);
8161

82-
// api2 re-registering should still warn, because api1 still owns it
83-
api2.registerGlobalProvider(provider);
84-
Mockito.verify(mockLogger).warn(contains("1.8.4"));
85-
} finally {
86-
LoggerMock.setMock(OpenFeatureAPI.class, null);
87-
}
62+
// api2 re-registering should still throw, because api1 still owns it
63+
assertThatThrownBy(() -> api2.registerGlobalProvider(provider))
64+
.isInstanceOf(IllegalStateException.class)
65+
.hasMessageContaining("1.8.4");
8866
}
8967

9068
/**

src/test/java/dev/openfeature/sdk/isolated/IsolatedAPITest.java

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package dev.openfeature.sdk.isolated;
22

33
import static org.assertj.core.api.Assertions.assertThat;
4-
import static org.mockito.ArgumentMatchers.contains;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
55

66
import dev.openfeature.sdk.ImmutableContext;
77
import dev.openfeature.sdk.NoOpProvider;
@@ -17,9 +17,6 @@
1717
import org.junit.jupiter.api.DisplayName;
1818
import org.junit.jupiter.api.Test;
1919
import org.junit.jupiter.api.Timeout;
20-
import org.mockito.Mockito;
21-
import org.simplify4u.slf4jmock.LoggerMock;
22-
import org.slf4j.Logger;
2320

2421
class IsolatedAPITest {
2522

@@ -211,25 +208,20 @@ void clientUsesItsOwnInstanceProvider() throws Exception {
211208
}
212209

213210
/**
214-
* Requirement 1.8.4 — a warning is logged when the same provider instance
211+
* Requirement 1.8.4 — an exception is thrown when the same provider instance
215212
* is registered with more than one API instance simultaneously.
216213
*/
217214
@Test
218-
@DisplayName("warn when same provider bound to multiple API instances (req 1.8.4)")
219-
void warnWhenProviderBoundToMultipleInstances() {
220-
Logger mockLogger = Mockito.mock(Logger.class);
221-
LoggerMock.setMock(OpenFeatureAPI.class, mockLogger);
222-
try {
223-
OpenFeatureAPI api1 = OpenFeatureAPI.createIsolated();
224-
OpenFeatureAPI api2 = OpenFeatureAPI.createIsolated();
225-
226-
NoOpProvider provider = new NoOpProvider();
227-
api1.setProvider(provider);
228-
api2.setProvider(provider);
229-
230-
Mockito.verify(mockLogger).warn(contains("1.8.4"));
231-
} finally {
232-
LoggerMock.setMock(OpenFeatureAPI.class, null);
233-
}
215+
@DisplayName("throw when same provider bound to multiple API instances (req 1.8.4)")
216+
void throwWhenProviderBoundToMultipleInstances() {
217+
OpenFeatureAPI api1 = OpenFeatureAPI.createIsolated();
218+
OpenFeatureAPI api2 = OpenFeatureAPI.createIsolated();
219+
220+
NoOpProvider provider = new NoOpProvider();
221+
api1.setProvider(provider);
222+
223+
assertThatThrownBy(() -> api2.setProvider(provider))
224+
.isInstanceOf(IllegalStateException.class)
225+
.hasMessageContaining("1.8.4");
234226
}
235227
}

0 commit comments

Comments
 (0)