Skip to content

Commit 6fe18eb

Browse files
committed
Add README documentation for Multiprovider
Signed-off-by: penguindan <[email protected]>
1 parent 70a892d commit 6fe18eb

File tree

1 file changed

+127
-0
lines changed

1 file changed

+127
-0
lines changed

docs/multiprovider/README.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
## MultiProvider (OpenFeature Kotlin SDK)
2+
3+
Combine multiple `FeatureProvider`s into a single provider with deterministic ordering, pluggable evaluation strategies, and unified status/event handling.
4+
5+
### Why use MultiProvider?
6+
- **Layer providers**: fall back from an in-memory or experiment provider to a remote provider.
7+
- **Migrate safely**: put the new provider first, retain the old as fallback.
8+
- **Handle errors predictably**: choose whether errors should short-circuit or be skipped.
9+
10+
This implementation is adapted for Kotlin coroutines, flows, and OpenFeature error types.
11+
12+
### Quick start
13+
```kotlin
14+
import dev.openfeature.kotlin.sdk.OpenFeatureAPI
15+
import dev.openfeature.kotlin.sdk.multiprovider.MultiProvider
16+
import dev.openfeature.kotlin.sdk.multiprovider.FirstMatchStrategy
17+
// import dev.openfeature.kotlin.sdk.multiprovider.FirstSuccessfulStrategy
18+
19+
// 1) Construct your providers (examples)
20+
val experiments = MyExperimentProvider() // e.g., local overrides/experiments
21+
val remote = MyRemoteProvider() // e.g., network-backed
22+
23+
// 2) Wrap them with MultiProvider in the desired order
24+
val multi = MultiProvider(
25+
providers = listOf(experiments, remote),
26+
strategy = FirstMatchStrategy() // default; FirstSuccessfulStrategy() also available
27+
)
28+
29+
// 3) Set the SDK provider and wait until ready (or observe status)
30+
OpenFeatureAPI.setProvider(multi)
31+
// Optionally await readiness via OpenFeatureAPI.statusFlow.
32+
33+
// 4) Use the client as usual
34+
val client = OpenFeatureAPI.getClient("my-app")
35+
val enabled = client.getBooleanValue("new-ui", defaultValue = false)
36+
```
37+
38+
### How it works (at a glance)
39+
- The `MultiProvider` delegates each evaluation to its child providers in the order you supply.
40+
- A pluggable `Strategy` decides which child result to return.
41+
- Provider events are observed and converted into a single aggregate SDK status.
42+
- Context changes are forwarded to all children concurrently.
43+
44+
### Strategies
45+
46+
- **FirstMatchStrategy (default)**
47+
- Returns the first child result that is not "flag not found".
48+
- If a child returns an error other than `FLAG_NOT_FOUND`, that error is returned immediately.
49+
- If all children report `FLAG_NOT_FOUND`, the default value is returned with reason `DEFAULT`.
50+
51+
- **FirstSuccessfulStrategy**
52+
- Skips over errors from children and continues to the next provider.
53+
- Returns the first successful evaluation (no error code).
54+
- If no provider succeeds, the default value is returned with `FLAG_NOT_FOUND`.
55+
56+
Pick the strategy that best matches your failure-policy:
57+
- Prefer early, explicit error surfacing: use `FirstMatchStrategy`.
58+
- Prefer resilience and best-effort success: use `FirstSuccessfulStrategy`.
59+
60+
### Evaluation order matters
61+
Children are evaluated in the order provided. Put the most authoritative or fastest provider first. For example, place a small in-memory override provider before a remote provider to reduce latency.
62+
63+
### Events and status aggregation
64+
`MultiProvider` listens to child provider events and emits a single, aggregate status via `OpenFeatureAPI.statusFlow`. The highest-precedence status among children wins:
65+
66+
1. Fatal
67+
2. NotReady
68+
3. Error
69+
4. Reconciling / Stale
70+
5. Ready
71+
72+
`ProviderConfigurationChanged` is re-emitted as-is. When the aggregate status changes due to a child event, the original triggering event is also emitted.
73+
74+
### Context propagation
75+
When the evaluation context changes, `MultiProvider` calls `onContextSet` on all child providers concurrently. Aggregate status transitions to Reconciling and then back to Ready (or Error) in line with SDK behavior.
76+
77+
### Provider metadata
78+
`MultiProvider.metadata` exposes:
79+
- `name = "multiprovider"`
80+
- `originalMetadata`: a map of child-name → child `ProviderMetadata`
81+
82+
Child names are derived from each provider’s `metadata.name`. If duplicates occur, stable suffixes are applied (e.g., `myProvider_1`, `myProvider_2`).
83+
84+
Example: inspect provider metadata
85+
```kotlin
86+
val meta = OpenFeatureAPI.getProviderMetadata()
87+
println(meta?.name) // "multiprovider"
88+
println(meta?.originalMetadata) // map of child names to their metadata
89+
```
90+
91+
### Shutdown behavior
92+
`shutdown()` is invoked on all children. If any child fails to shut down, an aggregated error is thrown that includes all individual failures. Resources should be released in child providers even if peers fail.
93+
94+
### Custom strategies
95+
You can provide your own composition policy by implementing `MultiProvider.Strategy`:
96+
```kotlin
97+
import dev.openfeature.kotlin.sdk.*
98+
import dev.openfeature.kotlin.sdk.multiprovider.MultiProvider
99+
100+
class MyStrategy : MultiProvider.Strategy {
101+
override fun <T> evaluate(
102+
providers: List<FeatureProvider>,
103+
key: String,
104+
defaultValue: T,
105+
evaluationContext: EvaluationContext?,
106+
flagEval: FeatureProvider.(String, T, EvaluationContext?) -> ProviderEvaluation<T>
107+
): ProviderEvaluation<T> {
108+
// Example: try all, prefer the highest integer value (demo only)
109+
var best: ProviderEvaluation<T>? = null
110+
for (p in providers) {
111+
val e = p.flagEval(key, defaultValue, evaluationContext)
112+
// ... decide whether to keep e as best ...
113+
best = best ?: e
114+
}
115+
return best ?: ProviderEvaluation(defaultValue)
116+
}
117+
}
118+
119+
val multi = MultiProvider(listOf(experiments, remote), strategy = MyStrategy())
120+
```
121+
122+
### Notes and limitations
123+
- Hooks on `MultiProvider` are currently not applied.
124+
- Ensure each child’s `metadata.name` is set for clearer diagnostics in `originalMetadata`.
125+
126+
127+

0 commit comments

Comments
 (0)