Skip to content

Commit 2e75cd2

Browse files
committed
Merge branch 'main' into feat/add-provider-event-details
2 parents 635b654 + 807fcc1 commit 2e75cd2

File tree

22 files changed

+1872
-245
lines changed

22 files changed

+1872
-245
lines changed

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ jobs:
1313
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
16-
uses: actions/checkout@v4
16+
uses: actions/checkout@v5
1717

1818
# For browser tests
19-
- uses: browser-actions/setup-chrome@v1
19+
- uses: browser-actions/setup-chrome@v2
2020

2121
- name: Run checks
2222
run: ./gradlew check --no-daemon --stacktrace

.github/workflows/lint-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
name: Validate PR title
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: amannn/action-semantic-pull-request@v5
15+
- uses: amannn/action-semantic-pull-request@v6
1616
id: lint_pr_title
1717
env:
1818
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/manual-tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
runs-on: ubuntu-latest
2222
steps:
2323
- name: Checkout code
24-
uses: actions/checkout@v4
24+
uses: actions/checkout@v5
2525
with:
2626
ref: ${{ github.event.inputs.branch }}
2727

.github/workflows/release_please.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
restore-keys: |
4444
${{ runner.os }}-gradle-
4545
46-
- uses: actions/checkout@v4
46+
- uses: actions/checkout@v5
4747

4848
- name: Configure GPG Key
4949
run: |
@@ -63,7 +63,7 @@ jobs:
6363
GPG_SIGNING_KEY_PASSWORD: ${{ secrets.GPG_SIGNING_KEY_PASSWORD }}
6464

6565
- name: Set up JDK 17
66-
uses: actions/setup-java@v3
66+
uses: actions/setup-java@v5
6767
with:
6868
java-version: 17
6969
distribution: 'zulu'

build.gradle.kts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
// Top-level build file where you can add configuration options common to all sub-projects/modules.
22
plugins {
3-
id("com.android.library").version("8.10.0").apply(false)
4-
id("com.android.application").version("8.10.0").apply(false)
5-
id("org.jetbrains.kotlin.multiplatform").version("2.1.21").apply(false)
6-
id("org.jetbrains.kotlin.plugin.compose").version("2.1.0").apply(false)
3+
id("com.android.library").version("8.12.2").apply(false)
4+
id("com.android.application").version("8.12.2").apply(false)
5+
id("org.jetbrains.kotlin.multiplatform").version("2.2.10").apply(false)
6+
id("org.jetbrains.kotlin.plugin.compose").version("2.2.10").apply(false)
77
id("org.jetbrains.dokka").version("2.0.0").apply(false)
88
id("org.jlleitschuh.gradle.ktlint").version("11.6.1").apply(true)
99
id("io.github.gradle-nexus.publish-plugin").version("2.0.0").apply(true)
10-
id("org.jetbrains.kotlinx.binary-compatibility-validator").version("0.17.0").apply(false)
10+
id("org.jetbrains.kotlinx.binary-compatibility-validator").version("0.18.1").apply(false)
1111
id("com.vanniktech.maven.publish").version("0.34.0").apply(false)
1212
}
1313
allprojects {

docs/multiprovider/README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+

gradle/wrapper/gradle-wrapper.jar

1.65 KB
Binary file not shown.

gradle/wrapper/gradle-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
distributionBase=GRADLE_USER_HOME
22
distributionPath=wrapper/dists
3-
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
44
networkTimeout=10000
55
validateDistributionUrl=true
66
zipStoreBase=GRADLE_USER_HOME

gradlew

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/bin/sh
22

33
#
4-
# Copyright © 2015-2021 the original authors.
4+
# Copyright © 2015 the original authors.
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.

0 commit comments

Comments
 (0)