Skip to content

Commit 3c0af0a

Browse files
Merge branch 'main' into feat/ofrep-provider
2 parents 99c92f1 + 0a3e58a commit 3c0af0a

4 files changed

Lines changed: 60 additions & 9 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package zio.openfeature
2+
3+
import dev.openfeature.sdk.multiprovider.{FirstMatchStrategy, FirstSuccessfulStrategy}
4+
5+
/** Built-in strategies for [[FeatureFlags.fromMultiProvider]] and [[FeatureFlags.fromMultiProviderAsync]].
6+
*
7+
* Re-exports the Java SDK's strategy implementations under stable Scala names so callers do not need to depend on the
8+
* `dev.openfeature.sdk.multiprovider` package directly.
9+
*/
10+
object MultiProviderStrategy {
11+
12+
/** Alias for [[dev.openfeature.sdk.multiprovider.Strategy]]. */
13+
type Strategy = dev.openfeature.sdk.multiprovider.Strategy
14+
15+
/** Returns the result of the first provider whose evaluation does not surface a default value. An evaluation error
16+
* from any provider aborts the chain; use [[firstSuccessful]] if you want errors to fall through to the next
17+
* provider. A fresh instance is returned each call to stay safe if the upstream class ever gains internal state.
18+
*/
19+
def firstMatch: Strategy = new FirstMatchStrategy
20+
21+
/** Returns the result of the first provider whose evaluation completes without an error. Errors fall through to the
22+
* next provider; default-reason results do not. A fresh instance is returned each call.
23+
*/
24+
def firstSuccessful: Strategy = new FirstSuccessfulStrategy
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package zio.openfeature
2+
3+
import dev.openfeature.sdk.multiprovider.{FirstMatchStrategy, FirstSuccessfulStrategy}
4+
import zio.test._
5+
6+
object MultiProviderStrategySpec extends ZIOSpecDefault {
7+
8+
def spec = suite("MultiProviderStrategy")(
9+
test("firstMatch alias resolves to FirstMatchStrategy") {
10+
assertTrue(MultiProviderStrategy.firstMatch.isInstanceOf[FirstMatchStrategy])
11+
},
12+
test("firstSuccessful alias resolves to FirstSuccessfulStrategy") {
13+
assertTrue(MultiProviderStrategy.firstSuccessful.isInstanceOf[FirstSuccessfulStrategy])
14+
},
15+
test("Strategy type alias is assignable to the Java SDK Strategy type (compile-time check)") {
16+
// If the type alias were wrong, this assignment would not compile.
17+
val _: dev.openfeature.sdk.multiprovider.Strategy = MultiProviderStrategy.firstMatch
18+
assertCompletes
19+
},
20+
test("firstMatch and firstSuccessful return fresh instances on each call") {
21+
assertTrue(MultiProviderStrategy.firstMatch ne MultiProviderStrategy.firstMatch) &&
22+
assertTrue(MultiProviderStrategy.firstSuccessful ne MultiProviderStrategy.firstSuccessful)
23+
}
24+
)
25+
}

docs/extras.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ Adding `zio-openfeature-ofrep` pulls in Jackson (core/databind/jsr310), Guava, C
229229

230230
## Circuit Breaker Provider
231231

232-
A decorator that wraps any provider with circuit breaker logic for fast failover. When the delegate provider fails repeatedly or becomes unhealthy, the circuit opens and evaluations fail immediately (< 1ms) — enabling instant fallback when composed with `MultiProvider` and `FirstSuccessfulStrategy`.
232+
A decorator that wraps any provider with circuit breaker logic for fast failover. When the delegate provider fails repeatedly or becomes unhealthy, the circuit opens and evaluations fail immediately (< 1ms) — enabling instant fallback when composed with `MultiProvider` and `MultiProviderStrategy.firstSuccessful`.
233233

234234
### When to use
235235

@@ -256,7 +256,6 @@ The circuit breaker has three states:
256256
import zio.*
257257
import zio.openfeature.*
258258
import zio.openfeature.extras.*
259-
import dev.openfeature.sdk.multiprovider.FirstSuccessfulStrategy
260259

261260
// Wrap the primary provider with circuit breaker
262261
val resilientProvider = CircuitBreakerProvider(
@@ -273,7 +272,7 @@ val resilientProvider = CircuitBreakerProvider(
273272
// Compose with fallback using MultiProvider
274273
val layer = FeatureFlags.fromMultiProvider(
275274
List(resilientProvider, EnvVarProvider()),
276-
new FirstSuccessfulStrategy()
275+
MultiProviderStrategy.firstSuccessful
277276
)
278277
```
279278

@@ -286,7 +285,7 @@ for
286285
))
287286
layer = FeatureFlags.fromMultiProvider(
288287
List(cb, EnvVarProvider()),
289-
new FirstSuccessfulStrategy()
288+
MultiProviderStrategy.firstSuccessful
290289
)
291290
yield layer
292291
```
@@ -315,7 +314,7 @@ Controls how the circuit breaker reacts when the delegate provider is in `STALE`
315314

316315
| Approach | During outage | Failover latency |
317316
|:---------|:--------------|:-----------------|
318-
| `MultiProvider` + `FirstSuccessfulStrategy` alone | Tries primary every time, waits for failure | Up to minutes |
317+
| `MultiProvider` + `firstSuccessful` alone | Tries primary every time, waits for failure | Up to minutes |
319318
| Add timeout only (e.g., 50ms) | Still tries primary every time | 50ms per call |
320319
| **Circuit breaker** | Skips primary entirely when open | **< 1ms** |
321320

docs/providers.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ val layer = FeatureFlags.fromProviderWithHooks(provider, hooks)
446446

447447
### fromMultiProvider
448448

449-
Create from multiple providers using the SDK's MultiProvider support. The first provider that returns without error is used:
449+
Create from multiple providers using the SDK's MultiProvider support. By default this uses the first-match strategy — the first provider whose result is not a default value is returned (a provider error from any source aborts the chain):
450450

451451
```scala
452452
import dev.openfeature.sdk.FeatureProvider
@@ -458,17 +458,19 @@ val remoteProvider: FeatureProvider = // remote service
458458
val layer = FeatureFlags.fromMultiProvider(List(localProvider, remoteProvider))
459459
```
460460

461-
You can also supply a custom strategy:
461+
You can also supply a custom strategy. The two built-in strategies are exposed via `MultiProviderStrategy` so you don't need to import the Java SDK's multiprovider package directly:
462462

463463
```scala
464-
import dev.openfeature.sdk.multiprovider.FirstSuccessfulStrategy
464+
import zio.openfeature.MultiProviderStrategy
465465

466466
val layer = FeatureFlags.fromMultiProvider(
467467
List(primaryProvider, fallbackProvider),
468-
new FirstSuccessfulStrategy()
468+
MultiProviderStrategy.firstSuccessful
469469
)
470470
```
471471

472+
`MultiProviderStrategy.firstMatch` and `MultiProviderStrategy.firstSuccessful` are the two built-ins. For a custom strategy, implement the `MultiProviderStrategy.Strategy` interface (an alias for the Java SDK's `Strategy`) and pass an instance the same way.
473+
472474
### Async Variants (Non-Blocking Initialization)
473475

474476
Every factory method has an async counterpart that uses the Java SDK's non-blocking `setProvider` instead of `setProviderAndWait`. The provider initializes in the background; evaluations fail with `ProviderNotReady` until the provider is ready.

0 commit comments

Comments
 (0)