Skip to content

Commit 2192932

Browse files
authored
feat!: add rw locks to client/api, hook accessor name (#131)
* fix: add read/write locks to client/api Signed-off-by: Todd Baert <[email protected]> * dont lock entire evaluation Signed-off-by: Todd Baert <[email protected]> * add tests Signed-off-by: Todd Baert <[email protected]> * fixup comment Signed-off-by: Todd Baert <[email protected]> * fixup pom comment Signed-off-by: Todd Baert <[email protected]> * increase lock granularity, imporove tests Signed-off-by: Todd Baert <[email protected]> * fix spotbugs Signed-off-by: Todd Baert <[email protected]> * remove commented test Signed-off-by: Todd Baert <[email protected]> Signed-off-by: Todd Baert <[email protected]>
1 parent 71a5699 commit 2192932

12 files changed

+379
-49
lines changed

Diff for: pom.xml

+18
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@
164164
<build>
165165
<plugins>
166166

167+
<plugin>
168+
<groupId>org.codehaus.mojo</groupId>
169+
<artifactId>build-helper-maven-plugin</artifactId>
170+
<version>3.3.0</version>
171+
<executions>
172+
<execution>
173+
<phase>validate</phase>
174+
<id>get-cpu-count</id>
175+
<goals>
176+
<goal>cpu-count</goal>
177+
</goals>
178+
</execution>
179+
</executions>
180+
</plugin>
181+
167182
<plugin>
168183
<groupId>org.cyclonedx</groupId>
169184
<artifactId>cyclonedx-maven-plugin</artifactId>
@@ -231,6 +246,9 @@
231246
<argLine>
232247
${surefireArgLine}
233248
</argLine>
249+
<!-- fork a new JVM to isolate test suites, especially important with singletons -->
250+
<forkCount>${cpu.count}</forkCount>
251+
<reuseForks>false</reuseForks>
234252
<excludes>
235253
<!-- tests to exclude -->
236254
<exclude>${testExclusions}</exclude>

Diff for: spotbugs-exclusions.xml

+16-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,26 @@
99
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
1010
<Bug pattern="MS_EXPOSE_REP"/>
1111
</And>
12-
<!-- similarly, client using the singleton doesn't seem bad -->
12+
<!-- evaluation context and hooks are mutable if mutable impl is used -->
13+
<And>
14+
<Class name="dev.openfeature.sdk.OpenFeatureClient"/>
15+
<Bug pattern="EI_EXPOSE_REP"/>
16+
</And>
1317
<And>
1418
<Class name="dev.openfeature.sdk.OpenFeatureClient"/>
1519
<Bug pattern="EI_EXPOSE_REP2"/>
1620
</And>
21+
<And>
22+
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
23+
<Bug pattern="EI_EXPOSE_REP"/>
24+
</And>
25+
<And>
26+
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
27+
<Bug pattern="EI_EXPOSE_REP2"/>
28+
</And>
29+
30+
31+
1732

1833
<!-- Test class that should be excluded -->
1934
<Match>

Diff for: src/main/java/dev/openfeature/sdk/Client.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ public interface Client extends Features {
3232
* Fetch the hooks associated to this client.
3333
* @return A list of {@link Hook}s.
3434
*/
35-
List<Hook> getClientHooks();
35+
List<Hook> getHooks();
3636
}
+72-19
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,41 @@
11
package dev.openfeature.sdk;
22

3-
import lombok.Getter;
4-
import lombok.Setter;
5-
6-
import javax.annotation.Nullable;
73
import java.util.ArrayList;
84
import java.util.Arrays;
95
import java.util.List;
106

7+
import javax.annotation.Nullable;
8+
9+
import dev.openfeature.sdk.internal.AutoCloseableLock;
10+
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
11+
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
*/
1516
public class OpenFeatureAPI {
16-
private static OpenFeatureAPI api;
17-
@Getter
18-
@Setter
17+
// package-private multi-read/single-write lock
18+
static AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
19+
static AutoCloseableReentrantReadWriteLock providerLock = new AutoCloseableReentrantReadWriteLock();
20+
static AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
1921
private FeatureProvider provider;
20-
@Getter
21-
@Setter
2222
private EvaluationContext evaluationContext;
23-
@Getter
2423
private List<Hook> apiHooks;
2524

26-
public OpenFeatureAPI() {
25+
private OpenFeatureAPI() {
2726
this.apiHooks = new ArrayList<>();
2827
}
2928

29+
private static class SingletonHolder {
30+
private static final OpenFeatureAPI INSTANCE = new OpenFeatureAPI();
31+
}
32+
3033
/**
3134
* Provisions the {@link OpenFeatureAPI} singleton (if needed) and returns it.
3235
* @return The singleton instance.
3336
*/
3437
public static OpenFeatureAPI getInstance() {
35-
synchronized (OpenFeatureAPI.class) {
36-
if (api == null) {
37-
api = new OpenFeatureAPI();
38-
}
39-
}
40-
return api;
38+
return SingletonHolder.INSTANCE;
4139
}
4240

4341
public Metadata getProviderMetadata() {
@@ -56,11 +54,66 @@ public Client getClient(@Nullable String name, @Nullable String version) {
5654
return new OpenFeatureClient(this, name, version);
5755
}
5856

57+
/**
58+
* {@inheritDoc}
59+
*/
60+
public void setEvaluationContext(EvaluationContext evaluationContext) {
61+
try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) {
62+
this.evaluationContext = evaluationContext;
63+
}
64+
}
65+
66+
/**
67+
* {@inheritDoc}
68+
*/
69+
public EvaluationContext getEvaluationContext() {
70+
try (AutoCloseableLock __ = contextLock.readLockAutoCloseable()) {
71+
return this.evaluationContext;
72+
}
73+
}
74+
75+
/**
76+
* {@inheritDoc}
77+
*/
78+
public void setProvider(FeatureProvider provider) {
79+
try (AutoCloseableLock __ = providerLock.writeLockAutoCloseable()) {
80+
this.provider = provider;
81+
}
82+
}
83+
84+
/**
85+
* {@inheritDoc}
86+
*/
87+
public FeatureProvider getProvider() {
88+
try (AutoCloseableLock __ = providerLock.readLockAutoCloseable()) {
89+
return this.provider;
90+
}
91+
}
92+
93+
/**
94+
* {@inheritDoc}
95+
*/
5996
public void addHooks(Hook... hooks) {
60-
this.apiHooks.addAll(Arrays.asList(hooks));
97+
try (AutoCloseableLock __ = hooksLock.writeLockAutoCloseable()) {
98+
this.apiHooks.addAll(Arrays.asList(hooks));
99+
}
61100
}
62101

102+
/**
103+
* {@inheritDoc}
104+
*/
105+
public List<Hook> getHooks() {
106+
try (AutoCloseableLock __ = hooksLock.readLockAutoCloseable()) {
107+
return this.apiHooks;
108+
}
109+
}
110+
111+
/**
112+
* {@inheritDoc}
113+
*/
63114
public void clearHooks() {
64-
this.apiHooks.clear();
115+
try (AutoCloseableLock __ = hooksLock.writeLockAutoCloseable()) {
116+
this.apiHooks.clear();
117+
}
65118
}
66119
}

Diff for: src/main/java/dev/openfeature/sdk/OpenFeatureClient.java

+59-18
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
import dev.openfeature.sdk.exceptions.GeneralError;
1010
import dev.openfeature.sdk.exceptions.OpenFeatureError;
11+
import dev.openfeature.sdk.internal.AutoCloseableLock;
12+
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
1113
import dev.openfeature.sdk.internal.ObjectUtils;
1214
import lombok.Getter;
13-
import lombok.Setter;
1415
import lombok.extern.slf4j.Slf4j;
1516

1617
@Slf4j
@@ -22,12 +23,10 @@ public class OpenFeatureClient implements Client {
2223
private final String name;
2324
@Getter
2425
private final String version;
25-
@Getter
2626
private final List<Hook> clientHooks;
2727
private final HookSupport hookSupport;
28-
29-
@Getter
30-
@Setter
28+
AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock();
29+
AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock();
3130
private EvaluationContext evaluationContext;
3231

3332
/**
@@ -46,9 +45,44 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String vers
4645
this.hookSupport = new HookSupport();
4746
}
4847

48+
/**
49+
* {@inheritDoc}
50+
*/
4951
@Override
5052
public void addHooks(Hook... hooks) {
51-
this.clientHooks.addAll(Arrays.asList(hooks));
53+
try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) {
54+
this.clientHooks.addAll(Arrays.asList(hooks));
55+
}
56+
}
57+
58+
/**
59+
* {@inheritDoc}
60+
*/
61+
@Override
62+
public List<Hook> getHooks() {
63+
try (AutoCloseableLock __ = this.hooksLock.readLockAutoCloseable()) {
64+
return this.clientHooks;
65+
}
66+
}
67+
68+
/**
69+
* {@inheritDoc}
70+
*/
71+
@Override
72+
public void setEvaluationContext(EvaluationContext evaluationContext) {
73+
try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) {
74+
this.evaluationContext = evaluationContext;
75+
}
76+
}
77+
78+
/**
79+
* {@inheritDoc}
80+
*/
81+
@Override
82+
public EvaluationContext getEvaluationContext() {
83+
try (AutoCloseableLock __ = contextLock.readLockAutoCloseable()) {
84+
return this.evaluationContext;
85+
}
5286
}
5387

5488
private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue,
@@ -57,34 +91,41 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key
5791
() -> FlagEvaluationOptions.builder().build());
5892
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
5993
ctx = ObjectUtils.defaultIfNull(ctx, () -> new MutableContext());
60-
FeatureProvider provider = ObjectUtils.defaultIfNull(openfeatureApi.getProvider(), () -> {
61-
log.debug("No provider configured, using no-op provider.");
62-
return new NoOpProvider();
63-
});
94+
6495

