Skip to content

Commit 725dedc

Browse files
feat: execute child provider hooks in multiprovider evaluations
Signed-off-by: Jonathan Norris <jonathan.norris@dynatrace.com>
1 parent d7e9a13 commit 725dedc

5 files changed

Lines changed: 1222 additions & 5 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package dev.openfeature.sdk.multiprovider;
2+
3+
import dev.openfeature.sdk.ClientMetadata;
4+
import java.util.Map;
5+
6+
/** Captures hook lifecycle context (client metadata and hints) for per-provider hook execution. */
7+
final class HookExecutionContext {
8+
final ClientMetadata clientMetadata;
9+
final Map<String, Object> hints;
10+
11+
HookExecutionContext(ClientMetadata clientMetadata, Map<String, Object> hints) {
12+
this.clientMetadata = clientMetadata;
13+
this.hints = hints;
14+
}
15+
}

src/main/java/dev/openfeature/sdk/multiprovider/MultiProvider.java

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package dev.openfeature.sdk.multiprovider;
22

3+
import dev.openfeature.sdk.ClientMetadata;
34
import dev.openfeature.sdk.EvaluationContext;
45
import dev.openfeature.sdk.EventProvider;
56
import dev.openfeature.sdk.FeatureProvider;
7+
import dev.openfeature.sdk.FlagEvaluationDetails;
8+
import dev.openfeature.sdk.FlagValueType;
9+
import dev.openfeature.sdk.Hook;
10+
import dev.openfeature.sdk.HookContext;
611
import dev.openfeature.sdk.Metadata;
712
import dev.openfeature.sdk.ProviderEvaluation;
813
import dev.openfeature.sdk.Value;
@@ -15,6 +20,7 @@
1520
import java.util.List;
1621
import java.util.Map;
1722
import java.util.Objects;
23+
import java.util.Optional;
1824
import java.util.concurrent.Callable;
1925
import java.util.concurrent.ExecutorService;
2026
import java.util.concurrent.Executors;
@@ -28,6 +34,9 @@
2834
* <p>This provider delegates flag evaluations to multiple underlying providers using a configurable
2935
* {@link Strategy}. It also exposes combined metadata containing the original metadata of each
3036
* underlying provider.
37+
*
38+
* <p>Hooks registered on the child providers are executed around each child evaluation, so a child
39+
* provider's own hooks observe the evaluation it takes part in.
3140
*/
3241
@Slf4j
3342
public class MultiProvider extends EventProvider {
@@ -40,6 +49,9 @@ public class MultiProvider extends EventProvider {
4049

4150
private final Map<String, FeatureProvider> providers;
4251
private final Strategy strategy;
52+
private final ThreadLocal<HookExecutionContext> hookExecutionContextThreadLocal = new ThreadLocal<>();
53+
private final ClientMetadata hookClientMetadata = MultiProvider::getNAME;
54+
private final MultiProviderHookExecutor hookExecutor = new MultiProviderHookExecutor(hookClientMetadata);
4355
private MultiProviderMetadata metadata;
4456

4557
/**
@@ -63,6 +75,49 @@ public MultiProvider(List<FeatureProvider> providers, Strategy strategy) {
6375
this.strategy = Objects.requireNonNull(strategy, "strategy must not be null");
6476
}
6577

78+
@SuppressWarnings({"rawtypes", "unchecked"})
79+
private final List<Hook> providerHooks = List.of(new Hook() {
80+
@Override
81+
public Optional before(HookContext ctx, Map hints) {
82+
hookExecutionContextThreadLocal.set(
83+
new HookExecutionContext(ctx.getClientMetadata(), snapshotHints(hints)));
84+
return Optional.empty();
85+
}
86+
87+
@Override
88+
public void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) {
89+
hookExecutionContextThreadLocal.remove();
90+
}
91+
});
92+
93+
/**
94+
* Returns provider-level hooks for this MultiProvider.
95+
*
96+
* <p>Includes a {@code before} hook that captures the {@link ClientMetadata}
97+
* and hook hints from the SDK's hook lifecycle. This context is then available
98+
* during per-child-provider hook execution, matching the JS SDK's WeakMap-based
99+
* approach for passing hook context into the provider evaluation.
100+
*
101+
* @return the list of provider hooks
102+
*/
103+
@Override
104+
public List<Hook> getProviderHooks() {
105+
return providerHooks;
106+
}
107+
108+
/**
109+
* Defensively copies the hook hints. {@code FlagEvaluationOptions.hookHints} is backed by a
110+
* mutable map, and the captured hints may be read from other threads when a strategy evaluates
111+
* providers in parallel. A plain copy (rather than {@code Map.copyOf}) is used so that hints
112+
* containing null values are still supported.
113+
*/
114+
private static Map<String, Object> snapshotHints(Map<String, Object> hints) {
115+
if (hints == null || hints.isEmpty()) {
116+
return Collections.emptyMap();
117+
}
118+
return Collections.unmodifiableMap(new HashMap<>(hints));
119+
}
120+
66121
protected static Map<String, FeatureProvider> buildProviders(List<FeatureProvider> providers) {
67122
Map<String, FeatureProvider> providersMap = new LinkedHashMap<>(providers.size());
68123
for (FeatureProvider provider : providers) {
@@ -147,29 +202,96 @@ public Metadata getMetadata() {
147202

148203
@Override
149204
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
205+
HookExecutionContext hookExecutionContext = currentHookExecutionContext();
150206
return strategy.evaluate(
151-
providers, key, defaultValue, ctx, p -> p.getBooleanEvaluation(key, defaultValue, ctx));
207+
providers,
208+
key,
209+
defaultValue,
210+
ctx,
211+
provider -> hookExecutor.evaluate(
212+
provider,
213+
key,
214+
defaultValue,
215+
ctx,
216+
hookExecutionContext,
217+
FlagValueType.BOOLEAN,
218+
(p, evaluationContext) -> p.getBooleanEvaluation(key, defaultValue, evaluationContext)));
152219
}
153220

154221
@Override
155222
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
156-
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getStringEvaluation(key, defaultValue, ctx));
223+
HookExecutionContext hookExecutionContext = currentHookExecutionContext();
224+
return strategy.evaluate(
225+
providers,
226+
key,
227+
defaultValue,
228+
ctx,
229+
provider -> hookExecutor.evaluate(
230+
provider,
231+
key,
232+
defaultValue,
233+
ctx,
234+
hookExecutionContext,
235+
FlagValueType.STRING,
236+
(p, evaluationContext) -> p.getStringEvaluation(key, defaultValue, evaluationContext)));
157237
}
158238

159239
@Override
160240
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
241+
HookExecutionContext hookExecutionContext = currentHookExecutionContext();
161242
return strategy.evaluate(
162-
providers, key, defaultValue, ctx, p -> p.getIntegerEvaluation(key, defaultValue, ctx));
243+
providers,
244+
key,
245+
defaultValue,
246+
ctx,
247+
provider -> hookExecutor.evaluate(
248+
provider,
249+
key,
250+
defaultValue,
251+
ctx,
252+
hookExecutionContext,
253+
FlagValueType.INTEGER,
254+
(p, evaluationContext) -> p.getIntegerEvaluation(key, defaultValue, evaluationContext)));
163255
}
164256

