Skip to content

Commit 0f8125e

Browse files
chrfwowtoddbaert
andauthored
feat: Layered context to reduce memory churn (#1717)
* add bench Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * fix bench Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * merge master Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * improve benchmark Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * improve benchmark Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * layered context Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * add tests Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * add merge Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * add tests Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * lazy init hook ctx Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * use list for hook ctx Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * add str hook for ctx Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * spotless Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * spotless Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * improve as map Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * improve tests Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * spotless Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * remove deprectaed notice Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * check if hook context is empty Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> * revert comments Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> --------- Signed-off-by: christian.lutnik <christian.lutnik@dynatrace.com> Co-authored-by: Todd Baert <todd.baert@dynatrace.com>
1 parent 6de54e3 commit 0f8125e

File tree

10 files changed

+878
-37
lines changed

10 files changed

+878
-37
lines changed

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

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ class HookSupport {
1717
* Sets the {@link Hook}-{@link HookContext}-{@link Pair} list in the given data object with {@link HookContext}
1818
* set to null. Filters hooks by supported {@link FlagValueType}.
1919
*
20-
* @param hookSupportData the data object to modify
21-
* @param hooks the hooks to set
22-
* @param type the flag value type to filter unsupported hooks
20+
* @param hookSupportData the data object to modify
21+
* @param hooks the hooks to set
22+
* @param type the flag value type to filter unsupported hooks
2323
*/
2424
public void setHooks(HookSupportData hookSupportData, List<Hook> hooks, FlagValueType type) {
2525
List<Pair<Hook, HookContext>> hookContextPairs = new ArrayList<>();
@@ -35,35 +35,20 @@ public void setHooks(HookSupportData hookSupportData, List<Hook> hooks, FlagValu
3535
* Creates & sets a {@link HookContext} for every {@link Hook}-{@link HookContext}-{@link Pair}
3636
* in the given data object with a new {@link HookData} instance.
3737
*
38-
* @param hookSupportData the data object to modify
39-
* @param sharedContext the shared context from which the new {@link HookContext} is created
38+
* @param hookSupportData the data object to modify
39+
* @param sharedContext the shared context from which the new {@link HookContext} is created
4040
*/
41-
public void setHookContexts(HookSupportData hookSupportData, SharedHookContext sharedContext) {
41+
public void setHookContexts(
42+
HookSupportData hookSupportData,
43+
SharedHookContext sharedContext,
44+
LayeredEvaluationContext evaluationContext) {
4245
for (int i = 0; i < hookSupportData.hooks.size(); i++) {
4346
Pair<Hook, HookContext> hookContextPair = hookSupportData.hooks.get(i);
44-
HookContext curHookContext = sharedContext.hookContextFor(null, new DefaultHookData());
47+
HookContext curHookContext = sharedContext.hookContextFor(evaluationContext, new DefaultHookData());
4548
hookContextPair.setValue(curHookContext);
4649
}
4750
}
4851

49-
/**
50-
* Updates the evaluation context in the given data object's eval context and each hooks eval context.
51-
*
52-
* @param hookSupportData the data object to modify
53-
* @param evaluationContext the new context to set
54-
*/
55-
public void updateEvaluationContext(HookSupportData hookSupportData, EvaluationContext evaluationContext) {
56-
hookSupportData.evaluationContext = evaluationContext;
57-
if (hookSupportData.hooks != null) {
58-
for (Pair<Hook, HookContext> hookContextPair : hookSupportData.hooks) {
59-
var curHookContext = hookContextPair.getValue();
60-
if (curHookContext != null) {
61-
curHookContext.setCtx(evaluationContext);
62-
}
63-
}
64-
}
65-
}
66-
6752
public void executeBeforeHooks(HookSupportData data) {
6853
// These traverse backwards from normal.
6954
List<Pair<Hook, HookContext>> reversedHooks = new ArrayList<>(data.getHooks());
@@ -77,8 +62,10 @@ public void executeBeforeHooks(HookSupportData data) {
7762
hook.before(hookContext, data.getHints()))
7863
.orElse(Optional.empty());
7964
if (returnedEvalContext.isPresent()) {
80-
// update shared evaluation context for all hooks
81-
updateEvaluationContext(data, data.getEvaluationContext().merge(returnedEvalContext.get()));
65+
var returnedContext = returnedEvalContext.get();
66+
if (!returnedContext.isEmpty()) {
67+
data.evaluationContext.putHookContext(returnedContext);
68+
}
8269
}
8370
}
8471
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
class HookSupportData {
1212

1313
List<Pair<Hook, HookContext>> hooks;
14-
EvaluationContext evaluationContext;
14+
LayeredEvaluationContext evaluationContext;
1515
Map<String, Object> hints;
1616

1717
HookSupportData() {}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package dev.openfeature.sdk;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.HashMap;
6+
import java.util.HashSet;
7+
import java.util.Map;
8+
import java.util.Set;
9+
10+
/**
11+
* LayeredEvaluationContext implements EvaluationContext by layering multiple contexts:
12+
* API-level, Transaction-level, Client-level, Invocation-level, and Hook-level.
13+
* The contexts are checked in that order for values, with Hook-level having the highest precedence.
14+
*/
15+
public class LayeredEvaluationContext implements EvaluationContext {
16+
private final EvaluationContext apiContext;
17+
private final EvaluationContext transactionContext;
18+
private final EvaluationContext clientContext;
19+
private final EvaluationContext invocationContext;
20+
21+
private ArrayList<EvaluationContext> hookContexts;
22+
private String targetingKey;
23+
private Set<String> keySet = null;
24+
25+
/**
26+
* Constructor for LayeredEvaluationContext.
27+
*/
28+
public LayeredEvaluationContext(
29+
EvaluationContext apiContext,
30+
EvaluationContext transactionContext,
31+
EvaluationContext clientContext,
32+
EvaluationContext invocationContext) {
33+
this.apiContext = apiContext;
34+
this.transactionContext = transactionContext;
35+
this.clientContext = clientContext;
36+
this.invocationContext = invocationContext;
37+
38+
if (invocationContext != null && invocationContext.getTargetingKey() != null) {
39+
this.targetingKey = invocationContext.getTargetingKey();
40+
} else if (clientContext != null && clientContext.getTargetingKey() != null) {
41+
this.targetingKey = clientContext.getTargetingKey();
42+
} else if (transactionContext != null && transactionContext.getTargetingKey() != null) {
43+
this.targetingKey = transactionContext.getTargetingKey();
44+
} else if (apiContext != null && apiContext.getTargetingKey() != null) {
45+
this.targetingKey = apiContext.getTargetingKey();
46+
} else {
47+
this.targetingKey = null;
48+
}
49+
}
50+
51+
@Override
52+
public String getTargetingKey() {
53+
return targetingKey;
54+
}
55+
56+
@Override
57+
public EvaluationContext merge(EvaluationContext overridingContext) {
58+
var merged = new LayeredEvaluationContext(apiContext, transactionContext, clientContext, invocationContext);
59+
60+
if (this.hookContexts == null) {
61+
merged.hookContexts = new ArrayList<>(1);
62+
} else {
63+
merged.hookContexts = new ArrayList<>(this.hookContexts.size() + 1);
64+
merged.hookContexts.addAll(this.hookContexts);
65+
}
66+
merged.hookContexts.add(overridingContext);
67+
68+
var otherTargetingKey = overridingContext.getTargetingKey();
69+
if (otherTargetingKey != null) {
70+
merged.targetingKey = otherTargetingKey;
71+
}
72+
return merged;
73+
}
74+
75+
@Override
76+
public boolean isEmpty() {
77+
return (invocationContext == null || invocationContext.isEmpty())
78+
&& (clientContext == null || clientContext.isEmpty())
79+
&& (transactionContext == null || transactionContext.isEmpty())
80+
&& (apiContext == null || apiContext.isEmpty())
81+
&& areHookContextsEmpty();
82+
}
83+
84+
private boolean areHookContextsEmpty() {
85+
if (hookContexts == null || hookContexts.isEmpty()) {
86+
return true;
87+
}
88+
89+
for (int i = 0; i < hookContexts.size(); i++) {
90+
var current = hookContexts.get(i);
91+
if (!current.isEmpty()) {
92+
return false;
93+
}
94+
}
95+
96+
return true;
97+
}
98+
99+
@Override
100+
public Set<String> keySet() {
101+
return Collections.unmodifiableSet(ensureKeySet());
102+
}
103+
104+
private Set<String> ensureKeySet() {
105+
if (this.keySet != null) {
106+
return this.keySet;
107+
}
108+
109+
var keys = new HashSet<String>();
110+
111+
if (hookContexts != null) {
112+
for (int i = 0; i < hookContexts.size(); i++) {
113+
var current = hookContexts.get(i);
114+
keys.addAll(current.keySet());
115+
}
116+
}
117+
118+
if (invocationContext != null) {
119+
keys.addAll(invocationContext.keySet());
120+
}
121+
if (clientContext != null) {
122+
keys.addAll(clientContext.keySet());
123+
}
124+
if (transactionContext != null) {
125+
keys.addAll(transactionContext.keySet());
126+
}
127+
if (apiContext != null) {
128+
keys.addAll(apiContext.keySet());
129+
}
130+
this.keySet = keys;
131+
return keys;
132+
}
133+
134+
private Value getFromContext(EvaluationContext context, String key) {
135+
if (context != null) {
136+
return context.getValue(key);
137+
}
138+
return null;
139+
}
140+
141+
private Value getFromContext(ArrayList<EvaluationContext> context, String key) {
142+
if (context == null) {
143+
return null;
144+
}
145+
146+
for (int i = context.size() - 1; i >= 0; i--) {
147+
var current = context.get(i);
148+
var value = getFromContext(current, key);
149+
if (value != null) {
150+
return value;
151+
}
152+
}
153+
return null;
154+
}
155+
156+
@Override
157+
public Value getValue(String key) {
158+
var hookValue = getFromContext(hookContexts, key);
159+
if (hookValue != null) {
160+
return hookValue;
161+
}
162+
var invocationValue = getFromContext(invocationContext, key);
163+
if (invocationValue != null) {
164+
return invocationValue;
165+
}
166+
var clientValue = getFromContext(clientContext, key);
167+
if (clientValue != null) {
168+
return clientValue;
169+
}
170+
var transactionValue = getFromContext(transactionContext, key);
171+
if (transactionValue != null) {
172+
return transactionValue;
173+
}
174+
return getFromContext(apiContext, key);
175+
}
176+
177+
@Override
178+
public Map<String, Value> asMap() {
179+
if (keySet != null && keySet.isEmpty()) {
180+
return new HashMap<>(0);
181+
}
182+
183+
HashMap<String, Value> map;
184+
if (keySet != null) {
185+
map = new HashMap<>(keySet.size());
186+
} else {
187+
map = new HashMap<>();
188+
}
189+
190+
if (apiContext != null) {
191+
map.putAll(apiContext.asMap());
192+
}
193+
if (transactionContext != null) {
194+
map.putAll(transactionContext.asMap());
195+
}
196+
if (clientContext != null) {
197+
map.putAll(clientContext.asMap());
198+
}
199+
if (invocationContext != null) {
200+
map.putAll(invocationContext.asMap());
201+
}
202+
if (hookContexts != null) {
203+
for (int i = 0; i < hookContexts.size(); i++) {
204+
EvaluationContext hookContext = hookContexts.get(i);
205+
map.putAll(hookContext.asMap());
206+
}
207+
}
208+
return map;
209+
}
210+
211+
@Override
212+
public Map<String, Value> asUnmodifiableMap() {
213+
if (keySet != null && keySet.isEmpty()) {
214+
return Collections.emptyMap();
215+
}
216+
217+
return Collections.unmodifiableMap(asMap());
218+
}
219+
220+
@Override
221+
public Map<String, Object> asObjectMap() {
222+
if (keySet != null && keySet.isEmpty()) {
223+
return new HashMap<>(0);
224+
}
225+
226+
HashMap<String, Object> map;
227+
if (keySet != null) {
228+
map = new HashMap<>(keySet.size());
229+
} else {
230+
map = new HashMap<>();
231+
}
232+
233+
if (apiContext != null) {
234+
map.putAll(apiContext.asObjectMap());
235+
}
236+
if (transactionContext != null) {
237+
map.putAll(transactionContext.asObjectMap());
238+
}
239+
if (clientContext != null) {
240+
map.putAll(clientContext.asObjectMap());
241+
}
242+
if (invocationContext != null) {
243+
map.putAll(invocationContext.asObjectMap());
244+
}
245+
if (hookContexts != null) {
246+
for (int i = 0; i < hookContexts.size(); i++) {
247+
EvaluationContext hookContext = hookContexts.get(i);
248+
map.putAll(hookContext.asObjectMap());
249+
}
250+
}
251+
return map;
252+
}
253+
254+
void putHookContext(EvaluationContext context) {
255+
if (context == null || context.isEmpty()) {
256+
return;
257+
}
258+
259+
var targetingKey = context.getTargetingKey();
260+
if (targetingKey != null) {
261+
this.targetingKey = targetingKey;
262+
}
263+
if (this.hookContexts == null) {
264+
this.hookContexts = new ArrayList<>();
265+
}
266+
this.hookContexts.add(context);
267+
this.keySet = null;
268+
}
269+
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
166166
var flagOptions = ObjectUtils.defaultIfNull(
167167
options, () -> FlagEvaluationOptions.builder().build());
168168
hookSupportData.hints = Collections.unmodifiableMap(flagOptions.getHookHints());
169+
var context = new LayeredEvaluationContext(
170+
openfeatureApi.getEvaluationContext(),
171+
openfeatureApi.getTransactionContext(),
172+
evaluationContext.get(),
173+
ctx);
174+
hookSupportData.evaluationContext = context;
169175

170176
try {
171177
final var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain);
@@ -180,10 +186,7 @@ private <T> FlagEvaluationDetails<T> evaluateFlag(
180186

181187
var sharedHookContext =
182188
new SharedHookContext(key, type, this.getMetadata(), provider.getMetadata(), defaultValue);
183-
hookSupport.setHookContexts(hookSupportData, sharedHookContext);
184-
185-
var evalContext = mergeEvaluationContext(ctx);
186-
hookSupport.updateEvaluationContext(hookSupportData, evalContext);
189+
hookSupport.setHookContexts(hookSupportData, sharedHookContext, context);
187190

188191
hookSupport.executeBeforeHooks(hookSupportData);
189192

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ void mergeHappensCorrectly() {
723723
invocationCtx,
724724
FlagEvaluationOptions.builder().hook(hook).build());
725725

726-
ArgumentCaptor<ImmutableContext> captor = ArgumentCaptor.forClass(ImmutableContext.class);
726+
ArgumentCaptor<LayeredEvaluationContext> captor = ArgumentCaptor.forClass(LayeredEvaluationContext.class);
727727
verify(provider).getBooleanEvaluation(any(), any(), captor.capture());
728728
EvaluationContext ec = captor.getValue();
729729
assertEquals("works", ec.getValue("test").asString());

0 commit comments

Comments
 (0)