6596
FlagEvaluationDetails<T> details = null;
6697
List<Hook> mergedHooks = null;
6798
HookContext<T> hookCtx = null;
99+
FeatureProvider provider = null;
68100

69101
try {
102+
final EvaluationContext apiContext;
103+
final EvaluationContext clientContext;
70104

71-
hookCtx = HookContext.from(key, type, this.getMetadata(),
72-
openfeatureApi.getProvider().getMetadata(), ctx, defaultValue);
105+
// openfeatureApi.getProvider() must be called once to maintain a consistent reference
106+
provider = ObjectUtils.defaultIfNull(openfeatureApi.getProvider(), () -> {
107+
log.debug("No provider configured, using no-op provider.");
108+
return new NoOpProvider();
109+
});
73110

74111
mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
75-
openfeatureApi.getApiHooks());
112+
openfeatureApi.getHooks());
76113

77-
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
78-
79-
EvaluationContext invocationCtx = ctx.merge(ctxFromHook);
114+
hookCtx = HookContext.from(key, type, this.getMetadata(),
115+
provider.getMetadata(), ctx, defaultValue);
80116

81117
// merge of: API.context, client.context, invocation.context
82-
EvaluationContext apiContext = openfeatureApi.getEvaluationContext() != null
118+
apiContext = openfeatureApi.getEvaluationContext() != null
83119
? openfeatureApi.getEvaluationContext()
84120
: new MutableContext();
85-
EvaluationContext clientContext = openfeatureApi.getEvaluationContext() != null
121+
clientContext = openfeatureApi.getEvaluationContext() != null
86122
? this.getEvaluationContext()
87123
: new MutableContext();
124+
125+
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
126+
127+
EvaluationContext invocationCtx = ctx.merge(ctxFromHook);
128+
88129
EvaluationContext mergedCtx = apiContext.merge(clientContext.merge(invocationCtx));
89130

90131
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package dev.openfeature.sdk.internal;
2+
3+
public interface AutoCloseableLock extends AutoCloseable {
4+
5+
/**
6+
* Override the exception in AutoClosable.
7+
*/
8+
@Override
9+
void close();
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dev.openfeature.sdk.internal;
2+
3+
import java.util.concurrent.locks.ReentrantReadWriteLock;
4+
5+
/**
6+
* A utility class that wraps a multi-read/single-write lock construct as AutoCloseable, so it can
7+
* be used in a try-with-resources.
8+
*/
9+
public class AutoCloseableReentrantReadWriteLock extends ReentrantReadWriteLock {
10+
11+
/**
12+
* Get the single write lock as an AutoCloseableLock.
13+
* @return unlock method ref
14+
*/
15+
public AutoCloseableLock writeLockAutoCloseable() {
16+
this.writeLock().lock();
17+
return this.writeLock()::unlock;
18+
}
19+
20+
/**
21+
* Get the multi read lock as an AutoCloseableLock.
22+
* @return unlock method ref
23+
*/
24+
public AutoCloseableLock readLockAutoCloseable() {
25+
this.readLock().lock();
26+
return this.readLock()::unlock;
27+
}
28+
}

0 commit comments

Comments
 (0)