165257
@Override
166258
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
167-
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getDoubleEvaluation(key, defaultValue, ctx));
259+
HookExecutionContext hookExecutionContext = currentHookExecutionContext();
260+
return strategy.evaluate(
261+
providers,
262+
key,
263+
defaultValue,
264+
ctx,
265+
provider -> hookExecutor.evaluate(
266+
provider,
267+
key,
268+
defaultValue,
269+
ctx,
270+
hookExecutionContext,
271+
FlagValueType.DOUBLE,
272+
(p, evaluationContext) -> p.getDoubleEvaluation(key, defaultValue, evaluationContext)));
168273
}
169274

170275
@Override
171276
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
172-
return strategy.evaluate(providers, key, defaultValue, ctx, p -> p.getObjectEvaluation(key, defaultValue, ctx));
277+
HookExecutionContext hookExecutionContext = currentHookExecutionContext();
278+
return strategy.evaluate(
279+
providers,
280+
key,
281+
defaultValue,
282+
ctx,
283+
provider -> hookExecutor.evaluate(
284+
provider,
285+
key,
286+
defaultValue,
287+
ctx,
288+
hookExecutionContext,
289+
FlagValueType.OBJECT,
290+
(p, evaluationContext) -> p.getObjectEvaluation(key, defaultValue, evaluationContext)));
291+
}
292+
293+
private HookExecutionContext currentHookExecutionContext() {
294+
return hookExecutionContextThreadLocal.get();
173295
}
174296

175297
@Override

0 commit comments

Comments
 (0)