|
| 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 |
| 30 | +OpenFeatureAPI.setProviderAndWait() |
| 31 | + |
| 32 | +// 4) Use the client as usual |
| 33 | +val client = OpenFeatureAPI.getClient("my-app") |
| 34 | +val enabled = client.getBooleanValue("new-ui", defaultValue = false) |
| 35 | +``` |
| 36 | + |
| 37 | +### How it works (at a glance) |
| 38 | +- The `MultiProvider` delegates each evaluation to its child providers in the order you supply. |
| 39 | +- A pluggable `Strategy` decides which child result to return. |
| 40 | +- Provider events are observed and converted into a single aggregate SDK status. |
| 41 | +- Context changes are forwarded to all children concurrently. |
| 42 | + |
| 43 | +### Strategies |
| 44 | + |
| 45 | +- **FirstMatchStrategy (default)** |
| 46 | + - Returns the first child result that is not "flag not found". |
| 47 | + - If a child returns an error other than `FLAG_NOT_FOUND`, that error is returned immediately. |
| 48 | + - If all children report `FLAG_NOT_FOUND`, the default value is returned with reason `DEFAULT`. |
| 49 | + |
| 50 | +- **FirstSuccessfulStrategy** |
| 51 | + - Skips over errors from children and continues to the next provider. |
| 52 | + - Returns the first successful evaluation (no error code). |
| 53 | + - If no provider succeeds, the default value is returned with `FLAG_NOT_FOUND`. |
| 54 | + |
| 55 | +Pick the strategy that best matches your failure-policy: |
| 56 | +- Prefer early, explicit error surfacing: use `FirstMatchStrategy`. |
| 57 | +- Prefer resilience and best-effort success: use `FirstSuccessfulStrategy`. |
| 58 | + |
| 59 | +### Evaluation order matters |
| 60 | +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. |
| 61 | + |
| 62 | +### Events and status aggregation |
| 63 | +`MultiProvider` listens to child provider events and emits a single, aggregate status via `OpenFeatureAPI.statusFlow`. The highest-precedence status among children wins: |
| 64 | + |
| 65 | +1. Fatal |
| 66 | +2. NotReady |
| 67 | +3. Error |
| 68 | +4. Reconciling / Stale |
| 69 | +5. Ready |
| 70 | + |
| 71 | +`ProviderConfigurationChanged` is re-emitted as-is. When the aggregate status changes due to a child event, the original triggering event is also emitted. |
| 72 | + |
| 73 | +### Context propagation |
| 74 | +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. |
| 75 | + |
| 76 | +### Provider metadata |
| 77 | +`MultiProvider.metadata` exposes: |
| 78 | +- `name = "multiprovider"` |
| 79 | +- `originalMetadata`: a map of child-name → child `ProviderMetadata` |
| 80 | + |
| 81 | +Child names are derived from each provider’s `metadata.name`. If duplicates occur, stable suffixes are applied (e.g., `myProvider_1`, `myProvider_2`). |
| 82 | + |
| 83 | +Example: inspect provider metadata |
| 84 | +```kotlin |
| 85 | +val meta = OpenFeatureAPI.getProviderMetadata() |
| 86 | +println(meta?.name) // "multiprovider" |
| 87 | +println(meta?.originalMetadata) // map of child names to their metadata |
| 88 | +``` |
| 89 | + |
| 90 | +### Shutdown behavior |
| 91 | +`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. |
| 92 | + |
| 93 | +### Custom strategies |
| 94 | +You can provide your own composition policy by implementing `MultiProvider.Strategy`: |
| 95 | +```kotlin |
| 96 | +import dev.openfeature.kotlin.sdk.* |
| 97 | +import dev.openfeature.kotlin.sdk.multiprovider.MultiProvider |
| 98 | + |
| 99 | +class MyStrategy : MultiProvider.Strategy { |
| 100 | + override fun <T> evaluate( |
| 101 | + providers: List<FeatureProvider>, |
| 102 | + key: String, |
| 103 | + defaultValue: T, |
| 104 | + evaluationContext: EvaluationContext?, |
| 105 | + flagEval: FeatureProvider.(String, T, EvaluationContext?) -> ProviderEvaluation<T> |
| 106 | + ): ProviderEvaluation<T> { |
| 107 | + // Example: try all, prefer the highest integer value (demo only) |
| 108 | + var best: ProviderEvaluation<T>? = null |
| 109 | + for (p in providers) { |
| 110 | + val e = p.flagEval(key, defaultValue, evaluationContext) |
| 111 | + // ... decide whether to keep e as best ... |
| 112 | + best = best ?: e |
| 113 | + } |
| 114 | + return best ?: ProviderEvaluation(defaultValue) |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +val multi = MultiProvider(listOf(experiments, remote), strategy = MyStrategy()) |
| 119 | +``` |
| 120 | + |
| 121 | +### Notes and limitations |
| 122 | +- Hooks on `MultiProvider` are currently not applied. |
| 123 | +- Ensure each child’s `metadata.name` is set for clearer diagnostics in `originalMetadata`. |
| 124 | + |
| 125 | + |
| 126 | + |
0 commit comments