Skip to content

Commit c7c86d3

Browse files
authored
feat: Implement multiprovider (#354)
Signed-off-by: Jordan Blacker <[email protected]>
1 parent 1be83ec commit c7c86d3

20 files changed

+3223
-15
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ openfeature.SetProviderAndWait(MyProvider{})
117117
```
118118

119119
In some situations, it may be beneficial to register multiple providers in the same application.
120-
This is possible using [domains](#domains), which is covered in more details below.
120+
This is possible using [domains](#domains), which is covered in more details below, or the included [multiprovider](#multi-provider-implementation)
121+
implementation.
121122

122123
### Targeting
123124

@@ -331,6 +332,11 @@ tCtx := openfeature.MergeTransactionContext(ctx, openfeature.EvaluationContext{}
331332
client.BooleanValue(tCtx, ....)
332333
```
333334

335+
### Multi-Provider Implementation
336+
337+
Included with this SDK is an _experimental_ multi-provider that can be used to query multiple feature flag providers simultaneously.
338+
More information can be found in the [multi package's README](openfeature/multi/README.md).
339+
334340
## Extending
335341

336342
### Develop a provider

codecov.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
ignore:
2-
- "**/*_mock.go"
2+
- "**/*_mock.go"

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@ go 1.24.0
55
require (
66
github.com/cucumber/godog v0.15.1
77
github.com/go-logr/logr v1.4.3
8+
github.com/stretchr/testify v1.11.1
89
go.uber.org/mock v0.6.0
10+
golang.org/x/sync v0.17.0
911
golang.org/x/text v0.30.0
1012
)
1113

1214
require (
1315
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
1416
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
17+
github.com/davecgh/go-spew v1.1.1 // indirect
1518
github.com/gofrs/uuid v4.4.0+incompatible // indirect
1619
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
1720
github.com/hashicorp/go-memdb v1.3.4 // indirect
1821
github.com/hashicorp/golang-lru v1.0.2 // indirect
22+
github.com/pmezard/go-difflib v1.0.0 // indirect
1923
github.com/spf13/pflag v1.0.7 // indirect
24+
gopkg.in/yaml.v3 v3.0.1 // indirect
2025
)

go.sum

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
22
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
33
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
4-
github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo=
5-
github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
64
github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI=
75
github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8=
86
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
@@ -30,14 +28,15 @@ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uG
3028
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
3129
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
3230
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
31+
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
3332
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
3433
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
34+
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
3535
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
3636
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3737
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3838
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
3939
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
40-
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
4140
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
4241
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
4342
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
@@ -48,21 +47,18 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
4847
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
4948
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
5049
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
51-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
52-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
53-
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
54-
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
50+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
51+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
5552
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
5653
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
57-
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
58-
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
59-
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
60-
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
54+
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
55+
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
6156
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
6257
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
6358
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
6459
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
6560
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
61+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
6662
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
6763
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
6864
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

openfeature/multi/README.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
OpenFeature Multi-Provider
2+
------------
3+
4+
> [!WARNING]
5+
> The multi package for the go-sdk is experimental.
6+
7+
8+
The multi-provider allows you to use multiple underlying providers as sources of flag data for the OpenFeature server SDK.
9+
The multi-provider acts as a wrapper providing a unified interface to interact with all of those providers at once.
10+
When a flag is being evaluated, the Multi-Provider will consult each underlying provider it is managing in order to
11+
determine the final result. Different evaluation strategies can be defined to control which providers get evaluated and
12+
which result is used.
13+
14+
The multi-provider is defined within [Appendix A: Included Utilities](https://openfeature.dev/specification/appendix-a#multi-provider)
15+
of the openfeature spec.
16+
17+
The multi-provider is a powerful tool for performing migrations between flag providers, or combining multiple providers
18+
into a single feature flagging interface. For example:
19+
20+
- **Migration**: When migrating between two providers, you can run both in parallel under a unified flagging interface.
21+
As flags are added to the new provider, the multi-provider will automatically find and return them, falling back to the old provider
22+
if the new provider does not have
23+
- **Multiple Data Sources**: The multi-provider allows you to seamlessly combine many sources of flagging data, such as
24+
environment variables, local files, database values and SaaS hosted feature management systems.
25+
26+
# Usage
27+
28+
```go
29+
import (
30+
"github.com/open-feature/go-sdk/openfeature"
31+
"github.com/open-feature/go-sdk/openfeature/multi"
32+
"github.com/open-feature/go-sdk/openfeature/memprovider"
33+
)
34+
35+
providers := make(multi.ProviderMap)
36+
providers["providerA"] = memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{})
37+
providers["providerB"] = myCustomProvider
38+
mprovider, err := multi.NewProvider(providers, multi.StrategyFirstMatch)
39+
if err != nil {
40+
return err
41+
}
42+
43+
openfeature.SetNamedProviderAndWait("multiprovider", mprovider)
44+
```
45+
46+
# Strategies
47+
48+
There are three strategies that are defined by the spec and are available within this multi-provider implementation. In
49+
addition to the three provided strategies a custom strategy can be defined as well.
50+
51+
The three provided strategies are:
52+
53+
- _First Match_
54+
- _First Success_
55+
- _Comparison_
56+
57+
## First Match Strategy
58+
59+
The first match strategy works by **sequentially** calling each provider until a valid result is returned.
60+
The first provider that returns a result will be used. It will try calling the next provider whenever it encounters a `FLAG_NOT_FOUND`
61+
error. However, if a provider returns an error other than `FLAG_NOT_FOUND` the provider will stop and return the default
62+
value along with setting the error details if a detailed request is issued.
63+
64+
## First Success Strategy
65+
66+
The first success strategy also works by calling each provider **sequentially**. The first provider that returns a response
67+
with no errors is used. This differs from the first match strategy in that any provider raising an error will not halt
68+
calling the next provider if a successful result has not yet been encountered. If no provider provides a successful result
69+
the default value will be returned to the caller.
70+
71+
## Comparison Strategy
72+
73+
The comparison strategy works by calling each provider in **parallel**. All results are collected from each provider and
74+
then the resolved results are compared to each other. If they all agree then that value is returned. If not a fallback
75+
provider can be specified to be executed instead or the default value will be returned. If a provider returns
76+
`FLAG_NOT_FOUND` that result will not be included in the comparison. If all providers return not found then the default
77+
value is returned. Finally, if any provider returns an error other than `FLAG_NOT_FOUND` the evaluation immediately stops
78+
and that error result is returned with the default value.
79+
80+
The fallback provider can be set using the `WithFallbackProvider` [`Option`](#options).
81+
82+
Special care must be taken when this strategy is used with `ObjectEvaluation`. If the resulting value is not a
83+
[`comparable`](https://go.dev/blog/comparable) type then the default result or fallback provider will always be used. In
84+
order to evaluate non `comparable` types a `Comparator` function must be provided as an `Option` to the constructor.
85+
86+
## Custom Strategies
87+
88+
A custom strategy can be defined using the `WithCustomStrategy` `Option` along with the `StrategyCustom` constant.
89+
A custom strategy is defined by the following generic function signature:
90+
91+
```go
92+
StrategyFn[T FlagTypes] func(ctx context.Context, flag string, defaultValue T, flatCtx openfeature.FlattenedContext) openfeature.GenericResolutionDetail[T]
93+
```
94+
95+
However, this doesn't provide any way to retrieve the providers! Therefore, there's the type `StrategyConstructor` that
96+
is called for you to close over the providers inside your `StratetegyFn` implementation.
97+
98+
```go
99+
type StrategyConstructor func(providers []*NamedProvider) StrategyFn[FlagTypes]
100+
```
101+
102+
Build your strategy to wrap around the slice of providers
103+
```go
104+
option := multi.WithCustomStrategy(func(providers []*NamedProvider) StrategyFn[FlagTypes] {
105+
return func[T FlagTypes](ctx context.Context, flag string, defaultValue T, flatCtx openfeature.FlattenedContext) openfeature.GenericResolutionDetail[T] {
106+
// implementation
107+
// ...
108+
}
109+
})
110+
```
111+
112+
It is highly recommended to use the provided exposed functions to build your custom strategy. Specifically, the functions
113+
`BuildDefaultResult` & `Evaluate` are exposed for those implementing their own custom strategies.
114+
115+
The `Evaluate` method should be used for evaluating the result of a single `NamedProvider`. It determines the evaluation
116+
type via the type of the generic `defaultVal` parameter.
117+
118+
The `BuildDefaultResult` method should be called when an error is encountered or the strategy "fails" and needs to return
119+
the default result passed to one of the Evaluation methods of `openfeature.FeatureProvider`.
120+
121+
# Options
122+
123+
The `multi.NewProvider` constructor implements the optional pattern for setting additional configuration.
124+
125+
## General Options
126+
127+
### `WithLogger`
128+
129+
Allows for providing a `slog.Logger` instance for internal logging of the multi-provider's evaluation processing for debugging
130+
purposes. By default, are logs are discarded unless this option is used.
131+
132+
### `WithCustomStrategy`
133+
134+
Allows for setting a custom strategy function for the evaluation of providers. This must be used in conjunction with the
135+
`StrategyCustom` `EvaluationStrategy` parameter. The option itself takes a `StrategyConstructor` function, which is
136+
essentially a factory that allows the `StrategyFn` to wrap around a slice of `NamedProvider` instances.
137+
138+
### `WithGlobalHooks`
139+
140+
Allows for setting global hooks for the multi-provider. These are `openfeature.Hook` implementations that affect
141+
**all** internal `FeatureProvider` instances.
142+
143+
### `WithProviderHooks`
144+
145+
Allows for setting `openfeature.Hook` implementations on a specific named `FeatureProvider` within the multi-provider.
146+
This should only be used when hooks need to be attached to a `FeatureProvider` instance that does not implement that functionality.
147+
Using a provider name that is not known will cause an error to be returned during the creation time. This option can be
148+
used multiple times using unique provider names.
149+
150+
## `StrategyComparision` specific options
151+
152+
There are two options specifically for usage with the `StrategyComparision` `EvaluationStrategy`. If these options are
153+
used with a different `EvaluationStrategy` they are ignored.
154+
155+
### `WithFallbackProvider`
156+
157+
When the results are not in agreement with each other the fallback provider will be called. The result of this provider
158+
is what will be returned to the caller. If no fallback provider is set then the default value will be returned instead.
159+
160+
### `WithCustomComparator`
161+
162+
When using `ObjectEvaluation` there are cases where the results are not able to be compared to each other by default.
163+
This happens if the returned type is not `comparable`. In that situation all the results are passed to the custom `Comparator`
164+
to evaluate if they are in agreement or not. If not provided and the return type is not `comparable` then either the fallback
165+
provider is used or the default value.

0 commit comments

Comments
 (0)