From c6249d4a438622b68637f002a9673ef7a1cf694c Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:44:38 -0500 Subject: [PATCH 01/68] feat(multi-provider): set up multi-provider directory Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/README.md | 0 providers/multi-provider/go.mod | 5 +++++ providers/multi-provider/go.sum | 2 ++ release-please-config.json | 8 ++++++++ 4 files changed, 15 insertions(+) create mode 100644 providers/multi-provider/README.md create mode 100644 providers/multi-provider/go.mod create mode 100644 providers/multi-provider/go.sum diff --git a/providers/multi-provider/README.md b/providers/multi-provider/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/providers/multi-provider/go.mod b/providers/multi-provider/go.mod new file mode 100644 index 000000000..100a1b2e5 --- /dev/null +++ b/providers/multi-provider/go.mod @@ -0,0 +1,5 @@ +module github.com/open-feature/go-sdk-contrib/providers/multi-provider + +go 1.23.0 + +require github.com/open-feature/go-sdk v1.14.1 // indirect diff --git a/providers/multi-provider/go.sum b/providers/multi-provider/go.sum new file mode 100644 index 000000000..675e06c68 --- /dev/null +++ b/providers/multi-provider/go.sum @@ -0,0 +1,2 @@ +github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= +github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= diff --git a/release-please-config.json b/release-please-config.json index ee5b1b009..d1b4c7301 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -139,6 +139,14 @@ "bump-patch-for-minor-pre-major": true, "versioning": "default", "extra-files": [] + }, + "providers/multi-provider": { + "release-type": "go", + "package-name": "providers/multi-provider", + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default", + "extra-files": [] } }, "changelog-sections": [ From 446b5438219c035d7eef0b54c6e1d4bc0073c818 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Fri, 21 Feb 2025 23:31:02 -0500 Subject: [PATCH 02/68] feat: set up func to create new multiprovider & register passed providers Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/go.mod | 7 +- providers/multi-provider/go.sum | 8 ++ providers/multi-provider/providers.go | 88 ++++++++++++++++++++++ providers/multi-provider/providers_test.go | 1 + 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 providers/multi-provider/providers.go create mode 100644 providers/multi-provider/providers_test.go diff --git a/providers/multi-provider/go.mod b/providers/multi-provider/go.mod index 100a1b2e5..91a059021 100644 --- a/providers/multi-provider/go.mod +++ b/providers/multi-provider/go.mod @@ -2,4 +2,9 @@ module github.com/open-feature/go-sdk-contrib/providers/multi-provider go 1.23.0 -require github.com/open-feature/go-sdk v1.14.1 // indirect +require github.com/open-feature/go-sdk v1.14.1 + +require ( + github.com/go-logr/logr v1.4.2 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect +) diff --git a/providers/multi-provider/go.sum b/providers/multi-provider/go.sum index 675e06c68..ac0b45fa2 100644 --- a/providers/multi-provider/go.sum +++ b/providers/multi-provider/go.sum @@ -1,2 +1,10 @@ +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= diff --git a/providers/multi-provider/providers.go b/providers/multi-provider/providers.go new file mode 100644 index 000000000..9ec018a1c --- /dev/null +++ b/providers/multi-provider/providers.go @@ -0,0 +1,88 @@ +package multiprovider + +import ( + "errors" + "fmt" + + of "github.com/open-feature/go-sdk/openfeature" +) + +var ( + errUniqueName = errors.New("Provider names must be unique.") +) + +// UniqueNameProvider allows for a unique name to be assigned to a provider during a multi-provider set up. +// The name will be used when reporting errors & results to specify the provider associated. +type UniqueNameProvider struct { + Provider of.FeatureProvider + Name string +} + +type MultiMetadata struct { + Name string + OriginalMetadata map[string]of.Metadata +} + +// MultiProvider implements openfeature `FeatureProvider` in a way to accept an array of providers. +type MultiProvider struct { + providersEntries []UniqueNameProvider + providersEntriesByName map[string]UniqueNameProvider + AggregatedMetadata map[string]of.Metadata +} + +func NewMultiProvider(providers []UniqueNameProvider) (*MultiProvider, error) { + multiProvider := &MultiProvider{ + providersEntries: []UniqueNameProvider{}, + providersEntriesByName: map[string]UniqueNameProvider{}, + } + // for i, provider := range providers { + + // } + + // + + return multiProvider, nil +} + +func (mp MultiProvider) Metadata() of.Metadata { + + return of.Metadata{ + Name: fmt.Sprintf("multiprovider"), + } +} + +// registerProviders ensures that when setting up an instant of MultiProvider the providers provided either have a unique name or the base `metadata.Name` is made unique by adding an indexed based number to it. +// registerProviders also stores the providers by their unique name and in an array for easy usage. +func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error { + providersByName := make(map[string][]UniqueNameProvider) + + for _, provider := range providers { + uniqueName := provider.Name + + if _, exists := providersByName[uniqueName]; exists { + return errUniqueName + } + + if uniqueName == "" { + providersByName[provider.Provider.Metadata().Name] = append(providersByName[provider.Provider.Metadata().Name], provider) + } else { + providersByName[uniqueName] = append(providersByName[uniqueName], provider) + } + } + + for name, providers := range providersByName { + if len(providers) == 1 { + mp.providersEntries = append(mp.providersEntries, providers[0]) + mp.providersEntriesByName[name] = providers[0] + mp.AggregatedMetadata[name] = providers[0].Provider.Metadata() + } else { + for i, provider := range providers { + uniqueName := fmt.Sprintf("%s-%d", name, i+1) + mp.providersEntries = append(mp.providersEntries, provider) + mp.providersEntriesByName[uniqueName] = provider + mp.AggregatedMetadata[uniqueName] = provider.Provider.Metadata() + } + } + } + return nil +} diff --git a/providers/multi-provider/providers_test.go b/providers/multi-provider/providers_test.go new file mode 100644 index 000000000..c7735289c --- /dev/null +++ b/providers/multi-provider/providers_test.go @@ -0,0 +1 @@ +package multiprovider \ No newline at end of file From 1fae6f01b8256faa70b49703d4a460539259dfdd Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Sat, 22 Feb 2025 16:41:10 -0500 Subject: [PATCH 03/68] feat: added Metadata Method to the multiprovider Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/providers.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/providers/multi-provider/providers.go b/providers/multi-provider/providers.go index 9ec018a1c..79f06ed47 100644 --- a/providers/multi-provider/providers.go +++ b/providers/multi-provider/providers.go @@ -18,6 +18,7 @@ type UniqueNameProvider struct { Name string } +// MultiMetadata defines the return of the MultiProvider metadata with the aggregated data of all the providers. type MultiMetadata struct { Name string OriginalMetadata map[string]of.Metadata @@ -30,24 +31,27 @@ type MultiProvider struct { AggregatedMetadata map[string]of.Metadata } -func NewMultiProvider(providers []UniqueNameProvider) (*MultiProvider, error) { +func NewMultiProvider(passedProviders []UniqueNameProvider) (*MultiProvider, error) { multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, + AggregatedMetadata: map[string]of.Metadata{}, } - // for i, provider := range providers { - // } - - // + err := registerProviders(multiProvider, passedProviders) + if err != nil { + return nil, err + } return multiProvider, nil } -func (mp MultiProvider) Metadata() of.Metadata { +// Metadata provides the name `multiprovider` and the names of each provider passed. +func (mp MultiProvider) Metadata() MultiMetadata { - return of.Metadata{ - Name: fmt.Sprintf("multiprovider"), + return MultiMetadata{ + Name: "multiprovider", + OriginalMetadata: mp.AggregatedMetadata, } } From aeb8bd3af8ff5a86c2f34bde3662116fa524308b Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:36:19 -0500 Subject: [PATCH 04/68] feat: added the based of the Init method that will process the initalize of the providers Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .release-please-manifest.json | 3 +- providers/multi-provider/providers.go | 57 +++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 3de50afc1..fc12117e5 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -15,5 +15,6 @@ "providers/ofrep": "0.1.5", "providers/prefab": "0.0.2", "tests/flagd": "1.4.1", - "providers/go-feature-flag-in-process": "0.1.0" + "providers/go-feature-flag-in-process": "0.1.0", + "providers/multi-provider": "0.0.1" } diff --git a/providers/multi-provider/providers.go b/providers/multi-provider/providers.go index 79f06ed47..6d63ce306 100644 --- a/providers/multi-provider/providers.go +++ b/providers/multi-provider/providers.go @@ -3,6 +3,7 @@ package multiprovider import ( "errors" "fmt" + "sync" of "github.com/open-feature/go-sdk/openfeature" ) @@ -29,13 +30,17 @@ type MultiProvider struct { providersEntries []UniqueNameProvider providersEntriesByName map[string]UniqueNameProvider AggregatedMetadata map[string]of.Metadata + EvaluationStrategy string + event chan of.Event + status of.State + mu sync.Mutex } -func NewMultiProvider(passedProviders []UniqueNameProvider) (*MultiProvider, error) { +func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy string) (*MultiProvider, error) { multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, - AggregatedMetadata: map[string]of.Metadata{}, + AggregatedMetadata: map[string]of.Metadata{}, } err := registerProviders(multiProvider, passedProviders) @@ -43,6 +48,11 @@ func NewMultiProvider(passedProviders []UniqueNameProvider) (*MultiProvider, err return nil, err } + // err = multiProvider.initialize() + // if err != nil { + // return nil, err + // } + return multiProvider, nil } @@ -50,7 +60,7 @@ func NewMultiProvider(passedProviders []UniqueNameProvider) (*MultiProvider, err func (mp MultiProvider) Metadata() MultiMetadata { return MultiMetadata{ - Name: "multiprovider", + Name: "multiprovider", OriginalMetadata: mp.AggregatedMetadata, } } @@ -90,3 +100,44 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error } return nil } + +type InitError struct { + ProviderName string + Error error +} + +// Init will run the initialize method for all of provides and aggregate the errors. +func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { + var wg sync.WaitGroup + + errChan := make(chan InitError, len(mp.providersEntries)) + for _, provider := range mp.providersEntries { + wg.Add(1) + go func(p UniqueNameProvider) { + defer wg.Done() + if initMethod, ok := p.Provider.(of.StateHandler); ok { + if err := initMethod.Init(evalCtx); err != nil { + errChan <- InitError{ProviderName: p.Name, Error: err} + } + } + }(provider) + } + + wg.Wait() + close(errChan) + + + return nil +} + +func (mp *MultiProvider) Status() of.State { + return of.ReadyState +} + +func (mp *MultiProvider) Shutdown() { + +} + +func (mp *MultiProvider) EventChannel() <-chan of.Event { + return +} From 0605d94cdda6dcf2b7551f55296b382a3b03d10b Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:26:36 -0500 Subject: [PATCH 05/68] moved providers into pkg directory Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/{ => pkg}/providers.go | 11 +++++++---- providers/multi-provider/{ => pkg}/providers_test.go | 0 2 files changed, 7 insertions(+), 4 deletions(-) rename providers/multi-provider/{ => pkg}/providers.go (95%) rename providers/multi-provider/{ => pkg}/providers_test.go (100%) diff --git a/providers/multi-provider/providers.go b/providers/multi-provider/pkg/providers.go similarity index 95% rename from providers/multi-provider/providers.go rename to providers/multi-provider/pkg/providers.go index 6d63ce306..704612881 100644 --- a/providers/multi-provider/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -31,7 +31,7 @@ type MultiProvider struct { providersEntriesByName map[string]UniqueNameProvider AggregatedMetadata map[string]of.Metadata EvaluationStrategy string - event chan of.Event + events chan of.Event status of.State mu sync.Mutex } @@ -57,7 +57,7 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s } // Metadata provides the name `multiprovider` and the names of each provider passed. -func (mp MultiProvider) Metadata() MultiMetadata { +func (mp *MultiProvider) Metadata() MultiMetadata { return MultiMetadata{ Name: "multiprovider", @@ -103,7 +103,7 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error type InitError struct { ProviderName string - Error error + Error error } // Init will run the initialize method for all of provides and aggregate the errors. @@ -126,6 +126,8 @@ func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { wg.Wait() close(errChan) + // var errors []InitError + return nil } @@ -139,5 +141,6 @@ func (mp *MultiProvider) Shutdown() { } func (mp *MultiProvider) EventChannel() <-chan of.Event { - return + ev := make(chan of.Event) + return ev } diff --git a/providers/multi-provider/providers_test.go b/providers/multi-provider/pkg/providers_test.go similarity index 100% rename from providers/multi-provider/providers_test.go rename to providers/multi-provider/pkg/providers_test.go From 51c33bc6ae793fa3624508de1f223ce4a1acb11c Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Thu, 27 Feb 2025 01:46:06 -0500 Subject: [PATCH 06/68] added Init method to handle any providers with intialization Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../internal/aggregate-errors.go | 30 +++++++++++++ providers/multi-provider/pkg/providers.go | 42 ++++++++++++------- .../multi-provider/pkg/providers_test.go | 6 ++- 3 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 providers/multi-provider/internal/aggregate-errors.go diff --git a/providers/multi-provider/internal/aggregate-errors.go b/providers/multi-provider/internal/aggregate-errors.go new file mode 100644 index 000000000..c4d3e772f --- /dev/null +++ b/providers/multi-provider/internal/aggregate-errors.go @@ -0,0 +1,30 @@ +package internal + +import "fmt" + +// InitError is how the error in the Init stage of a provider is reported. +type InitError struct { + ProviderName string + Err error +} + +func (e *InitError) Error() string { + return fmt.Sprintf("Provider %s had an error: %v", e.ProviderName, e.Err) +} + +type AggregateError struct { + Message string + Errors []InitError +} + +func (ae *AggregateError) Error() string { + return ae.Message +} + +func (ae *AggregateError) Construct(providerErrors []InitError) { + // Show first error message for convenience, but all errors in the object + msg := fmt.Sprintf("Provider errors occurred: %s: %v", providerErrors[0].ProviderName, providerErrors[0].Err) + + ae.Message = msg + ae.Errors = providerErrors +} \ No newline at end of file diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 704612881..770d101d2 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -5,7 +5,10 @@ import ( "fmt" "sync" + err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" + of "github.com/open-feature/go-sdk/openfeature" + ofhooks "github.com/open-feature/go-sdk/openfeature/hooks" ) var ( @@ -36,7 +39,8 @@ type MultiProvider struct { mu sync.Mutex } -func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy string) (*MultiProvider, error) { +// NewMultiProvider returns the unified interface of multiple providers for interaction. +func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy string, logger ofhooks.LoggingHook) (*MultiProvider, error) { multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, @@ -48,10 +52,10 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s return nil, err } - // err = multiProvider.initialize() - // if err != nil { - // return nil, err - // } + err = multiProvider.Init(of.EvaluationContext{}) + if err != nil { + return nil, err + } return multiProvider, nil } @@ -101,32 +105,38 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error return nil } -type InitError struct { - ProviderName string - Error error -} - // Init will run the initialize method for all of provides and aggregate the errors. func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { var wg sync.WaitGroup - errChan := make(chan InitError, len(mp.providersEntries)) + errChan := make(chan err.InitError, len(mp.providersEntries)) for _, provider := range mp.providersEntries { wg.Add(1) go func(p UniqueNameProvider) { defer wg.Done() if initMethod, ok := p.Provider.(of.StateHandler); ok { - if err := initMethod.Init(evalCtx); err != nil { - errChan <- InitError{ProviderName: p.Name, Error: err} + if initErr := initMethod.Init(evalCtx); initErr != nil { + errChan <- err.InitError{ProviderName: p.Name, Err: initErr} } } }(provider) } - wg.Wait() - close(errChan) + go func() { + wg.Wait() + close(errChan) + }() + + var errors []err.InitError + for err := range errChan { + errors = append(errors, err) + } - // var errors []InitError + if len(errors) > 0 { + var aggErr err.AggregateError + aggErr.Construct(errors) + return &aggErr + } return nil diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index c7735289c..ab244afd2 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -1 +1,5 @@ -package multiprovider \ No newline at end of file +package multiprovider + +import "testing" + +func TestNewMultiProvider(t *testing.T){} \ No newline at end of file From 1d7eed5bb0fab3b16517dcee5456cf73ed69971e Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Thu, 27 Feb 2025 02:16:26 -0500 Subject: [PATCH 07/68] added the Shutdown method Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../internal/aggregate-errors.go | 14 ++++----- providers/multi-provider/pkg/providers.go | 31 +++++++++++++------ 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/providers/multi-provider/internal/aggregate-errors.go b/providers/multi-provider/internal/aggregate-errors.go index c4d3e772f..e974e9c05 100644 --- a/providers/multi-provider/internal/aggregate-errors.go +++ b/providers/multi-provider/internal/aggregate-errors.go @@ -2,29 +2,29 @@ package internal import "fmt" -// InitError is how the error in the Init stage of a provider is reported. -type InitError struct { +// StateErr is how the error in the Init of Shutdown stage of a provider is reported. +type StateErr struct { ProviderName string - Err error + Err error } -func (e *InitError) Error() string { +func (e *StateErr) Error() string { return fmt.Sprintf("Provider %s had an error: %v", e.ProviderName, e.Err) } type AggregateError struct { Message string - Errors []InitError + Errors []StateErr } func (ae *AggregateError) Error() string { return ae.Message } -func (ae *AggregateError) Construct(providerErrors []InitError) { +func (ae *AggregateError) Construct(providerErrors []StateErr) { // Show first error message for convenience, but all errors in the object msg := fmt.Sprintf("Provider errors occurred: %s: %v", providerErrors[0].ProviderName, providerErrors[0].Err) ae.Message = msg ae.Errors = providerErrors -} \ No newline at end of file +} diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 770d101d2..44de41214 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -52,10 +52,10 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s return nil, err } - err = multiProvider.Init(of.EvaluationContext{}) - if err != nil { - return nil, err - } + // err = multiProvider.Init(of.EvaluationContext{}) + // if err != nil { + // return nil, err + // } return multiProvider, nil } @@ -108,15 +108,15 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error // Init will run the initialize method for all of provides and aggregate the errors. func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { var wg sync.WaitGroup + errChan := make(chan err.StateErr, len(mp.providersEntries)) - errChan := make(chan err.InitError, len(mp.providersEntries)) for _, provider := range mp.providersEntries { wg.Add(1) go func(p UniqueNameProvider) { defer wg.Done() - if initMethod, ok := p.Provider.(of.StateHandler); ok { - if initErr := initMethod.Init(evalCtx); initErr != nil { - errChan <- err.InitError{ProviderName: p.Name, Err: initErr} + if stateHandle, ok := p.Provider.(of.StateHandler); ok { + if initErr := stateHandle.Init(evalCtx); initErr != nil { + errChan <- err.StateErr{ProviderName: p.Name, Err: initErr} } } }(provider) @@ -127,7 +127,7 @@ func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { close(errChan) }() - var errors []err.InitError + var errors []err.StateErr for err := range errChan { errors = append(errors, err) } @@ -138,7 +138,6 @@ func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { return &aggErr } - return nil } @@ -147,7 +146,19 @@ func (mp *MultiProvider) Status() of.State { } func (mp *MultiProvider) Shutdown() { + var wg sync.WaitGroup + + for _, provider := range mp.providersEntries { + wg.Add(1) + go func(p UniqueNameProvider) { + defer wg.Done() + if stateHandle, ok := p.Provider.(of.StateHandler); ok { + stateHandle.Shutdown() + } + }(provider) + } + wg.Wait() } func (mp *MultiProvider) EventChannel() <-chan of.Event { From 9ddd19b956259118939a4dd721121af467b937c3 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:28:35 -0500 Subject: [PATCH 08/68] added getter methods to the unexported fields Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/pkg/providers.go | 67 +++++++++++++---------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 44de41214..8c83ad3eb 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -7,44 +7,44 @@ import ( err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" - of "github.com/open-feature/go-sdk/openfeature" - ofhooks "github.com/open-feature/go-sdk/openfeature/hooks" + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/hooks" ) var ( - errUniqueName = errors.New("Provider names must be unique.") + errUniqueName = errors.New("provider names must be unique") ) // UniqueNameProvider allows for a unique name to be assigned to a provider during a multi-provider set up. // The name will be used when reporting errors & results to specify the provider associated. type UniqueNameProvider struct { - Provider of.FeatureProvider - Name string + Provider openfeature.FeatureProvider + UniqueName string } // MultiMetadata defines the return of the MultiProvider metadata with the aggregated data of all the providers. type MultiMetadata struct { Name string - OriginalMetadata map[string]of.Metadata + OriginalMetadata map[string]openfeature.Metadata } // MultiProvider implements openfeature `FeatureProvider` in a way to accept an array of providers. type MultiProvider struct { providersEntries []UniqueNameProvider providersEntriesByName map[string]UniqueNameProvider - AggregatedMetadata map[string]of.Metadata + AggregatedMetadata map[string]openfeature.Metadata EvaluationStrategy string - events chan of.Event - status of.State + events chan openfeature.Event + status openfeature.State mu sync.Mutex } // NewMultiProvider returns the unified interface of multiple providers for interaction. -func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy string, logger ofhooks.LoggingHook) (*MultiProvider, error) { +func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy string, logger *hooks.LoggingHook) (*MultiProvider, error) { multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, - AggregatedMetadata: map[string]of.Metadata{}, + AggregatedMetadata: map[string]openfeature.Metadata{}, } err := registerProviders(multiProvider, passedProviders) @@ -52,21 +52,26 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s return nil, err } - // err = multiProvider.Init(of.EvaluationContext{}) - // if err != nil { - // return nil, err - // } - return multiProvider, nil } +func (mp *MultiProvider) Providers() []UniqueNameProvider { + return mp.providersEntries +} + +func (mp *MultiProvider) ProvidersByName() []UniqueNameProvider { + return mp.providersEntries +} + +func (mp *MultiProvider) ProviderByName(name string) (UniqueNameProvider, bool) { + provider, exists := mp.providersEntriesByName[name] + return provider, exists +} + // Metadata provides the name `multiprovider` and the names of each provider passed. -func (mp *MultiProvider) Metadata() MultiMetadata { +func (mp *MultiProvider) Metadata() openfeature.Metadata { - return MultiMetadata{ - Name: "multiprovider", - OriginalMetadata: mp.AggregatedMetadata, - } + return openfeature.Metadata{Name: "multiprovider"} } // registerProviders ensures that when setting up an instant of MultiProvider the providers provided either have a unique name or the base `metadata.Name` is made unique by adding an indexed based number to it. @@ -75,7 +80,7 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error providersByName := make(map[string][]UniqueNameProvider) for _, provider := range providers { - uniqueName := provider.Name + uniqueName := provider.UniqueName if _, exists := providersByName[uniqueName]; exists { return errUniqueName @@ -90,12 +95,14 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error for name, providers := range providersByName { if len(providers) == 1 { + providers[0].UniqueName = name mp.providersEntries = append(mp.providersEntries, providers[0]) mp.providersEntriesByName[name] = providers[0] mp.AggregatedMetadata[name] = providers[0].Provider.Metadata() } else { for i, provider := range providers { uniqueName := fmt.Sprintf("%s-%d", name, i+1) + provider.UniqueName = uniqueName mp.providersEntries = append(mp.providersEntries, provider) mp.providersEntriesByName[uniqueName] = provider mp.AggregatedMetadata[uniqueName] = provider.Provider.Metadata() @@ -106,7 +113,7 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error } // Init will run the initialize method for all of provides and aggregate the errors. -func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { +func (mp *MultiProvider) Init(evalCtx openfeature.EvaluationContext) error { var wg sync.WaitGroup errChan := make(chan err.StateErr, len(mp.providersEntries)) @@ -114,9 +121,9 @@ func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { wg.Add(1) go func(p UniqueNameProvider) { defer wg.Done() - if stateHandle, ok := p.Provider.(of.StateHandler); ok { + if stateHandle, ok := p.Provider.(openfeature.StateHandler); ok { if initErr := stateHandle.Init(evalCtx); initErr != nil { - errChan <- err.StateErr{ProviderName: p.Name, Err: initErr} + errChan <- err.StateErr{ProviderName: p.UniqueName, Err: initErr} } } }(provider) @@ -141,8 +148,8 @@ func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { return nil } -func (mp *MultiProvider) Status() of.State { - return of.ReadyState +func (mp *MultiProvider) Status() openfeature.State { + return openfeature.ReadyState } func (mp *MultiProvider) Shutdown() { @@ -152,7 +159,7 @@ func (mp *MultiProvider) Shutdown() { wg.Add(1) go func(p UniqueNameProvider) { defer wg.Done() - if stateHandle, ok := p.Provider.(of.StateHandler); ok { + if stateHandle, ok := p.Provider.(openfeature.StateHandler); ok { stateHandle.Shutdown() } }(provider) @@ -161,7 +168,7 @@ func (mp *MultiProvider) Shutdown() { wg.Wait() } -func (mp *MultiProvider) EventChannel() <-chan of.Event { - ev := make(chan of.Event) +func (mp *MultiProvider) EventChannel() <-chan openfeature.Event { + ev := make(chan openfeature.Event) return ev } From 09a988f929fe3a519077fdd14dc5e2017c96b823 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Thu, 27 Feb 2025 20:35:12 -0500 Subject: [PATCH 09/68] added tests to check unique name for providers passed Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../multi-provider/pkg/providers_test.go | 164 +++++++++++++++++- 1 file changed, 162 insertions(+), 2 deletions(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index ab244afd2..ec4380d14 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -1,5 +1,165 @@ package multiprovider -import "testing" +import ( + "testing" -func TestNewMultiProvider(t *testing.T){} \ No newline at end of file + // "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/hooks" + "github.com/open-feature/go-sdk/openfeature/memprovider" + oft "github.com/open-feature/go-sdk/openfeature/testing" +) + +func TestNewMultiProvider_ProviderMetadataUniqueNames(t *testing.T) { + testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ + "boolFlag": { + Key: "boolFlag", + State: memprovider.Enabled, + DefaultVariant: "true", + Variants: map[string]interface{}{ + "true": true, + "false": false, + }, + ContextEvaluator: nil, + }, + }) + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + multiProvider, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + }, { + Provider: testProvider2, + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providerEntries := multiProvider.Providers() + + if providerEntries[0].UniqueName != "InMemoryProvider" { + t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", providerEntries[0].UniqueName) + } + if providerEntries[1].UniqueName != "NoopProvider" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[1].UniqueName) + } + + if len(providerEntries) != 2 { + t.Errorf("Expected there to be 2 provider entries, got: '%d'", len(providerEntries)) + } +} + +func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { + testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ + "boolFlag": { + Key: "boolFlag", + State: memprovider.Enabled, + DefaultVariant: "true", + Variants: map[string]interface{}{ + "true": true, + "false": false, + }, + ContextEvaluator: nil, + }, + }) + testProvider2 := oft.NewTestProvider() + testProvider3 := oft.NewTestProvider() + testProvider4 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + multiProvider, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + }, { + Provider: testProvider2, + }, { + Provider: testProvider3, + },{ + Provider: testProvider4, + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providerEntries := multiProvider.Providers() + + if providerEntries[0].UniqueName != "InMemoryProvider" { + t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", providerEntries[0].UniqueName) + } + if providerEntries[1].UniqueName != "NoopProvider-1" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[1].UniqueName) + } + if providerEntries[2].UniqueName != "NoopProvider-2" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[2].UniqueName) + } + if providerEntries[3].UniqueName != "NoopProvider-3" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[3].UniqueName) + } + + if len(providerEntries) != 4 { + t.Errorf("Expected there to be 4 provider entries, got: '%d'", len(providerEntries)) + } +} +func TestNewMultiProvider_(t *testing.T) { + testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ + "boolFlag": { + Key: "boolFlag", + State: memprovider.Enabled, + DefaultVariant: "true", + Variants: map[string]interface{}{ + "true": true, + "false": false, + }, + ContextEvaluator: nil, + }, + }) + testProvider2 := oft.NewTestProvider() + testProvider3 := oft.NewTestProvider() + testProvider4 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + multiProvider, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + }, { + Provider: testProvider2, + }, { + Provider: testProvider3, + },{ + Provider: testProvider4, + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + if multiProvider.providersEntries[0].UniqueName != "InMemoryProvider" { + t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", multiProvider.providersEntries[0].UniqueName) + } + if multiProvider.providersEntries[1].UniqueName != "NoopProvider-1" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", multiProvider.providersEntries[1].UniqueName) + } + if multiProvider.providersEntries[2].UniqueName != "NoopProvider-2" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", multiProvider.providersEntries[2].UniqueName) + } + if multiProvider.providersEntries[3].UniqueName != "NoopProvider-3" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", multiProvider.providersEntries[3].UniqueName) + } +} From 7df8cba25edf1cc0b3af4b7f976c13b87d9d1171 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Sat, 1 Mar 2025 00:09:12 -0500 Subject: [PATCH 10/68] added test checking if error thrown for unique name if not unique Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../multi-provider/pkg/providers_test.go | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index ec4380d14..16c25b465 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -31,7 +31,7 @@ func TestNewMultiProvider_ProviderMetadataUniqueNames(t *testing.T) { multiProvider, err := NewMultiProvider([]UniqueNameProvider{ { - Provider: testProvider1, + Provider: testProvider1, }, { Provider: testProvider2, }, @@ -79,12 +79,12 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { multiProvider, err := NewMultiProvider([]UniqueNameProvider{ { - Provider: testProvider1, + Provider: testProvider1, }, { Provider: testProvider2, }, { Provider: testProvider3, - },{ + }, { Provider: testProvider4, }, }, "test", defaultLogger) @@ -112,22 +112,9 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { t.Errorf("Expected there to be 4 provider entries, got: '%d'", len(providerEntries)) } } -func TestNewMultiProvider_(t *testing.T) { - testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ - "boolFlag": { - Key: "boolFlag", - State: memprovider.Enabled, - DefaultVariant: "true", - Variants: map[string]interface{}{ - "true": true, - "false": false, - }, - ContextEvaluator: nil, - }, - }) +func TestNewMultiProvider_ProvidersUsePassedNames(t *testing.T) { + testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() - testProvider3 := oft.NewTestProvider() - testProvider4 := oft.NewTestProvider() defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -137,12 +124,10 @@ func TestNewMultiProvider_(t *testing.T) { multiProvider, err := NewMultiProvider([]UniqueNameProvider{ { Provider: testProvider1, + UniqueName: "theFirst", }, { - Provider: testProvider2, - }, { - Provider: testProvider3, - },{ - Provider: testProvider4, + Provider: testProvider2, + UniqueName: "theSecond", }, }, "test", defaultLogger) @@ -150,16 +135,44 @@ func TestNewMultiProvider_(t *testing.T) { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) } - if multiProvider.providersEntries[0].UniqueName != "InMemoryProvider" { - t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", multiProvider.providersEntries[0].UniqueName) + providerEntries := multiProvider.Providers() + + if providerEntries[0].UniqueName != "theFirst" { + t.Errorf("Expected unique provider name to be: 'theFirst', got: '%s'", providerEntries[0].UniqueName) } - if multiProvider.providersEntries[1].UniqueName != "NoopProvider-1" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", multiProvider.providersEntries[1].UniqueName) + if providerEntries[1].UniqueName != "theSecond" { + t.Errorf("Expected unique provider name to be: 'theSecond', got: '%s'", providerEntries[1].UniqueName) } - if multiProvider.providersEntries[2].UniqueName != "NoopProvider-2" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", multiProvider.providersEntries[2].UniqueName) + + if len(providerEntries) != 2 { + t.Errorf("Expected there to be 2 provider entries, got: '%d'", len(providerEntries)) } - if multiProvider.providersEntries[3].UniqueName != "NoopProvider-3" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", multiProvider.providersEntries[3].UniqueName) +} + +func TestNewMultiProvider_ProvidersErrorNameNotUnique(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + _, err = NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider", + }, { + Provider: testProvider2, + UniqueName: "provider", + }, + }, "test", defaultLogger) + + if err == nil { + t.Errorf("Expected the multiprovider to have an error") + } + + if err.Error() != "provider names must be unique" { + t.Errorf("Expected the multiprovider to have an error of: '%s', got: '%s'", errUniqueName, err.Error()) } } From dcbc5d723dbf5356eb3b249a6b6344796894684c Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Tue, 4 Mar 2025 13:04:43 -0500 Subject: [PATCH 11/68] added test to check for metadata, currently fails Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../multi-provider/pkg/providers_test.go | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 16c25b465..7440bb686 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -3,13 +3,13 @@ package multiprovider import ( "testing" - // "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" "github.com/open-feature/go-sdk/openfeature/memprovider" oft "github.com/open-feature/go-sdk/openfeature/testing" ) -func TestNewMultiProvider_ProviderMetadataUniqueNames(t *testing.T) { +func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ "boolFlag": { Key: "boolFlag", @@ -176,3 +176,41 @@ func TestNewMultiProvider_ProvidersErrorNameNotUnique(t *testing.T) { t.Errorf("Expected the multiprovider to have an error of: '%s', got: '%s'", errUniqueName, err.Error()) } } + +// todo: currently the `multiProvider.Metadata()` just give the `Name` of the multi provider it doesn't aggregate the passed providers as stated in this specification https://openfeature.dev/specification/appendix-a/#metadata so this test fails +func TestAggregatedMetaData(t *testing.T){ + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + multiProvider, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + expectedMetadata := MultiMetadata{ + Name: "multiprovider", + OriginalMetadata: map[string]openfeature.Metadata{ + "provider1": openfeature.Metadata{Name:"NoopProvider"}, + "provider2": openfeature.Metadata{Name:"NoopProvider"}, + + }, + } + + if multiProvider.Metadata().Name != "hi" { + t.Errorf("Expected to see the aggregated metadata of all passed providers: '%s', got: '%s'", expectedMetadata, multiProvider.Metadata().Name) + } +} \ No newline at end of file From f06e7455a0920f34e11491769846c63da503db20 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Wed, 5 Mar 2025 14:25:17 -0500 Subject: [PATCH 12/68] added test for getter methods of the struct provider entries Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/pkg/providers.go | 4 +- .../multi-provider/pkg/providers_test.go | 244 ++++++++++++++---- 2 files changed, 202 insertions(+), 46 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 8c83ad3eb..51ffa6863 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -59,8 +59,8 @@ func (mp *MultiProvider) Providers() []UniqueNameProvider { return mp.providersEntries } -func (mp *MultiProvider) ProvidersByName() []UniqueNameProvider { - return mp.providersEntries +func (mp *MultiProvider) ProvidersByName() map[string]UniqueNameProvider { + return mp.providersEntriesByName } func (mp *MultiProvider) ProviderByName(name string) (UniqueNameProvider, bool) { diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 7440bb686..ee7d792f4 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -9,6 +9,200 @@ import ( oft "github.com/open-feature/go-sdk/openfeature/testing" ) +func TestMultiProvider_ProvidersMethod(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providers := mp.Providers() + + if len(providers) != 2 { + t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) + } + + if providers[0].UniqueName != "provider1" { + t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", providers[0].UniqueName) + } + if providers[1].UniqueName != "provider2" { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", providers[1].UniqueName) + } +} + +func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providers := mp.ProvidersByName() + + if len(providers) != 2 { + t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) + } + + if provider, exists := providers["provider1"]; exists { + if provider.UniqueName != "provider1" { + t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", provider.UniqueName) + } + if provider.Provider != testProvider1 { + t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", provider.UniqueName) + } + } else { + t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider1") + } + + if provider, exists := providers["provider2"]; exists { + if provider.UniqueName != "provider2" { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + if provider.Provider != testProvider2 { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + } else { + t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider2") + } + +} + +func TestMultiProvider_ProviderByNameMethod(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + providers := mp.ProvidersByName() + + if len(providers) != 2 { + t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) + } + if provider, exists := mp.ProviderByName("provider2"); exists { + if provider.UniqueName != "provider2" { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + if provider.Provider != testProvider2 { + t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) + } + } else { + t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider1") + } + +} + +// todo: currently the `multiProvider.Metadata()` just give the `Name` of the multi provider it doesn't aggregate the passed providers as stated in this specification https://openfeature.dev/specification/appendix-a/#metadata so this test fails +func TestMultiProvider_MetaData(t *testing.T) { + testProvider1 := oft.NewTestProvider() + testProvider2 := oft.NewTestProvider() + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + expectedMetadata := MultiMetadata{ + Name: "multiprovider", + OriginalMetadata: map[string]openfeature.Metadata{ + "provider1": openfeature.Metadata{Name: "NoopProvider"}, + "provider2": openfeature.Metadata{Name: "NoopProvider"}, + }, + } + + if mp.Metadata().Name != "hi" { + t.Errorf("Expected to see the aggregated metadata of all passed providers: '%s', got: '%s'", expectedMetadata, mp.Metadata().Name) + } +} + +// func TestMultiProvider_Init(t *testing.T) { +// testProvider1 := oft.NewTestProvider() +// testProvider2 := oft.NewTestProvider() + +// defaultLogger, err := hooks.NewLoggingHook(false) +// if err != nil { +// t.Errorf("Issue setting up logger,'%s'", err) +// } + +// mp, err := NewMultiProvider([]UniqueNameProvider{ +// { +// Provider: testProvider1, +// UniqueName: "provider1", +// }, { +// Provider: testProvider2, +// UniqueName: "provider2", +// }, +// }, "test", defaultLogger) + +// if err != nil { +// t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) +// } + +// mp.Init() +// } + func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ "boolFlag": { @@ -29,7 +223,7 @@ func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { t.Errorf("Issue setting up logger,'%s'", err) } - multiProvider, err := NewMultiProvider([]UniqueNameProvider{ + mp, err := NewMultiProvider([]UniqueNameProvider{ { Provider: testProvider1, }, { @@ -41,7 +235,7 @@ func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) } - providerEntries := multiProvider.Providers() + providerEntries := mp.Providers() if providerEntries[0].UniqueName != "InMemoryProvider" { t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", providerEntries[0].UniqueName) @@ -77,7 +271,7 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { t.Errorf("Issue setting up logger,'%s'", err) } - multiProvider, err := NewMultiProvider([]UniqueNameProvider{ + mp, err := NewMultiProvider([]UniqueNameProvider{ { Provider: testProvider1, }, { @@ -93,7 +287,7 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) } - providerEntries := multiProvider.Providers() + providerEntries := mp.Providers() if providerEntries[0].UniqueName != "InMemoryProvider" { t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", providerEntries[0].UniqueName) @@ -121,7 +315,7 @@ func TestNewMultiProvider_ProvidersUsePassedNames(t *testing.T) { t.Errorf("Issue setting up logger,'%s'", err) } - multiProvider, err := NewMultiProvider([]UniqueNameProvider{ + mp, err := NewMultiProvider([]UniqueNameProvider{ { Provider: testProvider1, UniqueName: "theFirst", @@ -135,7 +329,7 @@ func TestNewMultiProvider_ProvidersUsePassedNames(t *testing.T) { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) } - providerEntries := multiProvider.Providers() + providerEntries := mp.Providers() if providerEntries[0].UniqueName != "theFirst" { t.Errorf("Expected unique provider name to be: 'theFirst', got: '%s'", providerEntries[0].UniqueName) @@ -176,41 +370,3 @@ func TestNewMultiProvider_ProvidersErrorNameNotUnique(t *testing.T) { t.Errorf("Expected the multiprovider to have an error of: '%s', got: '%s'", errUniqueName, err.Error()) } } - -// todo: currently the `multiProvider.Metadata()` just give the `Name` of the multi provider it doesn't aggregate the passed providers as stated in this specification https://openfeature.dev/specification/appendix-a/#metadata so this test fails -func TestAggregatedMetaData(t *testing.T){ - testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() - - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - multiProvider, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, - }, "test", defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } - - expectedMetadata := MultiMetadata{ - Name: "multiprovider", - OriginalMetadata: map[string]openfeature.Metadata{ - "provider1": openfeature.Metadata{Name:"NoopProvider"}, - "provider2": openfeature.Metadata{Name:"NoopProvider"}, - - }, - } - - if multiProvider.Metadata().Name != "hi" { - t.Errorf("Expected to see the aggregated metadata of all passed providers: '%s', got: '%s'", expectedMetadata, multiProvider.Metadata().Name) - } -} \ No newline at end of file From 676bcee4fa8950cba37f12d1deb2fd83839247ba Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:16:38 -0500 Subject: [PATCH 13/68] added test for MP Init method with mock providers that have Init methods Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .release-please-manifest.json | 2 +- .../multi-provider/pkg/providers_test.go | 119 ++++++++++++++---- 2 files changed, 94 insertions(+), 27 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index fc12117e5..23e4291d1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -16,5 +16,5 @@ "providers/prefab": "0.0.2", "tests/flagd": "1.4.1", "providers/go-feature-flag-in-process": "0.1.0", - "providers/multi-provider": "0.0.1" + "providers/multi-provider": "0.0.2" } diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index ee7d792f4..1d9b08119 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -2,6 +2,7 @@ package multiprovider import ( "testing" + "time" "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" @@ -9,6 +10,49 @@ import ( oft "github.com/open-feature/go-sdk/openfeature/testing" ) +// MockProvider utilizes openfeature's TestProvider to add testable Init & Shutdown methods to test the MultiProvider functionality +type MockProvider struct { + oft.TestProvider + InitCount *int +} + +func (m *MockProvider) Init(evalCtx openfeature.EvaluationContext) error { + *m.InitCount += 1 + return nil +} + +func (m *MockProvider) Shutdown() { +} + +func NewMockProvider(initCount *int) *MockProvider { + return &MockProvider{ + TestProvider: oft.NewTestProvider(), + InitCount: initCount, + } +} + +// MockProviderDelay utilizes openfeature's TestProvider to add testable Init & Shutdown methods to test the MultiProvider functionality with a small delay making sure the the go routines properly wait. +type MockProviderDelay struct { + oft.TestProvider + InitCount *int +} + +func (m *MockProviderDelay) Init(evalCtx openfeature.EvaluationContext) error { + time.Sleep(1 * time.Millisecond) + *m.InitCount += 1 + return nil +} + +func (m *MockProviderDelay) Shutdown() { +} + +func NewMockProviderDelay(initCount *int) *MockProviderDelay { + return &MockProviderDelay{ + TestProvider: oft.NewTestProvider(), + InitCount: initCount, + } +} + func TestMultiProvider_ProvidersMethod(t *testing.T) { testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() @@ -123,7 +167,7 @@ func TestMultiProvider_ProviderByNameMethod(t *testing.T) { } providers := mp.ProvidersByName() - + if len(providers) != 2 { t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) } @@ -177,31 +221,54 @@ func TestMultiProvider_MetaData(t *testing.T) { } } -// func TestMultiProvider_Init(t *testing.T) { -// testProvider1 := oft.NewTestProvider() -// testProvider2 := oft.NewTestProvider() - -// defaultLogger, err := hooks.NewLoggingHook(false) -// if err != nil { -// t.Errorf("Issue setting up logger,'%s'", err) -// } - -// mp, err := NewMultiProvider([]UniqueNameProvider{ -// { -// Provider: testProvider1, -// UniqueName: "provider1", -// }, { -// Provider: testProvider2, -// UniqueName: "provider2", -// }, -// }, "test", defaultLogger) - -// if err != nil { -// t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) -// } - -// mp.Init() -// } +func TestMultiProvider_Init(t *testing.T) { + initializations := 0 + + testProvider1 := NewMockProvider(&initializations) + testProvider2 := oft.NewTestProvider() + testProvider3 := NewMockProviderDelay(&initializations) + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + },{ + Provider: testProvider3, + UniqueName: "provider3", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + attributes := map[string]interface{}{ + "foo": "bar", + } + evalCtx := openfeature.NewTargetlessEvaluationContext(attributes) + + err = mp.Init(evalCtx) + if err != nil { + t.Errorf("Expected the initialization process to be successful, got error: '%s'", err) + } + + if initializations == 0 { + t.Errorf("Expected there to be initializations, but none were ran.") + } + + if initializations != 2 { + t.Errorf("Expected there to be '2' init steps ran, but got: '%d'.", initializations) + } + +} func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ From 463478934db7335970292c802436f44079d92bd0 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:57:24 -0500 Subject: [PATCH 14/68] added shutdown test confirming method run in MP when called Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../multi-provider/pkg/providers_test.go | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 1d9b08119..12cc2d952 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -14,6 +14,7 @@ import ( type MockProvider struct { oft.TestProvider InitCount *int + ShutCount *int } func (m *MockProvider) Init(evalCtx openfeature.EvaluationContext) error { @@ -22,12 +23,14 @@ func (m *MockProvider) Init(evalCtx openfeature.EvaluationContext) error { } func (m *MockProvider) Shutdown() { + *m.ShutCount += 1 } -func NewMockProvider(initCount *int) *MockProvider { +func NewMockProvider(initCount *int, shutCount *int) *MockProvider { return &MockProvider{ TestProvider: oft.NewTestProvider(), - InitCount: initCount, + InitCount: initCount, + ShutCount: shutCount, } } @@ -35,6 +38,7 @@ func NewMockProvider(initCount *int) *MockProvider { type MockProviderDelay struct { oft.TestProvider InitCount *int + ShutCount *int } func (m *MockProviderDelay) Init(evalCtx openfeature.EvaluationContext) error { @@ -44,12 +48,15 @@ func (m *MockProviderDelay) Init(evalCtx openfeature.EvaluationContext) error { } func (m *MockProviderDelay) Shutdown() { + time.Sleep(2 * time.Millisecond) + *m.ShutCount += 1 } -func NewMockProviderDelay(initCount *int) *MockProviderDelay { +func NewMockProviderDelay(initCount *int, shutCount *int) *MockProviderDelay { return &MockProviderDelay{ TestProvider: oft.NewTestProvider(), - InitCount: initCount, + InitCount: initCount, + ShutCount: shutCount, } } @@ -223,10 +230,11 @@ func TestMultiProvider_MetaData(t *testing.T) { func TestMultiProvider_Init(t *testing.T) { initializations := 0 + shutdowns := 0 - testProvider1 := NewMockProvider(&initializations) + testProvider1 := NewMockProvider(&initializations, &shutdowns) testProvider2 := oft.NewTestProvider() - testProvider3 := NewMockProviderDelay(&initializations) + testProvider3 := NewMockProviderDelay(&initializations, &shutdowns) defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -240,7 +248,7 @@ func TestMultiProvider_Init(t *testing.T) { }, { Provider: testProvider2, UniqueName: "provider2", - },{ + }, { Provider: testProvider3, UniqueName: "provider3", }, @@ -270,6 +278,47 @@ func TestMultiProvider_Init(t *testing.T) { } +func TestMultiProvider_Shutdown(t *testing.T) { + initializations := 0 + shutdowns := 0 + + testProvider1 := NewMockProvider(&initializations, &shutdowns) + testProvider2 := oft.NewTestProvider() + testProvider3 := NewMockProviderDelay(&initializations, &shutdowns) + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, { + Provider: testProvider3, + UniqueName: "provider3", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + mp.Shutdown() + + if shutdowns == 0 { + t.Errorf("Expected there to be shutdowns, but none were ran.") + } + + if shutdowns != 2 { + t.Errorf("Expected there to be '2' shutdown steps ran, but got: '%d'.", shutdowns) + } +} + func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ "boolFlag": { From af137363b554cf7114d4b097e1c583d08b524c1d Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Sat, 8 Mar 2025 23:33:14 -0500 Subject: [PATCH 15/68] added the empty bases of the evaluations and hooks methods Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/pkg/providers.go | 49 ++++++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 51ffa6863..d35377cf8 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -1,6 +1,7 @@ package multiprovider import ( + "context" "errors" "fmt" "sync" @@ -68,12 +69,6 @@ func (mp *MultiProvider) ProviderByName(name string) (UniqueNameProvider, bool) return provider, exists } -// Metadata provides the name `multiprovider` and the names of each provider passed. -func (mp *MultiProvider) Metadata() openfeature.Metadata { - - return openfeature.Metadata{Name: "multiprovider"} -} - // registerProviders ensures that when setting up an instant of MultiProvider the providers provided either have a unique name or the base `metadata.Name` is made unique by adding an indexed based number to it. // registerProviders also stores the providers by their unique name and in an array for easy usage. func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error { @@ -112,6 +107,48 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error return nil } +// Metadata provides the name `multiprovider` and the names of each provider passed. +func (mp *MultiProvider) Metadata() openfeature.Metadata { + + return openfeature.Metadata{Name: "multiprovider"} +} + +// Hooks returns a collection of openfeature.Hook defined by this provider +func (mp *MultiProvider) Hooks() []openfeature.Hook { + // Hooks that should be included with the provider + return []openfeature.Hook{} +} + +// BooleanEvaluation returns a boolean flag +func (mp *MultiProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { + // code to evaluate boolean + return openfeature.BoolResolutionDetail{} +} + +// StringEvaluation returns a string flag +func (mp *MultiProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { + // code to evaluate string + return openfeature.StringResolutionDetail{} +} + +// FloatEvaluation returns a float flag +func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { + // code to evaluate float + return openfeature.FloatResolutionDetail{} +} + +// IntEvaluation returns an int flag +func (mp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { + // code to evaluate int + return openfeature.IntResolutionDetail{} +} + +// ObjectEvaluation returns an object flag +func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { + // code to evaluate object + return openfeature.InterfaceResolutionDetail{} +} + // Init will run the initialize method for all of provides and aggregate the errors. func (mp *MultiProvider) Init(evalCtx openfeature.EvaluationContext) error { var wg sync.WaitGroup From fcc1f7c15d1abf05c9aafd03954f37594a6c72ed Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Sun, 9 Mar 2025 04:33:12 -0400 Subject: [PATCH 16/68] added test for init errors by a provider and reduced the mock provider struct made Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../internal/aggregate-errors.go | 21 ++- providers/multi-provider/pkg/providers.go | 2 +- .../multi-provider/pkg/providers_test.go | 133 +++++++++++++----- 3 files changed, 114 insertions(+), 42 deletions(-) diff --git a/providers/multi-provider/internal/aggregate-errors.go b/providers/multi-provider/internal/aggregate-errors.go index e974e9c05..4d1808218 100644 --- a/providers/multi-provider/internal/aggregate-errors.go +++ b/providers/multi-provider/internal/aggregate-errors.go @@ -1,11 +1,15 @@ package internal -import "fmt" +import ( + "encoding/json" + "fmt" +) -// StateErr is how the error in the Init of Shutdown stage of a provider is reported. +// StateErr is how the error in the Init stage of a provider is reported. type StateErr struct { - ProviderName string - Err error + ProviderName string `json:"source"` + Err error `json:"-"` + ErrMessage string `json:"error"` } func (e *StateErr) Error() string { @@ -13,12 +17,15 @@ func (e *StateErr) Error() string { } type AggregateError struct { - Message string - Errors []StateErr + Message string `json:"message"` + Errors []StateErr `json:"errors"` } func (ae *AggregateError) Error() string { - return ae.Message + errorsJSON, _ := json.Marshal(ae.Errors) + + return fmt.Sprintf("%s\n%s", ae.Message, string(errorsJSON)) + } func (ae *AggregateError) Construct(providerErrors []StateErr) { diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index d35377cf8..17673dab0 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -160,7 +160,7 @@ func (mp *MultiProvider) Init(evalCtx openfeature.EvaluationContext) error { defer wg.Done() if stateHandle, ok := p.Provider.(openfeature.StateHandler); ok { if initErr := stateHandle.Init(evalCtx); initErr != nil { - errChan <- err.StateErr{ProviderName: p.UniqueName, Err: initErr} + errChan <- err.StateErr{ProviderName: p.UniqueName, Err: initErr, ErrMessage: initErr.Error()} } } }(provider) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 12cc2d952..ae879c316 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -1,9 +1,14 @@ package multiprovider import ( + "encoding/json" + "fmt" + "strings" "testing" "time" + errs "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" + "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" "github.com/open-feature/go-sdk/openfeature/memprovider" @@ -15,48 +20,38 @@ type MockProvider struct { oft.TestProvider InitCount *int ShutCount *int + TestErr string + InitDelay int + ShutDelay int } func (m *MockProvider) Init(evalCtx openfeature.EvaluationContext) error { - *m.InitCount += 1 - return nil -} - -func (m *MockProvider) Shutdown() { - *m.ShutCount += 1 -} - -func NewMockProvider(initCount *int, shutCount *int) *MockProvider { - return &MockProvider{ - TestProvider: oft.NewTestProvider(), - InitCount: initCount, - ShutCount: shutCount, + if m.TestErr != "" { + return fmt.Errorf(m.TestErr) } -} - -// MockProviderDelay utilizes openfeature's TestProvider to add testable Init & Shutdown methods to test the MultiProvider functionality with a small delay making sure the the go routines properly wait. -type MockProviderDelay struct { - oft.TestProvider - InitCount *int - ShutCount *int -} -func (m *MockProviderDelay) Init(evalCtx openfeature.EvaluationContext) error { - time.Sleep(1 * time.Millisecond) + if m.InitDelay != 0 { + time.Sleep(time.Duration(m.InitDelay) * time.Millisecond) + } *m.InitCount += 1 return nil } -func (m *MockProviderDelay) Shutdown() { - time.Sleep(2 * time.Millisecond) +func (m *MockProvider) Shutdown() { + if m.ShutDelay != 0 { + time.Sleep(time.Duration(m.ShutDelay) * time.Millisecond) + } *m.ShutCount += 1 } -func NewMockProviderDelay(initCount *int, shutCount *int) *MockProviderDelay { - return &MockProviderDelay{ +func NewMockProvider(initCount *int, shutCount *int, testErr string, initDelay int, shutDelay int) *MockProvider { + return &MockProvider{ TestProvider: oft.NewTestProvider(), InitCount: initCount, ShutCount: shutCount, + TestErr: testErr, + InitDelay: initDelay, + ShutDelay: shutDelay, } } @@ -232,9 +227,9 @@ func TestMultiProvider_Init(t *testing.T) { initializations := 0 shutdowns := 0 - testProvider1 := NewMockProvider(&initializations, &shutdowns) + testProvider1 := NewMockProvider(&initializations, &shutdowns,"", 0, 0) testProvider2 := oft.NewTestProvider() - testProvider3 := NewMockProviderDelay(&initializations, &shutdowns) + testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 1, 0) defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -278,13 +273,82 @@ func TestMultiProvider_Init(t *testing.T) { } +func TestMultiProvider_InitErrorWithProvider(t *testing.T) { + initializations := 0 + shutdowns := 0 + + testProvider1 := oft.NewTestProvider() + testProvider2 := NewMockProvider(&initializations, &shutdowns, "test error 1 end", 0, 0) + testProvider3 := NewMockProvider(&initializations, &shutdowns, "test error 2 end", 0, 0) + + defaultLogger, err := hooks.NewLoggingHook(false) + if err != nil { + t.Errorf("Issue setting up logger,'%s'", err) + } + + mp, err := NewMultiProvider([]UniqueNameProvider{ + { + Provider: testProvider1, + UniqueName: "provider1", + }, { + Provider: testProvider2, + UniqueName: "provider2", + }, { + Provider: testProvider3, + UniqueName: "provider3", + }, + }, "test", defaultLogger) + + if err != nil { + t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) + } + + attributes := map[string]interface{}{ + "foo": "bar", + } + evalCtx := openfeature.NewTargetlessEvaluationContext(attributes) + + err = mp.Init(evalCtx) + if err == nil { + t.Errorf("Expected the initialization process to throw an error.") + } + + var errors []errs.StateErr + + fullErr := err.Error() + fullErrArr := strings.SplitAfterN(fullErr, "end", 2) + errJSON := fullErrArr[1] + errMsg := fullErrArr[0] + + if !strings.Contains(errMsg, "Provider errors occurred:") { + t.Errorf("Expected the first line of error message to contain: '%s', got: '%s'", "Provider errors occurred:", errMsg) + } + + if err = json.Unmarshal([]byte(errJSON), &errors); err != nil { + t.Errorf("Failed to unmarshal error details: %v", err) + } + + if len(errors) != 2 { + t.Errorf("Expected there to be '2' errors found, got: '%d'", len(errors)) + } + + // if errors[0].ProviderName != "provider2" || errors[0].ErrMessage != "test error 1 end" { + // t.Errorf("Expected the first error to be for 'provider2' with 'test error 1 end', got: '%s' with '%s'", errors[0].ProviderName, errors[0].ErrMessage) + // } + + // if errors[1].ProviderName != "provider3" || errors[1].ErrMessage != "test error 1 end" { + // t.Errorf("Expected the second error to be for 'provider3' with 'test error 2 end', got: '%s' with '%s'", errors[1].ProviderName, errors[1].ErrMessage) + // } + +} + func TestMultiProvider_Shutdown(t *testing.T) { initializations := 0 shutdowns := 0 - testProvider1 := NewMockProvider(&initializations, &shutdowns) + testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0) testProvider2 := oft.NewTestProvider() - testProvider3 := NewMockProviderDelay(&initializations, &shutdowns) + testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 0, 2) defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -405,6 +469,10 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { providerEntries := mp.Providers() + if len(providerEntries) != 4 { + t.Errorf("Expected there to be 4 provider entries, got: '%d'", len(providerEntries)) + } + if providerEntries[0].UniqueName != "InMemoryProvider" { t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", providerEntries[0].UniqueName) } @@ -418,9 +486,6 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[3].UniqueName) } - if len(providerEntries) != 4 { - t.Errorf("Expected there to be 4 provider entries, got: '%d'", len(providerEntries)) - } } func TestNewMultiProvider_ProvidersUsePassedNames(t *testing.T) { testProvider1 := oft.NewTestProvider() From dc539b9227d096b00aff6de2125225f72067e733 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Sun, 9 Mar 2025 04:57:04 -0400 Subject: [PATCH 17/68] removed memprovider since NewTestProvider uses it Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../multi-provider/pkg/providers_test.go | 84 ++++++++----------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index ae879c316..c9794128e 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -11,7 +11,6 @@ import ( "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" - "github.com/open-feature/go-sdk/openfeature/memprovider" oft "github.com/open-feature/go-sdk/openfeature/testing" ) @@ -23,6 +22,7 @@ type MockProvider struct { TestErr string InitDelay int ShutDelay int + MockMeta string } func (m *MockProvider) Init(evalCtx openfeature.EvaluationContext) error { @@ -44,7 +44,11 @@ func (m *MockProvider) Shutdown() { *m.ShutCount += 1 } -func NewMockProvider(initCount *int, shutCount *int, testErr string, initDelay int, shutDelay int) *MockProvider { +func (m *MockProvider) Metadata() openfeature.Metadata { + return openfeature.Metadata{Name: m.MockMeta} +} + +func NewMockProvider(initCount *int, shutCount *int, testErr string, initDelay int, shutDelay int, meta string) *MockProvider { return &MockProvider{ TestProvider: oft.NewTestProvider(), InitCount: initCount, @@ -52,6 +56,7 @@ func NewMockProvider(initCount *int, shutCount *int, testErr string, initDelay i TestErr: testErr, InitDelay: initDelay, ShutDelay: shutDelay, + MockMeta: meta, } } @@ -188,8 +193,11 @@ func TestMultiProvider_ProviderByNameMethod(t *testing.T) { // todo: currently the `multiProvider.Metadata()` just give the `Name` of the multi provider it doesn't aggregate the passed providers as stated in this specification https://openfeature.dev/specification/appendix-a/#metadata so this test fails func TestMultiProvider_MetaData(t *testing.T) { + initializations := 0 + shutdowns := 0 + testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() + testProvider2 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test2") defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -214,7 +222,7 @@ func TestMultiProvider_MetaData(t *testing.T) { Name: "multiprovider", OriginalMetadata: map[string]openfeature.Metadata{ "provider1": openfeature.Metadata{Name: "NoopProvider"}, - "provider2": openfeature.Metadata{Name: "NoopProvider"}, + "provider2": openfeature.Metadata{Name: "test2"}, }, } @@ -227,9 +235,9 @@ func TestMultiProvider_Init(t *testing.T) { initializations := 0 shutdowns := 0 - testProvider1 := NewMockProvider(&initializations, &shutdowns,"", 0, 0) + testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test1") testProvider2 := oft.NewTestProvider() - testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 1, 0) + testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 1, 0, "test3") defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -278,8 +286,8 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { shutdowns := 0 testProvider1 := oft.NewTestProvider() - testProvider2 := NewMockProvider(&initializations, &shutdowns, "test error 1 end", 0, 0) - testProvider3 := NewMockProvider(&initializations, &shutdowns, "test error 2 end", 0, 0) + testProvider2 := NewMockProvider(&initializations, &shutdowns, "test error 1 end", 0, 0, "test2") + testProvider3 := NewMockProvider(&initializations, &shutdowns, "test error 2 end", 0, 0, "test3") defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -346,9 +354,9 @@ func TestMultiProvider_Shutdown(t *testing.T) { initializations := 0 shutdowns := 0 - testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0) + testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test1") testProvider2 := oft.NewTestProvider() - testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 0, 2) + testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 0, 2, "test3") defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -384,19 +392,11 @@ func TestMultiProvider_Shutdown(t *testing.T) { } func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { - testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ - "boolFlag": { - Key: "boolFlag", - State: memprovider.Enabled, - DefaultVariant: "true", - Variants: map[string]interface{}{ - "true": true, - "false": false, - }, - ContextEvaluator: nil, - }, - }) - testProvider2 := oft.NewTestProvider() + initializations := 0 + shutdowns := 0 + + testProvider1 := oft.NewTestProvider() + testProvider2 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test2") defaultLogger, err := hooks.NewLoggingHook(false) if err != nil { @@ -417,11 +417,12 @@ func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { providerEntries := mp.Providers() - if providerEntries[0].UniqueName != "InMemoryProvider" { - t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", providerEntries[0].UniqueName) + if providerEntries[0].UniqueName != "NoopProvider" { + t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[0].UniqueName) } - if providerEntries[1].UniqueName != "NoopProvider" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[1].UniqueName) + + if providerEntries[1].UniqueName != "test2" { + t.Errorf("Expected unique provider name to be: 'test2', got: '%s'", providerEntries[1].UniqueName) } if len(providerEntries) != 2 { @@ -430,18 +431,7 @@ func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { } func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { - testProvider1 := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{ - "boolFlag": { - Key: "boolFlag", - State: memprovider.Enabled, - DefaultVariant: "true", - Variants: map[string]interface{}{ - "true": true, - "false": false, - }, - ContextEvaluator: nil, - }, - }) + testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() testProvider3 := oft.NewTestProvider() testProvider4 := oft.NewTestProvider() @@ -473,17 +463,17 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { t.Errorf("Expected there to be 4 provider entries, got: '%d'", len(providerEntries)) } - if providerEntries[0].UniqueName != "InMemoryProvider" { - t.Errorf("Expected unique provider name to be: 'InMemoryProvider', got: '%s'", providerEntries[0].UniqueName) + if providerEntries[0].UniqueName != "NoopProvider-1" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-1', got: '%s'", providerEntries[0].UniqueName) } - if providerEntries[1].UniqueName != "NoopProvider-1" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[1].UniqueName) + if providerEntries[1].UniqueName != "NoopProvider-2" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-2', got: '%s'", providerEntries[1].UniqueName) } - if providerEntries[2].UniqueName != "NoopProvider-2" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[2].UniqueName) + if providerEntries[2].UniqueName != "NoopProvider-3" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-3', got: '%s'", providerEntries[2].UniqueName) } - if providerEntries[3].UniqueName != "NoopProvider-3" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[3].UniqueName) + if providerEntries[3].UniqueName != "NoopProvider-4" { + t.Errorf("Expected unique provider name to be: 'NoopProvider-4', got: '%s'", providerEntries[3].UniqueName) } } From 2b926df3c0afb535d73b0c801e1157ba56ed8d6f Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Sun, 9 Mar 2025 05:52:00 -0400 Subject: [PATCH 18/68] added metadata aggregate to the method and updated test to properly use Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .../internal/aggregate-errors.go | 5 ++++- providers/multi-provider/pkg/providers.go | 19 ++++++++++++------- .../multi-provider/pkg/providers_test.go | 19 ++++++++----------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/providers/multi-provider/internal/aggregate-errors.go b/providers/multi-provider/internal/aggregate-errors.go index 4d1808218..5b7ccbcf0 100644 --- a/providers/multi-provider/internal/aggregate-errors.go +++ b/providers/multi-provider/internal/aggregate-errors.go @@ -22,7 +22,10 @@ type AggregateError struct { } func (ae *AggregateError) Error() string { - errorsJSON, _ := json.Marshal(ae.Errors) + errorsJSON, err := json.Marshal(ae.Errors) + if err != nil { + return fmt.Sprintf("Error in json marshal of errors, %s", err) + } return fmt.Sprintf("%s\n%s", ae.Message, string(errorsJSON)) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 17673dab0..5b8b3363f 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -2,6 +2,7 @@ package multiprovider import ( "context" + "encoding/json" "errors" "fmt" "sync" @@ -25,15 +26,15 @@ type UniqueNameProvider struct { // MultiMetadata defines the return of the MultiProvider metadata with the aggregated data of all the providers. type MultiMetadata struct { - Name string - OriginalMetadata map[string]openfeature.Metadata + Name string `json:"name"` + OriginalMetadata map[string]openfeature.Metadata `json:"originalMetadata"` } // MultiProvider implements openfeature `FeatureProvider` in a way to accept an array of providers. type MultiProvider struct { providersEntries []UniqueNameProvider providersEntriesByName map[string]UniqueNameProvider - AggregatedMetadata map[string]openfeature.Metadata + AggregatedMetadata MultiMetadata EvaluationStrategy string events chan openfeature.Event status openfeature.State @@ -45,7 +46,10 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, - AggregatedMetadata: map[string]openfeature.Metadata{}, + AggregatedMetadata: MultiMetadata{ + Name: "multiprovider", + OriginalMetadata: map[string]openfeature.Metadata{}, + }, } err := registerProviders(multiProvider, passedProviders) @@ -93,14 +97,14 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error providers[0].UniqueName = name mp.providersEntries = append(mp.providersEntries, providers[0]) mp.providersEntriesByName[name] = providers[0] - mp.AggregatedMetadata[name] = providers[0].Provider.Metadata() + mp.AggregatedMetadata.OriginalMetadata[name] = providers[0].Provider.Metadata() } else { for i, provider := range providers { uniqueName := fmt.Sprintf("%s-%d", name, i+1) provider.UniqueName = uniqueName mp.providersEntries = append(mp.providersEntries, provider) mp.providersEntriesByName[uniqueName] = provider - mp.AggregatedMetadata[uniqueName] = provider.Provider.Metadata() + mp.AggregatedMetadata.OriginalMetadata[uniqueName] = provider.Provider.Metadata() } } } @@ -109,8 +113,9 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error // Metadata provides the name `multiprovider` and the names of each provider passed. func (mp *MultiProvider) Metadata() openfeature.Metadata { + metaJSON, _ := json.Marshal(mp.AggregatedMetadata) - return openfeature.Metadata{Name: "multiprovider"} + return openfeature.Metadata{Name: string(metaJSON)} } // Hooks returns a collection of openfeature.Hook defined by this provider diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index c9794128e..16d7117e7 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -218,16 +218,21 @@ func TestMultiProvider_MetaData(t *testing.T) { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) } - expectedMetadata := MultiMetadata{ + expectedJSON, err := json.Marshal(MultiMetadata{ Name: "multiprovider", OriginalMetadata: map[string]openfeature.Metadata{ "provider1": openfeature.Metadata{Name: "NoopProvider"}, "provider2": openfeature.Metadata{Name: "test2"}, }, + }) + if err != nil { + t.Errorf("Error in JSON marshal of the expected answer, '%s'", err) } - if mp.Metadata().Name != "hi" { - t.Errorf("Expected to see the aggregated metadata of all passed providers: '%s', got: '%s'", expectedMetadata, mp.Metadata().Name) + expectedMetadata := openfeature.Metadata{Name: string(expectedJSON)} + + if mp.Metadata() != expectedMetadata { + t.Errorf("Expected to see the aggregated metadata of all passed providers: '%s', got: '%s'", expectedMetadata, mp.Metadata()) } } @@ -340,14 +345,6 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { t.Errorf("Expected there to be '2' errors found, got: '%d'", len(errors)) } - // if errors[0].ProviderName != "provider2" || errors[0].ErrMessage != "test error 1 end" { - // t.Errorf("Expected the first error to be for 'provider2' with 'test error 1 end', got: '%s' with '%s'", errors[0].ProviderName, errors[0].ErrMessage) - // } - - // if errors[1].ProviderName != "provider3" || errors[1].ErrMessage != "test error 1 end" { - // t.Errorf("Expected the second error to be for 'provider3' with 'test error 2 end', got: '%s' with '%s'", errors[1].ProviderName, errors[1].ErrMessage) - // } - } func TestMultiProvider_Shutdown(t *testing.T) { From b0f777dea28ddde31b131d9e4e3e36d14f9c240a Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:48:44 -0400 Subject: [PATCH 19/68] added ready and error state to the init method for the multi provider Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/pkg/providers.go | 8 +++++--- providers/multi-provider/pkg/providers_test.go | 8 +++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 5b8b3363f..560e8a718 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -184,14 +184,17 @@ func (mp *MultiProvider) Init(evalCtx openfeature.EvaluationContext) error { if len(errors) > 0 { var aggErr err.AggregateError aggErr.Construct(errors) + mp.status = openfeature.ErrorState return &aggErr } + mp.status = openfeature.ReadyState + return nil } func (mp *MultiProvider) Status() openfeature.State { - return openfeature.ReadyState + return mp.status } func (mp *MultiProvider) Shutdown() { @@ -211,6 +214,5 @@ func (mp *MultiProvider) Shutdown() { } func (mp *MultiProvider) EventChannel() <-chan openfeature.Event { - ev := make(chan openfeature.Event) - return ev + return mp.events } diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 16d7117e7..0f691565b 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -191,7 +191,6 @@ func TestMultiProvider_ProviderByNameMethod(t *testing.T) { } -// todo: currently the `multiProvider.Metadata()` just give the `Name` of the multi provider it doesn't aggregate the passed providers as stated in this specification https://openfeature.dev/specification/appendix-a/#metadata so this test fails func TestMultiProvider_MetaData(t *testing.T) { initializations := 0 shutdowns := 0 @@ -276,6 +275,10 @@ func TestMultiProvider_Init(t *testing.T) { t.Errorf("Expected the initialization process to be successful, got error: '%s'", err) } + if mp.status != openfeature.ReadyState { + t.Errorf("Expected the state of the multiprovider to be in 'Error', got: '%s'", mp.status) + } + if initializations == 0 { t.Errorf("Expected there to be initializations, but none were ran.") } @@ -345,6 +348,9 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { t.Errorf("Expected there to be '2' errors found, got: '%d'", len(errors)) } + if mp.status != openfeature.ErrorState { + t.Errorf("Expected the state of the multiprovider to be in 'Error', got: '%s'", mp.status) + } } func TestMultiProvider_Shutdown(t *testing.T) { From 89f1f8d68b6c43ffd6bb8a04c7f0f1fae35186c9 Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Wed, 9 Apr 2025 15:53:32 -0400 Subject: [PATCH 20/68] added a func to determine which eval strategy to use and added it to where it should be when strategies pushed Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/pkg/providers.go | 43 ++++++++++++++++------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 560e8a718..4d1719a99 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -8,6 +8,7 @@ import ( "sync" err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" + strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" @@ -35,7 +36,7 @@ type MultiProvider struct { providersEntries []UniqueNameProvider providersEntriesByName map[string]UniqueNameProvider AggregatedMetadata MultiMetadata - EvaluationStrategy string + EvaluationStrategy strategies.Strategy events chan openfeature.Event status openfeature.State mu sync.Mutex @@ -52,11 +53,13 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s }, } - err := registerProviders(multiProvider, passedProviders) + err := multiProvider.registerProviders(passedProviders) if err != nil { return nil, err } + multiProvider.EvaluationStrategy = multiProvider.strategyMethod(evaluationStrategy, multiProvider.providersEntries) + return multiProvider, nil } @@ -75,7 +78,7 @@ func (mp *MultiProvider) ProviderByName(name string) (UniqueNameProvider, bool) // registerProviders ensures that when setting up an instant of MultiProvider the providers provided either have a unique name or the base `metadata.Name` is made unique by adding an indexed based number to it. // registerProviders also stores the providers by their unique name and in an array for easy usage. -func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error { +func (mp *MultiProvider) registerProviders(providers []UniqueNameProvider) error { providersByName := make(map[string][]UniqueNameProvider) for _, provider := range providers { @@ -111,6 +114,20 @@ func registerProviders(mp *MultiProvider, providers []UniqueNameProvider) error return nil } +// strategyMethod determines the strategy for the evaluation of the flags in the providers based on the passed value in the set up +func (mp *MultiProvider) strategyMethod(name string, providers []UniqueNameProvider) strategies.Strategy { + switch name { + case string(strategies.StrategyFirstMatch): + return strategies.strategies.NewFirstMatchStrategy(providers) + case string(strategies.StrategyFirstSuccess): + return strategies.strategies.NewFirstSuccessStrategy(providers) + case string(strategies.StrategyComparison): + return strategies.strategies.NewComparisonStrategy(providers) + default: + return strategies.strategies.NewFirstMatchStrategy(providers) + } +} + // Metadata provides the name `multiprovider` and the names of each provider passed. func (mp *MultiProvider) Metadata() openfeature.Metadata { metaJSON, _ := json.Marshal(mp.AggregatedMetadata) @@ -126,32 +143,32 @@ func (mp *MultiProvider) Hooks() []openfeature.Hook { // BooleanEvaluation returns a boolean flag func (mp *MultiProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { - // code to evaluate boolean - return openfeature.BoolResolutionDetail{} + + return mp.EvaluationStrategy.BooleanEvaluation(ctx, flag, defaultValue, evalCtx) } // StringEvaluation returns a string flag func (mp *MultiProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { - // code to evaluate string - return openfeature.StringResolutionDetail{} + + return mp.EvaluationStrategy.StringEvaluation(ctx, flag, defaultValue, evalCtx) } // FloatEvaluation returns a float flag func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { - // code to evaluate float - return openfeature.FloatResolutionDetail{} + + return mp.EvaluationStrategy.FloatEvaluation(ctx, flag, defaultValue, evalCtx) } // IntEvaluation returns an int flag func (mp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { - // code to evaluate int - return openfeature.IntResolutionDetail{} + + return mp.EvaluationStrategy.IntEvaluation(ctx, flag, defaultValue, evalCtx) } // ObjectEvaluation returns an object flag func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { - // code to evaluate object - return openfeature.InterfaceResolutionDetail{} + + return mp.EvaluationStrategy.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) } // Init will run the initialize method for all of provides and aggregate the errors. From 10b8d74ceadf0387a4e2835ba3da862d41bc148f Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Wed, 9 Apr 2025 20:33:06 -0400 Subject: [PATCH 21/68] creating the base of a base strategy similar to js version that can be used to define custom strategies Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- providers/multi-provider/pkg/base_stratgey.go | 81 +++++++++++++++++++ providers/multi-provider/pkg/providers.go | 28 +++++-- 2 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 providers/multi-provider/pkg/base_stratgey.go diff --git a/providers/multi-provider/pkg/base_stratgey.go b/providers/multi-provider/pkg/base_stratgey.go new file mode 100644 index 000000000..f88043021 --- /dev/null +++ b/providers/multi-provider/pkg/base_stratgey.go @@ -0,0 +1,81 @@ +package multiprovider + +import ( + "github.com/open-feature/go-sdk/openfeature" +) + +// RunModeValue indicates whether providers are evaluated sequentially or in parallel. +type RunModeValue string + +const ( + RunModeSequential = "sequential" + RunModeParallel = "parallel" +) + +// StrategyEvaluationContext contains flag-wide info. +type StrategyEvaluationContext struct { + FlagKey string + FlagType openfeature.Type +} + +type StrategyPerProviderContext struct { + StrategyEvaluationContext + Provider openfeature.FeatureProvider + ProviderName string + Status openfeature.State +} + +type FinalResult[T openfeature.Type] struct { + Provider openfeature.FeatureProvider + ProviderName string + Details interface{} + Errors []openfeature.ResolutionError +} + +func (fr *FinalResult[T]) determineDetails(detailType openfeature.Type) { + switch detailType { + + case openfeature.Boolean: + fr.Details = openfeature.BoolResolutionDetail{} + case openfeature.String: + fr.Details = openfeature.StringResolutionDetail{} + case openfeature.Float: + fr.Details = openfeature.FloatResolutionDetail{} + case openfeature.Int: + fr.Details = openfeature.IntResolutionDetail{} + case openfeature.Object: + fr.Details = openfeature.InterfaceResolutionDetail{} + + } +} + +type EvaluationStrategy interface { + // ShouldEvaluateThisProvider determines if the provider should be evaluated + ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool + + // ShouldEvaluateNextProvider determines whether the next provider should be evaluated + ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result openfeature.InterfaceResolutionDetail) bool + + // DetermineFinalResult decides the final result from the evaluated providers + DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []openfeature.InterfaceResolutionDetail) FinalResult[openfeature.Type] +} + +// BaseEvaluationStrategy Provides default implementations for the methods that can be used to create user defined strategies +type BaseEvaluationStrategy struct { + RunMode RunModeValue +} + +// ShouldEvaluateThisProvider checks if the provider should be evaluated +func (s *BaseEvaluationStrategy) ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool { + if strategyContext.Status == openfeature.NotReadyState || strategyContext.Status == openfeature.FatalState { + return false + } + return true +} + +func (s *BaseEvaluationStrategy) ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result openfeature.InterfaceResolutionDetail) bool { + return true +} + +func (s *BaseEvaluationStrategy) DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []openfeature.InterfaceResolutionDetail) { +} diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 4d1719a99..bdaa4c9f1 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -43,7 +43,7 @@ type MultiProvider struct { } // NewMultiProvider returns the unified interface of multiple providers for interaction. -func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy string, logger *hooks.LoggingHook) (*MultiProvider, error) { +func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy strategies.Strategy, logger *hooks.LoggingHook) (*MultiProvider, error) { multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, @@ -51,6 +51,7 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s Name: "multiprovider", OriginalMetadata: map[string]openfeature.Metadata{}, }, + EvaluationStrategy: evaluationStrategy, } err := multiProvider.registerProviders(passedProviders) @@ -58,7 +59,7 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s return nil, err } - multiProvider.EvaluationStrategy = multiProvider.strategyMethod(evaluationStrategy, multiProvider.providersEntries) + // multiProvider.EvaluationStrategy = multiProvider.strategyMethod(evaluationStrategy, multiProvider.providersEntries) return multiProvider, nil } @@ -114,17 +115,30 @@ func (mp *MultiProvider) registerProviders(providers []UniqueNameProvider) error return nil } +// // strategyMethod determines the strategy for the evaluation of the flags in the providers based on the passed value in the set up +// func (mp *MultiProvider) strategyMethod(name string, providers []UniqueNameProvider) strategies.Strategy { +// switch name { +// case string(strategies.StrategyFirstMatch): +// return strategies.strategies.NewFirstMatchStrategy(providers) +// case string(strategies.StrategyFirstSuccess): +// return strategies.strategies.NewFirstSuccessStrategy(providers) +// case string(strategies.StrategyComparison): +// return strategies.strategies.NewComparisonStrategy(providers) +// default: +// return strategies.strategies.NewFirstMatchStrategy(providers) +// } +// } // strategyMethod determines the strategy for the evaluation of the flags in the providers based on the passed value in the set up func (mp *MultiProvider) strategyMethod(name string, providers []UniqueNameProvider) strategies.Strategy { switch name { case string(strategies.StrategyFirstMatch): - return strategies.strategies.NewFirstMatchStrategy(providers) + return strategies.NewFirstMatchStrategy(providers) case string(strategies.StrategyFirstSuccess): - return strategies.strategies.NewFirstSuccessStrategy(providers) + return strategies.NewFirstSuccessStrategy(providers) case string(strategies.StrategyComparison): - return strategies.strategies.NewComparisonStrategy(providers) + return strategies.NewComparisonStrategy(providers) default: - return strategies.strategies.NewFirstMatchStrategy(providers) + return strategies.NewFirstMatchStrategy(providers) } } @@ -232,4 +246,4 @@ func (mp *MultiProvider) Shutdown() { func (mp *MultiProvider) EventChannel() <-chan openfeature.Event { return mp.events -} +} \ No newline at end of file From c5f5f145fdd15b8ddabc45768d91dba209a75cdf Mon Sep 17 00:00:00 2001 From: bbland1 <104288486+bbland1@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:54:03 -0400 Subject: [PATCH 22/68] fleshing out the BaseStrategy to have the help funcs to be used in strategies similar to TS and passing that type into the provider Signed-off-by: bbland1 <104288486+bbland1@users.noreply.github.com> --- .release-please-manifest.json | 2 +- providers/multi-provider/pkg/base_stratgey.go | 81 ----------- providers/multi-provider/pkg/providers.go | 57 ++------ .../multi-provider/pkg/providers_test.go | 25 ++-- .../strategies/base_strategy_test.go | 94 +++++++++++++ .../strategies/base_stratgey.go | 127 ++++++++++++++++++ 6 files changed, 250 insertions(+), 136 deletions(-) delete mode 100644 providers/multi-provider/pkg/base_stratgey.go create mode 100644 providers/multi-provider/strategies/base_strategy_test.go create mode 100644 providers/multi-provider/strategies/base_stratgey.go diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 23e4291d1..83d6fb6cb 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -16,5 +16,5 @@ "providers/prefab": "0.0.2", "tests/flagd": "1.4.1", "providers/go-feature-flag-in-process": "0.1.0", - "providers/multi-provider": "0.0.2" + "providers/multi-provider": "0.0.3" } diff --git a/providers/multi-provider/pkg/base_stratgey.go b/providers/multi-provider/pkg/base_stratgey.go deleted file mode 100644 index f88043021..000000000 --- a/providers/multi-provider/pkg/base_stratgey.go +++ /dev/null @@ -1,81 +0,0 @@ -package multiprovider - -import ( - "github.com/open-feature/go-sdk/openfeature" -) - -// RunModeValue indicates whether providers are evaluated sequentially or in parallel. -type RunModeValue string - -const ( - RunModeSequential = "sequential" - RunModeParallel = "parallel" -) - -// StrategyEvaluationContext contains flag-wide info. -type StrategyEvaluationContext struct { - FlagKey string - FlagType openfeature.Type -} - -type StrategyPerProviderContext struct { - StrategyEvaluationContext - Provider openfeature.FeatureProvider - ProviderName string - Status openfeature.State -} - -type FinalResult[T openfeature.Type] struct { - Provider openfeature.FeatureProvider - ProviderName string - Details interface{} - Errors []openfeature.ResolutionError -} - -func (fr *FinalResult[T]) determineDetails(detailType openfeature.Type) { - switch detailType { - - case openfeature.Boolean: - fr.Details = openfeature.BoolResolutionDetail{} - case openfeature.String: - fr.Details = openfeature.StringResolutionDetail{} - case openfeature.Float: - fr.Details = openfeature.FloatResolutionDetail{} - case openfeature.Int: - fr.Details = openfeature.IntResolutionDetail{} - case openfeature.Object: - fr.Details = openfeature.InterfaceResolutionDetail{} - - } -} - -type EvaluationStrategy interface { - // ShouldEvaluateThisProvider determines if the provider should be evaluated - ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool - - // ShouldEvaluateNextProvider determines whether the next provider should be evaluated - ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result openfeature.InterfaceResolutionDetail) bool - - // DetermineFinalResult decides the final result from the evaluated providers - DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []openfeature.InterfaceResolutionDetail) FinalResult[openfeature.Type] -} - -// BaseEvaluationStrategy Provides default implementations for the methods that can be used to create user defined strategies -type BaseEvaluationStrategy struct { - RunMode RunModeValue -} - -// ShouldEvaluateThisProvider checks if the provider should be evaluated -func (s *BaseEvaluationStrategy) ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool { - if strategyContext.Status == openfeature.NotReadyState || strategyContext.Status == openfeature.FatalState { - return false - } - return true -} - -func (s *BaseEvaluationStrategy) ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result openfeature.InterfaceResolutionDetail) bool { - return true -} - -func (s *BaseEvaluationStrategy) DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []openfeature.InterfaceResolutionDetail) { -} diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index bdaa4c9f1..c8cbc9590 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -8,7 +8,7 @@ import ( "sync" err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" - strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" + strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/strategies" "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" @@ -36,14 +36,14 @@ type MultiProvider struct { providersEntries []UniqueNameProvider providersEntriesByName map[string]UniqueNameProvider AggregatedMetadata MultiMetadata - EvaluationStrategy strategies.Strategy + EvaluationStrategy strategies.BaseEvaluationStrategy events chan openfeature.Event status openfeature.State mu sync.Mutex } // NewMultiProvider returns the unified interface of multiple providers for interaction. -func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy strategies.Strategy, logger *hooks.LoggingHook) (*MultiProvider, error) { +func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy strategies.BaseEvaluationStrategy, logger *hooks.LoggingHook) (*MultiProvider, error) { multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, @@ -59,8 +59,6 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s return nil, err } - // multiProvider.EvaluationStrategy = multiProvider.strategyMethod(evaluationStrategy, multiProvider.providersEntries) - return multiProvider, nil } @@ -115,33 +113,6 @@ func (mp *MultiProvider) registerProviders(providers []UniqueNameProvider) error return nil } -// // strategyMethod determines the strategy for the evaluation of the flags in the providers based on the passed value in the set up -// func (mp *MultiProvider) strategyMethod(name string, providers []UniqueNameProvider) strategies.Strategy { -// switch name { -// case string(strategies.StrategyFirstMatch): -// return strategies.strategies.NewFirstMatchStrategy(providers) -// case string(strategies.StrategyFirstSuccess): -// return strategies.strategies.NewFirstSuccessStrategy(providers) -// case string(strategies.StrategyComparison): -// return strategies.strategies.NewComparisonStrategy(providers) -// default: -// return strategies.strategies.NewFirstMatchStrategy(providers) -// } -// } -// strategyMethod determines the strategy for the evaluation of the flags in the providers based on the passed value in the set up -func (mp *MultiProvider) strategyMethod(name string, providers []UniqueNameProvider) strategies.Strategy { - switch name { - case string(strategies.StrategyFirstMatch): - return strategies.NewFirstMatchStrategy(providers) - case string(strategies.StrategyFirstSuccess): - return strategies.NewFirstSuccessStrategy(providers) - case string(strategies.StrategyComparison): - return strategies.NewComparisonStrategy(providers) - default: - return strategies.NewFirstMatchStrategy(providers) - } -} - // Metadata provides the name `multiprovider` and the names of each provider passed. func (mp *MultiProvider) Metadata() openfeature.Metadata { metaJSON, _ := json.Marshal(mp.AggregatedMetadata) @@ -157,32 +128,32 @@ func (mp *MultiProvider) Hooks() []openfeature.Hook { // BooleanEvaluation returns a boolean flag func (mp *MultiProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { - - return mp.EvaluationStrategy.BooleanEvaluation(ctx, flag, defaultValue, evalCtx) + // code to evaluate boolean + return openfeature.BoolResolutionDetail{} } // StringEvaluation returns a string flag func (mp *MultiProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { - - return mp.EvaluationStrategy.StringEvaluation(ctx, flag, defaultValue, evalCtx) + // code to evaluate string + return openfeature.StringResolutionDetail{} } // FloatEvaluation returns a float flag func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { - - return mp.EvaluationStrategy.FloatEvaluation(ctx, flag, defaultValue, evalCtx) + // code to evaluate float + return openfeature.FloatResolutionDetail{} } // IntEvaluation returns an int flag -func (mp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { - - return mp.EvaluationStrategy.IntEvaluation(ctx, flag, defaultValue, evalCtx) +func (imp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { + // code to evaluate int + return openfeature.IntResolutionDetail{} } // ObjectEvaluation returns an object flag func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { - - return mp.EvaluationStrategy.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) + // code to evaluate object + return openfeature.InterfaceResolutionDetail{} } // Init will run the initialize method for all of provides and aggregate the errors. diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 0f691565b..527a3f6ed 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -8,6 +8,7 @@ import ( "time" errs "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" + strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/strategies" "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" @@ -60,6 +61,8 @@ func NewMockProvider(initCount *int, shutCount *int, testErr string, initDelay i } } +// todo: the test that check the unique names of the providers are too dependent on order needs to use a map or something to better verify the correct unique name to the provider but not dependent on the order of a slice + func TestMultiProvider_ProvidersMethod(t *testing.T) { testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() @@ -77,7 +80,7 @@ func TestMultiProvider_ProvidersMethod(t *testing.T) { Provider: testProvider2, UniqueName: "provider2", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -114,7 +117,7 @@ func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { Provider: testProvider2, UniqueName: "provider2", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -167,7 +170,7 @@ func TestMultiProvider_ProviderByNameMethod(t *testing.T) { Provider: testProvider2, UniqueName: "provider2", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -211,7 +214,7 @@ func TestMultiProvider_MetaData(t *testing.T) { Provider: testProvider2, UniqueName: "provider2", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -259,7 +262,7 @@ func TestMultiProvider_Init(t *testing.T) { Provider: testProvider3, UniqueName: "provider3", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -313,7 +316,7 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { Provider: testProvider3, UniqueName: "provider3", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -377,7 +380,7 @@ func TestMultiProvider_Shutdown(t *testing.T) { Provider: testProvider3, UniqueName: "provider3", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -412,7 +415,7 @@ func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { }, { Provider: testProvider2, }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -454,7 +457,7 @@ func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { }, { Provider: testProvider4, }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -497,7 +500,7 @@ func TestNewMultiProvider_ProvidersUsePassedNames(t *testing.T) { Provider: testProvider2, UniqueName: "theSecond", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err != nil { t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) @@ -534,7 +537,7 @@ func TestNewMultiProvider_ProvidersErrorNameNotUnique(t *testing.T) { Provider: testProvider2, UniqueName: "provider", }, - }, "test", defaultLogger) + }, strategies.BaseEvaluationStrategy{}, defaultLogger) if err == nil { t.Errorf("Expected the multiprovider to have an error") diff --git a/providers/multi-provider/strategies/base_strategy_test.go b/providers/multi-provider/strategies/base_strategy_test.go new file mode 100644 index 000000000..ec387f52a --- /dev/null +++ b/providers/multi-provider/strategies/base_strategy_test.go @@ -0,0 +1,94 @@ +package strategies + +import ( + "testing" + + "github.com/open-feature/go-sdk/openfeature" + "github.com/stretchr/testify/assert" +) + +func TestShouldEvaluateThisProvider(t *testing.T) { + strategy := BaseEvaluationStrategy{} + + tests := []struct { + status openfeature.State + shouldEval bool + }{ + {openfeature.NotReadyState, false}, + {openfeature.FatalState, false}, + {openfeature.ReadyState, true}, + } + + for _, test := range tests { + ctx := StrategyPerProviderContext{ + Status: test.status, + } + result := strategy.ShouldEvaluateThisProvider(ctx, openfeature.EvaluationContext{}) + assert.Equal(t, test.shouldEval, result) + } +} + +func TestShouldEvaluateNextProvider(t *testing.T) { + strategy := BaseEvaluationStrategy{} + ctx := StrategyPerProviderContext{} + result := ResolutionDetail[openfeature.Type]{} + assert.True(t, strategy.ShouldEvaluateNextProvider(ctx, openfeature.EvaluationContext{}, result)) +} + +func TestDetermineFinalResultPanics(t *testing.T) { + strategy := BaseEvaluationStrategy{} + + assert.Panics(t, func() { + strategy.DetermineFinalResult(StrategyEvaluationContext{}, openfeature.EvaluationContext{}, nil) + }) +} + +func TestHasError(t *testing.T) { + noError := ResolutionDetail[openfeature.Type]{} + withError := ResolutionDetail[openfeature.Type]{ + ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewGeneralResolutionError("something broke"), + }, + } + + assert.False(t, HasError(noError)) + assert.True(t, HasError(withError)) +} + +func TestHasErrorWithCode(t *testing.T) { + resWithCode := ResolutionDetail[openfeature.Type]{ + ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewParseErrorResolutionError("bad parse"), + }, + } + assert.True(t, HasErrorWithCode(resWithCode, openfeature.ParseErrorCode)) + assert.False(t, HasErrorWithCode(resWithCode, openfeature.GeneralCode)) +} + +func TestCollectProviderErrors(t *testing.T) { + err1 := openfeature.NewParseErrorResolutionError("bad parse") + err2 := openfeature.NewFlagNotFoundResolutionError("what flag") + + resolutions := []ResolutionDetail[openfeature.Type]{ + {ProviderName: "prov1"}, + {ProviderName: "prov2", ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ResolutionError: err1}}, + {ProviderName: "prov3", ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ResolutionError: err2}}, + } + + final := CollectProviderErrors(resolutions) + assert.Len(t, final.Errors, 2) + assert.Equal(t, "prov2", final.Errors[0].ProviderName) + assert.Equal(t, "prov3", final.Errors[1].ProviderName) +} + +func TestResolutionToFinal(t *testing.T) { + res := ResolutionDetail[openfeature.Type]{ + Value: openfeature.Boolean, + ProviderName: "myProvider", + } + + final := ResolutionToFinal(res) + assert.Equal(t, "myProvider", final.ProviderName) + assert.Equal(t, res, final.Details) + assert.Empty(t, final.Errors) +} diff --git a/providers/multi-provider/strategies/base_stratgey.go b/providers/multi-provider/strategies/base_stratgey.go new file mode 100644 index 000000000..2e5429735 --- /dev/null +++ b/providers/multi-provider/strategies/base_stratgey.go @@ -0,0 +1,127 @@ +package strategies + +import ( + "strings" + + "github.com/open-feature/go-sdk/openfeature" +) + +// RunModeValue indicates whether providers are evaluated sequentially or in parallel. +type RunModeValue string + +const ( + RunModeSequential = "sequential" + RunModeParallel = "parallel" +) + +// StrategyEvaluationContext contains flag-wide info. +type StrategyEvaluationContext struct { + FlagKey string + FlagType openfeature.Type +} + +// StrategyPerProviderContext +type StrategyPerProviderContext struct { + StrategyEvaluationContext + Provider openfeature.FeatureProvider + ProviderName string + Status openfeature.State +} + +type ProviderError struct { + ProviderName string + Error openfeature.ResolutionError +} + +type ResolutionDetail[T openfeature.Type] struct { + Value T + ProviderName string + Provider openfeature.FeatureProvider + openfeature.ProviderResolutionDetail +} + +type FinalResult[T openfeature.Type] struct { + Provider openfeature.FeatureProvider + ProviderName string + Details ResolutionDetail[T] + Errors []ProviderError +} + +// EvaluationStrategy is the base functions needed for a strategy +type EvaluationStrategy interface { + // ShouldEvaluateThisProvider determines if the provider should be evaluated + ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool + + // ShouldEvaluateNextProvider determines whether the next provider should be evaluated + ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result ResolutionDetail[openfeature.Type]) bool + + // DetermineFinalResult decides the final result from the evaluated providers + DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []ResolutionDetail[openfeature.Type]) FinalResult[openfeature.Type] +} + +// BaseEvaluationStrategy Provides default implementations for the methods that can be used to create user defined strategies. +// DetermineFinalResult must be fully implemented for the user to create custom strategies +// ShouldEvaluateThisProvider & ShouldEvaluateNextProvider have default implementations +type BaseEvaluationStrategy struct { + RunMode RunModeValue +} + +// ShouldEvaluateThisProvider checks if the provider should be evaluated +func (s *BaseEvaluationStrategy) ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool { + if strategyContext.Status == openfeature.NotReadyState || strategyContext.Status == openfeature.FatalState { + return false + } + return true +} + +// ShouldEvaluateNextProvider checks if the next provider should be evaluated based on the result of the previous provider +func (s *BaseEvaluationStrategy) ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result ResolutionDetail[openfeature.Type]) bool { + return true +} + +// DetermineFinalResult needs to be implemented by the user to properly define custom strategy +func (s *BaseEvaluationStrategy) DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []ResolutionDetail[openfeature.Type]) FinalResult[openfeature.Type] { + panic("DetermineFinalResult must be implemented by the custom strategy") +} + +// HasError helper function used to determine if a resolution has an error +func HasError[T openfeature.Type](resolution ResolutionDetail[T]) bool { + return resolution.ResolutionError != (openfeature.ResolutionError{}) +} + +// HasErrorWithCode helper function to determine if a resolution has a specific error code +func HasErrorWithCode[T openfeature.Type](resolution ResolutionDetail[T], code openfeature.ErrorCode) bool { + if !HasError(resolution) { + return false + } + + return strings.HasPrefix(resolution.ResolutionError.Error(), string(code)) +} + +// CollectProviderErrors helper function to collate the errors to add to the final result struct +func CollectProviderErrors[T openfeature.Type](resolutions []ResolutionDetail[T]) FinalResult[T] { + var errs []ProviderError + + for _, resolution := range resolutions { + if HasError(resolution) { + errs = append(errs, ProviderError{ + ProviderName: resolution.ProviderName, + Error: resolution.ResolutionError, + }) + } + } + + return FinalResult[T]{ + Errors: errs, + } +} + +// ResolutionToFinal converts successful resolution to final result +func ResolutionToFinal[T openfeature.Type](resolution ResolutionDetail[T]) FinalResult[T] { + return FinalResult[T]{ + Provider: resolution.Provider, + ProviderName: resolution.ProviderName, + Details: resolution, + Errors: []ProviderError{}, + } +} From ecb71701baebfcd5cecfad8945ab0af96021d323 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Mon, 7 Apr 2025 19:03:57 -0400 Subject: [PATCH 23/68] feat: Define strategy interface & strategy name type Helper methods added Signed-off-by: Jordan Blacker --- .../internal/strategies/strategies.go | 92 +++++++++++++++++++ providers/multi-provider/pkg/providers.go | 2 + 2 files changed, 94 insertions(+) create mode 100644 providers/multi-provider/internal/strategies/strategies.go diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go new file mode 100644 index 000000000..f39d71643 --- /dev/null +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -0,0 +1,92 @@ +// Package strategy Resolution strategies are defined within this package +package strategies + +import ( + "context" + of "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/pkg/openfeature" + "reflect" +) + +const ( + MetadataSuccessfulProviderName = "multiprovider-successful-provider-name" + MetadataStrategyUsed = "multiprovider-strategy-used" + // StrategyFirstMatch First provider whose response that is not FlagNotFound will be returned. This is executed + // sequentially, and not in parallel. + StrategyFirstMatch EvaluationStrategy = "first-match" + // StrategyFirstSuccess First provider response that is not an error will be returned. This is executed in parallel + StrategyFirstSuccess EvaluationStrategy = "first-success" + // StrategyComparison All providers are called in parallel. If all responses agree the value will be returned. + // Otherwise, the value from the designated fallback provider's response will be returned. The fallback provider + // will be assigned to the first provider registered. (NOT YET IMPLEMENTED, SUBJECT TO CHANGE) + StrategyComparison EvaluationStrategy = "comparison" +) + +// EvaluationStrategy Defines a strategy to use for resolving the result from multiple providers +type EvaluationStrategy = string + +// Strategy Interface for evaluating providers within the multi-provider. +type Strategy interface { + Name() EvaluationStrategy + BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail + StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail + FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail + IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail + ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail +} + +type resultConstraint interface { + of.BoolResolutionDetail | of.IntResolutionDetail | of.StringResolutionDetail | of.FloatResolutionDetail | of.InterfaceResolutionDetail +} + +type resultWrapper[R resultConstraint] struct { + result *R +} + +// buildDefaultResult Creates a default result using reflection via generics +func buildDefaultResult[R resultConstraint, DV bool | string | int64 | float64 | interface{}](strategy EvaluationStrategy, defaultValue DV, err error) resultWrapper[R] { + result := *new(R) + details := of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError(err.Error()), + Reason: of.DefaultReason, + FlagMetadata: of.FlagMetadata{MetadataSuccessfulProviderName: "none", MetadataStrategyUsed: strategy}, + } + switch reflect.TypeOf(result).Name() { + case "BoolResolutionDetail": + r := any(result).(of.BoolResolutionDetail) + r.Value = any(defaultValue).(bool) + r.ProviderResolutionDetail = details + result = any(r).(R) + case "StringResolutionDetail": + r := any(result).(of.StringResolutionDetail) + r.Value = any(defaultValue).(string) + r.ProviderResolutionDetail = details + result = any(r).(R) + case "IntResolutionDetail": + r := any(result).(of.IntResolutionDetail) + r.Value = any(defaultValue).(int64) + r.ProviderResolutionDetail = details + result = any(r).(R) + case "FloatResolutionDetail": + r := any(result).(of.FloatResolutionDetail) + r.Value = any(defaultValue).(float64) + r.ProviderResolutionDetail = details + result = any(r).(R) + default: + r := any(result).(of.InterfaceResolutionDetail) + r.Value = defaultValue + r.ProviderResolutionDetail = details + result = any(r).(R) + } + + return resultWrapper[R]{result: &result} +} + +func setFlagMetadata(strategyUsed EvaluationStrategy, successProviderName string, metadata of.FlagMetadata) of.FlagMetadata { + if metadata == nil { + metadata = make(of.FlagMetadata) + } + metadata[MetadataSuccessfulProviderName] = successProviderName + metadata[MetadataStrategyUsed] = strategyUsed + return metadata +} diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index c8cbc9590..ed68b229a 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -31,6 +31,8 @@ type MultiMetadata struct { OriginalMetadata map[string]openfeature.Metadata `json:"originalMetadata"` } +var _ openfeature.FeatureProvider = (*MultiProvider)(nil) + // MultiProvider implements openfeature `FeatureProvider` in a way to accept an array of providers. type MultiProvider struct { providersEntries []UniqueNameProvider From e0cc5ade027a4fdce022a4b2366eadb8b6da573f Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 9 Apr 2025 14:23:11 -0400 Subject: [PATCH 24/68] feat: Add FirstSuccessStrategy Implementation Signed-off-by: Jordan Blacker --- .../internal/strategies/first_success.go | 125 ++++++++++++++++++ .../internal/strategies/first_success_test.go | 23 ++++ 2 files changed, 148 insertions(+) create mode 100644 providers/multi-provider/internal/strategies/first_success.go create mode 100644 providers/multi-provider/internal/strategies/first_success_test.go diff --git a/providers/multi-provider/internal/strategies/first_success.go b/providers/multi-provider/internal/strategies/first_success.go new file mode 100644 index 000000000..aa39fb761 --- /dev/null +++ b/providers/multi-provider/internal/strategies/first_success.go @@ -0,0 +1,125 @@ +package strategies + +import ( + "context" + "errors" + multiprovider "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg" + of "github.com/open-feature/go-sdk/openfeature" + "sync" +) + +type FirstSuccessStrategy struct { + providers []multiprovider.UniqueNameProvider +} + +var _ Strategy = (*FirstSuccessStrategy)(nil) + +// NewFirstSuccessStrategy Creates a new FirstSuccessStrategy instance as a Strategy +func NewFirstSuccessStrategy(providers []multiprovider.UniqueNameProvider) Strategy { + return &FirstSuccessStrategy{providers: providers} +} + +func (f *FirstSuccessStrategy) Name() EvaluationStrategy { + return StrategyFirstSuccess +} + +func (f *FirstSuccessStrategy) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { + return *evaluateFirstSuccess[of.BoolResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstSuccessStrategy) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail { + return *evaluateFirstSuccess[of.StringResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstSuccessStrategy) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { + return *evaluateFirstSuccess[of.FloatResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstSuccessStrategy) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { + return *evaluateFirstSuccess[of.IntResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstSuccessStrategy) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { + return *evaluateFirstSuccess[of.InterfaceResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []multiprovider.UniqueNameProvider, flag string, defaultValue DV, evalCtx of.FlattenedContext) resultWrapper[R] { + var ( + mutex sync.Mutex + wg sync.WaitGroup + result *resultWrapper[R] + ) + errChan := make(chan error) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + for _, provider := range providers { + wg.Add(1) + go func(p multiprovider.UniqueNameProvider) { + defer wg.Done() + switch any(defaultValue).(type) { + case bool: + r := provider.Provider.BooleanEvaluation(ctx, flag, any(defaultValue).(bool), evalCtx) + if r.Error() == nil { + mutex.Lock() + result = &resultWrapper[R]{result: any(r).(*R)} + cancel() + mutex.Unlock() + } + errChan <- r.Error() + case string: + r := provider.Provider.StringEvaluation(ctx, flag, any(defaultValue).(string), evalCtx) + if r.Error() == nil { + mutex.Lock() + result = &resultWrapper[R]{result: any(r).(*R)} + cancel() + mutex.Unlock() + } + errChan <- r.Error() + case int64: + r := provider.Provider.IntEvaluation(ctx, flag, any(defaultValue).(int64), evalCtx) + if r.Error() == nil { + mutex.Lock() + result = &resultWrapper[R]{result: any(r).(*R)} + cancel() + mutex.Unlock() + } + errChan <- r.Error() + case float64: + r := provider.Provider.FloatEvaluation(ctx, flag, any(defaultValue).(float64), evalCtx) + if r.Error() == nil { + mutex.Lock() + result = &resultWrapper[R]{result: any(r).(*R)} + cancel() + mutex.Unlock() + } + errChan <- r.Error() + default: + r := provider.Provider.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) + if r.Error() == nil { + mutex.Lock() + result = &resultWrapper[R]{result: any(r).(*R)} + cancel() + mutex.Unlock() + } + errChan <- r.Error() + } + + }(provider) + } + go func() { + wg.Wait() + close(errChan) + }() + + errs := make([]error, 0, 1) + for e := range errChan { + errs = append(errs, e) + } + + if result != nil { + return *result + } + + err := errors.Join(errs...) + return buildDefaultResult[R](StrategyFirstSuccess, defaultValue, err) +} diff --git a/providers/multi-provider/internal/strategies/first_success_test.go b/providers/multi-provider/internal/strategies/first_success_test.go new file mode 100644 index 000000000..170f29ac0 --- /dev/null +++ b/providers/multi-provider/internal/strategies/first_success_test.go @@ -0,0 +1,23 @@ +package strategies + +import "testing" + +func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { + // TODO +} + +func Test_FirstStrategy_StringEvaluation(t *testing.T) { + // TODO +} + +func Test_FirstStrategy_IntEvaluation(t *testing.T) { + // TODO +} + +func Test_FirstStrategy_FloatEvaluation(t *testing.T) { + // TODO +} + +func Test_FirstStrategy_ObjectEvaluation(t *testing.T) { + // TODO +} From af798932c3a4644cca73295fd3e3c5b6a2c04c38 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 16 Apr 2025 17:52:07 -0400 Subject: [PATCH 25/68] doc: Add README for multiprovider Signed-off-by: Jordan Blacker --- providers/multi-provider/README.md | 62 ++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/providers/multi-provider/README.md b/providers/multi-provider/README.md index e69de29bb..005fc2d5d 100644 --- a/providers/multi-provider/README.md +++ b/providers/multi-provider/README.md @@ -0,0 +1,62 @@ +OpenFeature Multi-Provider +------------ + +The Multi-Provider allows you to use multiple underlying providers as sources of flag data for the OpenFeature server SDK. +When a flag is being evaluated, the Multi-Provider will consult each underlying provider it is managing in order to +determine the final result. Different evaluation strategies can be defined to control which providers get evaluated and +which result is used. + +The Multi-Provider is a powerful tool for performing migrations between flag providers, or combining multiple providers +into a single feature flagging interface. For example: + +- **Migration**: When migrating between two providers, you can run both in parallel under a unified flagging interface. +As flags are added to the new provider, the Multi-Provider will automatically find and return them, falling back to the old provider +if the new provider does not have +- **Multiple Data Sources**: The Multi-Provider allows you to seamlessly combine many sources of flagging data, such as +environment variables, local files, database values and SaaS hosted feature management systems. + +# Installation + +```sh +go get github.com/open-feature/go-sdk-contrib/providers/multi-provider +go get github.com/open-feature/go-sdk +``` + +# Usage + +```go +import ( + "github.com/open-feature/go-sdk/openfeature" + "github.com/open-feature/go-sdk/openfeature/hooks" +) + +namedProviderA := UniqueNameProvider{ Provider: myProvider, UniqueName: "Provider A" } +namedProviderB := UniqueNameProvider{ Provider: myOtherProvider, UniqueName: "Provider B" } + +provider, err := NewMultiProvider([]UniqueNameProvider{namedProviderA, namedProviderB}, StrategyFirstMatch, hooks.NewLoggingHook(false)) +openfeature.SetProvider(provider) +``` + + +# Strategies + +There are multiple strategies that can be used to determine the result returned to the caller. A strategy must be set at +initialization time. + +There are 2 strategies available currently: + +- _First Match_ +- _First Success_ + +## First Match Strategy + +The first match strategy works by **sequentially** calling each provider in the order that they are provided to the mutli-provider. +The first provider that returns a result. It will try calling the next provider whenever it encounters a `FLAG_NOT_FOUND` +error. However, if a provider returns an error other than `FLAG_NOT_FOUND` the provider will stop and return the default +value along with setting the error details if a detailed request is issued. (allow changing this behavior?) + +## First Success Strategy + +The First Success strategy works by calling each provider in **parallel**. The first provider that returns a response +with no errors is returned and all other calls are cancelled. If no provider provides a successful result the default +value will be returned to the caller. \ No newline at end of file From 542e5d2ce928d51f0584e36b6f3ba37eb45c3cff Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 16 Apr 2025 18:02:01 -0400 Subject: [PATCH 26/68] feat: Configure strategy in provider initializer Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index ed68b229a..e7ccd8951 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" "sync" err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" @@ -38,14 +39,15 @@ type MultiProvider struct { providersEntries []UniqueNameProvider providersEntriesByName map[string]UniqueNameProvider AggregatedMetadata MultiMetadata - EvaluationStrategy strategies.BaseEvaluationStrategy + EvaluationStrategy string events chan openfeature.Event status openfeature.State mu sync.Mutex + strategy strategies.Strategy } // NewMultiProvider returns the unified interface of multiple providers for interaction. -func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy strategies.BaseEvaluationStrategy, logger *hooks.LoggingHook) (*MultiProvider, error) { +func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy strategies.EvaluationStrategy, logger *hooks.LoggingHook) (*MultiProvider, error) { multiProvider := &MultiProvider{ providersEntries: []UniqueNameProvider{}, providersEntriesByName: map[string]UniqueNameProvider{}, @@ -61,6 +63,17 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s return nil, err } + var strategy strategies.Strategy + switch evaluationStrategy { + case strategies.StrategyFirstMatch: + strategy = strategies.NewFirstMatchStrategy(multiProvider.Providers()) + case strategies.StrategyFirstSuccess: + strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers()) + default: + return nil, fmt.Errorf("%s is an unknown evalutation strategy", strategy) + } + multiProvider.strategy = strategy + return multiProvider, nil } From 8c78f3e2a3389f4dad946c36497e924d8c8de260 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 16 Apr 2025 18:03:27 -0400 Subject: [PATCH 27/68] feat: wire it all up Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index e7ccd8951..b03f8c801 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -143,32 +143,27 @@ func (mp *MultiProvider) Hooks() []openfeature.Hook { // BooleanEvaluation returns a boolean flag func (mp *MultiProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { - // code to evaluate boolean - return openfeature.BoolResolutionDetail{} + return mp.strategy.BooleanEvaluation(ctx, flag, defaultValue, evalCtx) } // StringEvaluation returns a string flag func (mp *MultiProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { - // code to evaluate string - return openfeature.StringResolutionDetail{} + return mp.strategy.StringEvaluation(ctx, flag, defaultValue, evalCtx) } // FloatEvaluation returns a float flag func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { - // code to evaluate float - return openfeature.FloatResolutionDetail{} + return mp.strategy.FloatEvaluation(ctx, flag, defaultValue, evalCtx) } // IntEvaluation returns an int flag func (imp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { - // code to evaluate int - return openfeature.IntResolutionDetail{} + return mp.strategy.IntEvaluation(ctx, flag, defaultValue, evalCtx) } // ObjectEvaluation returns an object flag func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { - // code to evaluate object - return openfeature.InterfaceResolutionDetail{} + return mp.strategy.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) } // Init will run the initialize method for all of provides and aggregate the errors. From aa2a0d142bc8645f27ad81a7a3c3fc550dfcda08 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 16 Apr 2025 18:06:51 -0400 Subject: [PATCH 28/68] refactor: use strategy's own name Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index b03f8c801..758db066b 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -39,7 +39,6 @@ type MultiProvider struct { providersEntries []UniqueNameProvider providersEntriesByName map[string]UniqueNameProvider AggregatedMetadata MultiMetadata - EvaluationStrategy string events chan openfeature.Event status openfeature.State mu sync.Mutex @@ -90,6 +89,10 @@ func (mp *MultiProvider) ProviderByName(name string) (UniqueNameProvider, bool) return provider, exists } +func (mp *MultiProvider) EvaluationStrategy() string { + return mp.strategy.Name() +} + // registerProviders ensures that when setting up an instant of MultiProvider the providers provided either have a unique name or the base `metadata.Name` is made unique by adding an indexed based number to it. // registerProviders also stores the providers by their unique name and in an array for easy usage. func (mp *MultiProvider) registerProviders(providers []UniqueNameProvider) error { From ed349d3e55b7326c2b694801db91915dc4b7ea22 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 16 Apr 2025 18:14:30 -0400 Subject: [PATCH 29/68] fix: handle post-rebase conflicts Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 758db066b..88354e448 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -9,7 +9,6 @@ import ( "sync" err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" - strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/strategies" "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/hooks" @@ -54,7 +53,6 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s Name: "multiprovider", OriginalMetadata: map[string]openfeature.Metadata{}, }, - EvaluationStrategy: evaluationStrategy, } err := multiProvider.registerProviders(passedProviders) @@ -160,7 +158,7 @@ func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defau } // IntEvaluation returns an int flag -func (imp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { +func (mp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { return mp.strategy.IntEvaluation(ctx, flag, defaultValue, evalCtx) } @@ -230,4 +228,4 @@ func (mp *MultiProvider) Shutdown() { func (mp *MultiProvider) EventChannel() <-chan openfeature.Event { return mp.events -} \ No newline at end of file +} From ef2514cf58742f7d11308a59492ea04ab94f7840 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Sat, 19 Apr 2025 13:32:00 -0400 Subject: [PATCH 30/68] refactor: Rename types, reorganize package structure - Rename UniqueNameProvider -> NamedProvider: More accurate description of type as it's not enforced - Updated signature of NewMultiProvider function: Using a map enforces unique names, use options pattern & config object for future extension - Relocated NamedProvider to strategies as it's not needed externally - Update shape of MultiProvider: Added logging, made all fields private, used map internally - Moved aggregate-errors into errors package - Added alias for ProviderMap shape & added helper functions - Use pointers since we're passing stuff around and want to avoid unnecessary clones of objects & extra garbage collection - Updated other various signatures throughout as needed - Update tests & remove tests that no longer apply Signed-off-by: Jordan Blacker --- .../internal/{ => errors}/aggregate-errors.go | 2 +- .../internal/strategies/first_match.go | 101 ++++ .../internal/strategies/first_match_test.go | 466 ++++++++++++++++ .../internal/strategies/first_success.go | 9 +- .../internal/strategies/strategies.go | 54 +- providers/multi-provider/pkg/providers.go | 291 ++++++---- .../multi-provider/pkg/providers_test.go | 516 +++--------------- 7 files changed, 852 insertions(+), 587 deletions(-) rename providers/multi-provider/internal/{ => errors}/aggregate-errors.go (98%) create mode 100644 providers/multi-provider/internal/strategies/first_match.go create mode 100644 providers/multi-provider/internal/strategies/first_match_test.go diff --git a/providers/multi-provider/internal/aggregate-errors.go b/providers/multi-provider/internal/errors/aggregate-errors.go similarity index 98% rename from providers/multi-provider/internal/aggregate-errors.go rename to providers/multi-provider/internal/errors/aggregate-errors.go index 5b7ccbcf0..a4d803311 100644 --- a/providers/multi-provider/internal/aggregate-errors.go +++ b/providers/multi-provider/internal/errors/aggregate-errors.go @@ -1,4 +1,4 @@ -package internal +package errors import ( "encoding/json" diff --git a/providers/multi-provider/internal/strategies/first_match.go b/providers/multi-provider/internal/strategies/first_match.go new file mode 100644 index 000000000..18c5209c9 --- /dev/null +++ b/providers/multi-provider/internal/strategies/first_match.go @@ -0,0 +1,101 @@ +package strategies + +import ( + "context" + of "github.com/open-feature/go-sdk/openfeature" +) + +type FirstMatchStrategy struct { + providers []*NamedProvider +} + +var _ Strategy = (*FirstMatchStrategy)(nil) + +// NewFirstMatchStrategy Creates a new FirstMatchStrategy instance as a Strategy +func NewFirstMatchStrategy(providers []*NamedProvider) Strategy { + return &FirstMatchStrategy{providers: providers} +} + +func (f *FirstMatchStrategy) Name() EvaluationStrategy { + return StrategyFirstMatch +} + +func (f *FirstMatchStrategy) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { + return *evaluateFirstMatch[of.BoolResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstMatchStrategy) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail { + return *evaluateFirstMatch[of.StringResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstMatchStrategy) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { + return *evaluateFirstMatch[of.FloatResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstMatchStrategy) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { + return *evaluateFirstMatch[of.IntResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func (f *FirstMatchStrategy) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { + return *evaluateFirstMatch[of.InterfaceResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result +} + +func evaluateFirstMatch[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []*NamedProvider, flag string, defaultValue DV, evalCtx of.FlattenedContext) resultWrapper[R] { + for _, provider := range providers { + switch any(defaultValue).(type) { + case bool: + r := provider.Provider.BooleanEvaluation(ctx, flag, any(defaultValue).(bool), evalCtx) + if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { + continue + } else if r.Error() != nil { + return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) + } + rp := &r + rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) + return resultWrapper[R]{result: any(rp).(*R)} + case string: + r := provider.Provider.StringEvaluation(ctx, flag, any(defaultValue).(string), evalCtx) + if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { + continue + } else if r.Error() != nil { + return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) + } + rp := &r + rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) + return resultWrapper[R]{result: any(rp).(*R)} + case int64: + r := provider.Provider.IntEvaluation(ctx, flag, any(defaultValue).(int64), evalCtx) + if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { + continue + } else if r.Error() != nil { + return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) + } + rp := &r + rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) + return resultWrapper[R]{result: any(rp).(*R)} + case float64: + r := provider.Provider.FloatEvaluation(ctx, flag, any(defaultValue).(float64), evalCtx) + if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { + continue + } else if r.Error() != nil { + return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) + } + rp := &r + rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) + return resultWrapper[R]{result: any(rp).(*R)} + default: + r := provider.Provider.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) + if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { + continue + } else if r.Error() != nil { + return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) + } + rp := &r + rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) + return resultWrapper[R]{result: any(rp).(*R)} + } + } + + // Build a default result if no matches are found + return buildDefaultResult[R](StrategyFirstMatch, defaultValue, nil) +} diff --git a/providers/multi-provider/internal/strategies/first_match_test.go b/providers/multi-provider/internal/strategies/first_match_test.go new file mode 100644 index 000000000..4bcc1f2bd --- /dev/null +++ b/providers/multi-provider/internal/strategies/first_match_test.go @@ -0,0 +1,466 @@ +package strategies + +import ( + "context" + "fmt" + of "github.com/open-feature/go-sdk/openfeature" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" +) + +func createMockProviders(ctrl *gomock.Controller, count int) ([]*NamedProvider, map[string]*MockFeatureProvider) { + providers := make([]*NamedProvider, 0, count) + providerMocks := make(map[string]*MockFeatureProvider) + for index := range count { + provider := NewMockFeatureProvider(ctrl) + namedProvider := NamedProvider{ + Provider: provider, + Name: fmt.Sprintf("%d", index), + } + providerMocks[namedProvider.Name] = provider + providers = append(providers, &namedProvider) + } + + return providers, providerMocks +} + +func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { + ctrl := gomock.NewController(t) + + t.Run("Single Provider Match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.BoolResolutionDetail{Value: true}) + + strategy := NewFirstMatchStrategy(providers) + result := strategy.BooleanEvaluation(context.Background(), "test-string", false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[0].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Default Resolution", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.BoolResolutionDetail{ + Value: false, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("not found"), + }, + }) + strategy := NewFirstMatchStrategy(providers) + result := strategy.BooleanEvaluation(context.Background(), "test-string", false, of.FlattenedContext{}) + assert.False(t, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewFlagNotFoundResolutionError("not found in any provider").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) + + t.Run("Evaluation stops after match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.BoolResolutionDetail{ + Value: false, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("Flag not found"), + }, + }) + mocks[providers[1].Name].EXPECT(). + BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.BoolResolutionDetail{Value: true}) + for i, p := range providers { + if i <= 1 { + continue + } + mocks[p.Name].EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + + strategy := NewFirstMatchStrategy(providers) + result := strategy.BooleanEvaluation(context.Background(), "test-flag", false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[1].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.BoolResolutionDetail{ + Value: false, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError("something went wrong"), + }, + }) + for i, p := range providers { + if i == 0 { + continue + } + mocks[p.Name].EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + strategy := NewFirstMatchStrategy(providers) + result := strategy.BooleanEvaluation(context.Background(), "test-string", false, of.FlattenedContext{}) + assert.False(t, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) +} + +func Test_FirstMatchStrategy_StringEvaluation(t *testing.T) { + ctrl := gomock.NewController(t) + + t.Run("Single Provider Match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.StringResolutionDetail{Value: "test"}) + + strategy := NewFirstMatchStrategy(providers) + result := strategy.StringEvaluation(context.Background(), "test-string", "", of.FlattenedContext{}) + assert.Equal(t, "test", result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[0].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Default Resolution", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.StringResolutionDetail{ + Value: "", + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("not found"), + }, + }) + strategy := NewFirstMatchStrategy(providers) + result := strategy.StringEvaluation(context.Background(), "test-string", "", of.FlattenedContext{}) + assert.Equal(t, "", result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewFlagNotFoundResolutionError("not found in any provider").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) + + t.Run("Evaluation stops after match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.StringResolutionDetail{ + Value: "", + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("Flag not found"), + }, + }) + mocks[providers[1].Name].EXPECT(). + StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.StringResolutionDetail{Value: "test"}) + for i, p := range providers { + if i <= 1 { + continue + } + mocks[p.Name].EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + + strategy := NewFirstMatchStrategy(providers) + result := strategy.StringEvaluation(context.Background(), "test-flag", "", of.FlattenedContext{}) + assert.Equal(t, "test", result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[1].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.StringResolutionDetail{ + Value: "", + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError("something went wrong"), + }, + }) + for i, p := range providers { + if i == 0 { + continue + } + mocks[p.Name].EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + strategy := NewFirstMatchStrategy(providers) + result := strategy.StringEvaluation(context.Background(), "test-string", "", of.FlattenedContext{}) + assert.Equal(t, "", result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) +} + +func Test_FirstMatchStrategy_IntEvaluation(t *testing.T) { + ctrl := gomock.NewController(t) + + t.Run("Single Provider Match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.IntResolutionDetail{Value: 123}) + + strategy := NewFirstMatchStrategy(providers) + result := strategy.IntEvaluation(context.Background(), "test-string", 0, of.FlattenedContext{}) + assert.Equal(t, int64(123), result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[0].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Default Resolution", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.IntResolutionDetail{ + Value: 0, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("not found"), + }, + }) + strategy := NewFirstMatchStrategy(providers) + result := strategy.IntEvaluation(context.Background(), "test-string", 0, of.FlattenedContext{}) + assert.Equal(t, int64(0), result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewFlagNotFoundResolutionError("not found in any provider").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) + + t.Run("Evaluation stops after match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.IntResolutionDetail{ + Value: 0, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("Flag not found"), + }, + }) + mocks[providers[1].Name].EXPECT(). + IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.IntResolutionDetail{Value: 123}) + for i, p := range providers { + if i <= 1 { + continue + } + mocks[p.Name].EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + + strategy := NewFirstMatchStrategy(providers) + result := strategy.IntEvaluation(context.Background(), "test-flag", 0, of.FlattenedContext{}) + assert.Equal(t, int64(123), result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[1].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.IntResolutionDetail{ + Value: 123, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError("something went wrong"), + }, + }) + for i, p := range providers { + if i == 0 { + continue + } + mocks[p.Name].EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + strategy := NewFirstMatchStrategy(providers) + result := strategy.IntEvaluation(context.Background(), "test-string", 123, of.FlattenedContext{}) + assert.Equal(t, int64(123), result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) +} + +func Test_FirstMatchStrategy_FloatEvaluation(t *testing.T) { + ctrl := gomock.NewController(t) + + t.Run("Single Provider Match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.FloatResolutionDetail{Value: 123}) + + strategy := NewFirstMatchStrategy(providers) + result := strategy.FloatEvaluation(context.Background(), "test-string", 0, of.FlattenedContext{}) + assert.Equal(t, float64(123), result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[0].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Default Resolution", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.FloatResolutionDetail{ + Value: 0, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("not found"), + }, + }) + strategy := NewFirstMatchStrategy(providers) + result := strategy.FloatEvaluation(context.Background(), "test-string", 0, of.FlattenedContext{}) + assert.Equal(t, float64(0), result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewFlagNotFoundResolutionError("not found in any provider").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) + + t.Run("Evaluation stops after match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.FloatResolutionDetail{ + Value: 0, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("Flag not found"), + }, + }) + mocks[providers[1].Name].EXPECT(). + FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.FloatResolutionDetail{Value: 123}) + for i, p := range providers { + if i <= 1 { + continue + } + mocks[p.Name].EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + + strategy := NewFirstMatchStrategy(providers) + result := strategy.FloatEvaluation(context.Background(), "test-flag", 0, of.FlattenedContext{}) + assert.Equal(t, float64(123), result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[1].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.FloatResolutionDetail{ + Value: 123.0, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError("something went wrong"), + }, + }) + for i, p := range providers { + if i == 0 { + continue + } + mocks[p.Name].EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + strategy := NewFirstMatchStrategy(providers) + result := strategy.FloatEvaluation(context.Background(), "test-string", 123, of.FlattenedContext{}) + assert.Equal(t, 123.0, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) +} + +func Test_FirstMatchStrategy_ObjectEvaluation(t *testing.T) { + ctrl := gomock.NewController(t) + + t.Run("Single Provider Match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.InterfaceResolutionDetail{Value: struct{ Field int }{Field: 123}}) + + strategy := NewFirstMatchStrategy(providers) + result := strategy.ObjectEvaluation(context.Background(), "test-string", struct{}{}, of.FlattenedContext{}) + assert.Equal(t, struct{ Field int }{Field: 123}, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[0].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Default Resolution", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 1) + mocks[providers[0].Name].EXPECT(). + ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.InterfaceResolutionDetail{ + Value: struct{}{}, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("not found"), + }, + }) + strategy := NewFirstMatchStrategy(providers) + result := strategy.ObjectEvaluation(context.Background(), "test-string", struct{}{}, of.FlattenedContext{}) + assert.Equal(t, struct{}{}, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewFlagNotFoundResolutionError("not found in any provider").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) + + t.Run("Evaluation stops after match", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.InterfaceResolutionDetail{ + Value: 0, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewFlagNotFoundResolutionError("Flag not found"), + }, + }) + mocks[providers[1].Name].EXPECT(). + ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.InterfaceResolutionDetail{Value: struct{ Field int }{Field: 123}}) + for i, p := range providers { + if i <= 1 { + continue + } + mocks[p.Name].EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + + strategy := NewFirstMatchStrategy(providers) + result := strategy.ObjectEvaluation(context.Background(), "test-flag", struct{}{}, of.FlattenedContext{}) + assert.Equal(t, struct{ Field int }{Field: 123}, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, providers[1].Name, result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { + providers, mocks := createMockProviders(ctrl, 5) + mocks[providers[0].Name].EXPECT(). + ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(of.InterfaceResolutionDetail{ + Value: struct{}{}, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError("something went wrong"), + }, + }) + for i, p := range providers { + if i == 0 { + continue + } + mocks[p.Name].EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + } + strategy := NewFirstMatchStrategy(providers) + result := strategy.ObjectEvaluation(context.Background(), "test-string", struct{}{}, of.FlattenedContext{}) + assert.Equal(t, struct{}{}, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) + }) +} diff --git a/providers/multi-provider/internal/strategies/first_success.go b/providers/multi-provider/internal/strategies/first_success.go index aa39fb761..3d20cb7d2 100644 --- a/providers/multi-provider/internal/strategies/first_success.go +++ b/providers/multi-provider/internal/strategies/first_success.go @@ -3,19 +3,18 @@ package strategies import ( "context" "errors" - multiprovider "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg" of "github.com/open-feature/go-sdk/openfeature" "sync" ) type FirstSuccessStrategy struct { - providers []multiprovider.UniqueNameProvider + providers []*NamedProvider } var _ Strategy = (*FirstSuccessStrategy)(nil) // NewFirstSuccessStrategy Creates a new FirstSuccessStrategy instance as a Strategy -func NewFirstSuccessStrategy(providers []multiprovider.UniqueNameProvider) Strategy { +func NewFirstSuccessStrategy(providers []*NamedProvider) Strategy { return &FirstSuccessStrategy{providers: providers} } @@ -43,7 +42,7 @@ func (f *FirstSuccessStrategy) ObjectEvaluation(ctx context.Context, flag string return *evaluateFirstSuccess[of.InterfaceResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result } -func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []multiprovider.UniqueNameProvider, flag string, defaultValue DV, evalCtx of.FlattenedContext) resultWrapper[R] { +func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []*NamedProvider, flag string, defaultValue DV, evalCtx of.FlattenedContext) resultWrapper[R] { var ( mutex sync.Mutex wg sync.WaitGroup @@ -54,7 +53,7 @@ func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 defer cancel() for _, provider := range providers { wg.Add(1) - go func(p multiprovider.UniqueNameProvider) { + go func(p *NamedProvider) { defer wg.Done() switch any(defaultValue).(type) { case bool: diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index f39d71643..0178a15d7 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -4,44 +4,44 @@ package strategies import ( "context" of "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/pkg/openfeature" "reflect" ) const ( MetadataSuccessfulProviderName = "multiprovider-successful-provider-name" MetadataStrategyUsed = "multiprovider-strategy-used" - // StrategyFirstMatch First provider whose response that is not FlagNotFound will be returned. This is executed - // sequentially, and not in parallel. - StrategyFirstMatch EvaluationStrategy = "first-match" - // StrategyFirstSuccess First provider response that is not an error will be returned. This is executed in parallel - StrategyFirstSuccess EvaluationStrategy = "first-success" - // StrategyComparison All providers are called in parallel. If all responses agree the value will be returned. - // Otherwise, the value from the designated fallback provider's response will be returned. The fallback provider - // will be assigned to the first provider registered. (NOT YET IMPLEMENTED, SUBJECT TO CHANGE) - StrategyComparison EvaluationStrategy = "comparison" + StrategyFirstMatch = "strategy-first-match" + StrategyFirstSuccess = "strategy-first-success" ) -// EvaluationStrategy Defines a strategy to use for resolving the result from multiple providers -type EvaluationStrategy = string +type ( + // EvaluationStrategy Defines a strategy to use for resolving the result from multiple providers + EvaluationStrategy = string + // Strategy Interface for evaluating providers within the multi-provider. + Strategy interface { + Name() EvaluationStrategy + BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail + StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail + FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail + IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail + ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail + } -// Strategy Interface for evaluating providers within the multi-provider. -type Strategy interface { - Name() EvaluationStrategy - BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail - StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail - FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail - IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail - ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail -} + // NamedProvider allows for a unique name to be assigned to a provider during a multi-provider set up. + // The name will be used when reporting errors & results to specify the provider associated. + NamedProvider struct { + Name string + Provider of.FeatureProvider + } -type resultConstraint interface { - of.BoolResolutionDetail | of.IntResolutionDetail | of.StringResolutionDetail | of.FloatResolutionDetail | of.InterfaceResolutionDetail -} + resultConstraint interface { + of.BoolResolutionDetail | of.IntResolutionDetail | of.StringResolutionDetail | of.FloatResolutionDetail | of.InterfaceResolutionDetail + } -type resultWrapper[R resultConstraint] struct { - result *R -} + resultWrapper[R resultConstraint] struct { + result *R + } +) // buildDefaultResult Creates a default result using reflection via generics func buildDefaultResult[R resultConstraint, DV bool | string | int64 | float64 | interface{}](strategy EvaluationStrategy, defaultValue DV, err error) resultWrapper[R] { diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 88354e448..0c789ac95 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -2,70 +2,165 @@ package multiprovider import ( "context" - "encoding/json" "errors" "fmt" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" + "log/slog" "sync" - err "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" + mperr "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/errors" - "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/hooks" + of "github.com/open-feature/go-sdk/openfeature" ) -var ( - errUniqueName = errors.New("provider names must be unique") +type ( + MultiProvider struct { + providers ProviderMap + metadata of.Metadata + events chan of.Event + status of.State + mu sync.RWMutex + strategy strategies.Strategy + logger *slog.Logger + } + + Configuration struct { + useFallback bool + fallbackProvider *strategies.NamedProvider + logger *slog.Logger + publishEvents bool + metadata *of.Metadata + hooks []of.Hook // Not implemented yet + } + + // EvaluationStrategy Defines a strategy to use for resolving the result from multiple providers + EvaluationStrategy = string + ProviderMap map[string]of.FeatureProvider + Option func(*Configuration) ) -// UniqueNameProvider allows for a unique name to be assigned to a provider during a multi-provider set up. -// The name will be used when reporting errors & results to specify the provider associated. -type UniqueNameProvider struct { - Provider openfeature.FeatureProvider - UniqueName string +const ( + // StrategyFirstMatch First provider whose response that is not FlagNotFound will be returned. This is executed + // sequentially, and not in parallel. + StrategyFirstMatch EvaluationStrategy = strategies.StrategyFirstMatch + // StrategyFirstSuccess First provider response that is not an error will be returned. This is executed in parallel + StrategyFirstSuccess EvaluationStrategy = strategies.StrategyFirstSuccess + // StrategyComparison All providers are called in parallel. If all responses agree the value will be returned. + // Otherwise, the value from the designated fallback provider's response will be returned. The fallback provider + // will be assigned to the first provider registered. (NOT YET IMPLEMENTED, SUBJECT TO CHANGE) + StrategyComparison EvaluationStrategy = "comparison" +) + +var _ of.FeatureProvider = (*MultiProvider)(nil) + +// MultiProvider implements of `FeatureProvider` in a way to accept an array of providers. + +func (m ProviderMap) AsNamedProviderSlice() []*strategies.NamedProvider { + s := make([]*strategies.NamedProvider, 0, len(m)) + for name, provider := range m { + s = append(s, &strategies.NamedProvider{Name: name, Provider: provider}) + } + + return s } -// MultiMetadata defines the return of the MultiProvider metadata with the aggregated data of all the providers. -type MultiMetadata struct { - Name string `json:"name"` - OriginalMetadata map[string]openfeature.Metadata `json:"originalMetadata"` +func (m ProviderMap) buildMetadata() of.Metadata { + var separator string + metaName := "MultiProvider {" + for name, provider := range m { + metaName = fmt.Sprintf("%s%s%s: %s", metaName, separator, name, provider.Metadata().Name) + if separator == "" { + separator = ", " + } + } + metaName += "}" + return of.Metadata{ + Name: metaName, + } } -var _ openfeature.FeatureProvider = (*MultiProvider)(nil) +func WithLogger(l *slog.Logger) Option { + return func(conf *Configuration) { + conf.logger = l + } +} -// MultiProvider implements openfeature `FeatureProvider` in a way to accept an array of providers. -type MultiProvider struct { - providersEntries []UniqueNameProvider - providersEntriesByName map[string]UniqueNameProvider - AggregatedMetadata MultiMetadata - events chan openfeature.Event - status openfeature.State - mu sync.Mutex - strategy strategies.Strategy +func WithFallbackProvider(p of.FeatureProvider, name string) Option { + return func(conf *Configuration) { + conf.fallbackProvider = &strategies.NamedProvider{ + Provider: p, + Name: name, + } + conf.useFallback = true + } +} + +func WithNamedFallbackProvider(p *strategies.NamedProvider) Option { + return func(conf *Configuration) { + conf.fallbackProvider = p + conf.useFallback = true + } +} + +func WithEventPublishing() Option { + return func(conf *Configuration) { + conf.publishEvents = true + } +} + +func WithoutEventPublishing() Option { + return func(conf *Configuration) { + conf.publishEvents = false + } } // NewMultiProvider returns the unified interface of multiple providers for interaction. -func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy strategies.EvaluationStrategy, logger *hooks.LoggingHook) (*MultiProvider, error) { - multiProvider := &MultiProvider{ - providersEntries: []UniqueNameProvider{}, - providersEntriesByName: map[string]UniqueNameProvider{}, - AggregatedMetadata: MultiMetadata{ - Name: "multiprovider", - OriginalMetadata: map[string]openfeature.Metadata{}, - }, +func NewMultiProvider(providerMap ProviderMap, evaluationStrategy EvaluationStrategy, options ...Option) (*MultiProvider, error) { + if len(providerMap) == 0 { + return nil, errors.New("providerMap cannot be nil or empty") + } + // Validate Providers + for name, provider := range providerMap { + if name == "" { + return nil, errors.New("provider name cannot be the empty string") + } + + if provider == nil { + return nil, fmt.Errorf("provider %s cannot be nil", name) + } + } + + config := &Configuration{ + logger: slog.Default(), + } + + for _, opt := range options { + opt(config) + } + + var eventChannel chan of.Event + if config.publishEvents { + eventChannel = make(chan of.Event) } - err := multiProvider.registerProviders(passedProviders) - if err != nil { - return nil, err + logger := config.logger + if logger == nil { + logger = slog.Default() + } + + multiProvider := &MultiProvider{ + providers: providerMap, + events: eventChannel, + logger: logger, + metadata: providerMap.buildMetadata(), } var strategy strategies.Strategy switch evaluationStrategy { - case strategies.StrategyFirstMatch: - strategy = strategies.NewFirstMatchStrategy(multiProvider.Providers()) - case strategies.StrategyFirstSuccess: - strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers()) + case StrategyFirstMatch: + strategy = strategies.NewFirstMatchStrategy(multiProvider.Providers(), logger, eventChannel) + case StrategyFirstSuccess: + strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers(), logger, eventChannel) default: return nil, fmt.Errorf("%s is an unknown evalutation strategy", strategy) } @@ -74,114 +169,69 @@ func NewMultiProvider(passedProviders []UniqueNameProvider, evaluationStrategy s return multiProvider, nil } -func (mp *MultiProvider) Providers() []UniqueNameProvider { - return mp.providersEntries +func (mp *MultiProvider) Providers() []*strategies.NamedProvider { + return mp.providers.AsNamedProviderSlice() } -func (mp *MultiProvider) ProvidersByName() map[string]UniqueNameProvider { - return mp.providersEntriesByName -} - -func (mp *MultiProvider) ProviderByName(name string) (UniqueNameProvider, bool) { - provider, exists := mp.providersEntriesByName[name] - return provider, exists +func (mp *MultiProvider) ProvidersByName() ProviderMap { + return mp.providers } func (mp *MultiProvider) EvaluationStrategy() string { return mp.strategy.Name() } -// registerProviders ensures that when setting up an instant of MultiProvider the providers provided either have a unique name or the base `metadata.Name` is made unique by adding an indexed based number to it. -// registerProviders also stores the providers by their unique name and in an array for easy usage. -func (mp *MultiProvider) registerProviders(providers []UniqueNameProvider) error { - providersByName := make(map[string][]UniqueNameProvider) - - for _, provider := range providers { - uniqueName := provider.UniqueName - - if _, exists := providersByName[uniqueName]; exists { - return errUniqueName - } - - if uniqueName == "" { - providersByName[provider.Provider.Metadata().Name] = append(providersByName[provider.Provider.Metadata().Name], provider) - } else { - providersByName[uniqueName] = append(providersByName[uniqueName], provider) - } - } - - for name, providers := range providersByName { - if len(providers) == 1 { - providers[0].UniqueName = name - mp.providersEntries = append(mp.providersEntries, providers[0]) - mp.providersEntriesByName[name] = providers[0] - mp.AggregatedMetadata.OriginalMetadata[name] = providers[0].Provider.Metadata() - } else { - for i, provider := range providers { - uniqueName := fmt.Sprintf("%s-%d", name, i+1) - provider.UniqueName = uniqueName - mp.providersEntries = append(mp.providersEntries, provider) - mp.providersEntriesByName[uniqueName] = provider - mp.AggregatedMetadata.OriginalMetadata[uniqueName] = provider.Provider.Metadata() - } - } - } - return nil -} - // Metadata provides the name `multiprovider` and the names of each provider passed. -func (mp *MultiProvider) Metadata() openfeature.Metadata { - metaJSON, _ := json.Marshal(mp.AggregatedMetadata) - - return openfeature.Metadata{Name: string(metaJSON)} +func (mp *MultiProvider) Metadata() of.Metadata { + return mp.metadata } -// Hooks returns a collection of openfeature.Hook defined by this provider -func (mp *MultiProvider) Hooks() []openfeature.Hook { +// Hooks returns a collection of of.Hook defined by this provider +func (mp *MultiProvider) Hooks() []of.Hook { // Hooks that should be included with the provider - return []openfeature.Hook{} + return []of.Hook{} } // BooleanEvaluation returns a boolean flag -func (mp *MultiProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { +func (mp *MultiProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { return mp.strategy.BooleanEvaluation(ctx, flag, defaultValue, evalCtx) } // StringEvaluation returns a string flag -func (mp *MultiProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { +func (mp *MultiProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail { return mp.strategy.StringEvaluation(ctx, flag, defaultValue, evalCtx) } // FloatEvaluation returns a float flag -func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { +func (mp *MultiProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { return mp.strategy.FloatEvaluation(ctx, flag, defaultValue, evalCtx) } // IntEvaluation returns an int flag -func (mp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { +func (mp *MultiProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { return mp.strategy.IntEvaluation(ctx, flag, defaultValue, evalCtx) } // ObjectEvaluation returns an object flag -func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { +func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { return mp.strategy.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) } // Init will run the initialize method for all of provides and aggregate the errors. -func (mp *MultiProvider) Init(evalCtx openfeature.EvaluationContext) error { +func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { var wg sync.WaitGroup - errChan := make(chan err.StateErr, len(mp.providersEntries)) + errChan := make(chan mperr.StateErr) - for _, provider := range mp.providersEntries { + for name, provider := range mp.providers { wg.Add(1) - go func(p UniqueNameProvider) { + go func(p of.FeatureProvider, name string) { defer wg.Done() - if stateHandle, ok := p.Provider.(openfeature.StateHandler); ok { + if stateHandle, ok := provider.(of.StateHandler); ok { if initErr := stateHandle.Init(evalCtx); initErr != nil { - errChan <- err.StateErr{ProviderName: p.UniqueName, Err: initErr, ErrMessage: initErr.Error()} + errChan <- mperr.StateErr{ProviderName: name, Err: initErr, ErrMessage: initErr.Error()} } } - }(provider) + }(provider, name) } go func() { @@ -189,35 +239,40 @@ func (mp *MultiProvider) Init(evalCtx openfeature.EvaluationContext) error { close(errChan) }() - var errors []err.StateErr + errs := make([]mperr.StateErr, 0, 1) for err := range errChan { - errors = append(errors, err) + errs = append(errs, err) } - if len(errors) > 0 { - var aggErr err.AggregateError - aggErr.Construct(errors) - mp.status = openfeature.ErrorState + if len(errs) > 0 { + var aggErr mperr.AggregateError + aggErr.Construct(errs) + mp.mu.RLock() + defer mp.mu.Unlock() + mp.status = of.ErrorState + return &aggErr } - mp.status = openfeature.ReadyState + mp.mu.RLock() + defer mp.mu.Unlock() + mp.status = of.ReadyState return nil } -func (mp *MultiProvider) Status() openfeature.State { +func (mp *MultiProvider) Status() of.State { return mp.status } func (mp *MultiProvider) Shutdown() { var wg sync.WaitGroup - for _, provider := range mp.providersEntries { + for _, provider := range mp.providers { wg.Add(1) - go func(p UniqueNameProvider) { + go func(p of.FeatureProvider) { defer wg.Done() - if stateHandle, ok := p.Provider.(openfeature.StateHandler); ok { + if stateHandle, ok := provider.(of.StateHandler); ok { stateHandle.Shutdown() } }(provider) @@ -226,6 +281,6 @@ func (mp *MultiProvider) Shutdown() { wg.Wait() } -func (mp *MultiProvider) EventChannel() <-chan openfeature.Event { +func (mp *MultiProvider) EventChannel() <-chan of.Event { return mp.events } diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 527a3f6ed..3f2c94f2d 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -3,15 +3,20 @@ package multiprovider import ( "encoding/json" "fmt" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" + of "github.com/open-feature/go-sdk/openfeature" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "golang.org/x/exp/maps" + "regexp" "strings" "testing" "time" - errs "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal" - strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/strategies" + mperrs "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/errors" "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/hooks" oft "github.com/open-feature/go-sdk/openfeature/testing" ) @@ -49,224 +54,79 @@ func (m *MockProvider) Metadata() openfeature.Metadata { return openfeature.Metadata{Name: m.MockMeta} } -func NewMockProvider(initCount *int, shutCount *int, testErr string, initDelay int, shutDelay int, meta string) *MockProvider { - return &MockProvider{ - TestProvider: oft.NewTestProvider(), - InitCount: initCount, - ShutCount: shutCount, - TestErr: testErr, - InitDelay: initDelay, - ShutDelay: shutDelay, - MockMeta: meta, - } -} - -// todo: the test that check the unique names of the providers are too dependent on order needs to use a map or something to better verify the correct unique name to the provider but not dependent on the order of a slice - func TestMultiProvider_ProvidersMethod(t *testing.T) { testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } - - providers := mp.Providers() + providers := make(ProviderMap) + providers["provider1"] = testProvider1 + providers["provider2"] = testProvider2 - if len(providers) != 2 { - t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) - } + mp, err := NewMultiProvider(providers, strategies.StrategyFirstSuccess) + require.NoError(t, err) - if providers[0].UniqueName != "provider1" { - t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", providers[0].UniqueName) - } - if providers[1].UniqueName != "provider2" { - t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", providers[1].UniqueName) - } + p := mp.Providers() + assert.Len(t, p, 2) + assert.Regexp(t, regexp.MustCompile("provider[1-2]"), p[0].Name) + assert.NotNil(t, p[0].Provider) + assert.Regexp(t, regexp.MustCompile("provider[1-2]"), p[1].Name) + assert.NotNil(t, p[1].Provider) } func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } - - providers := mp.ProvidersByName() - - if len(providers) != 2 { - t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) - } - - if provider, exists := providers["provider1"]; exists { - if provider.UniqueName != "provider1" { - t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", provider.UniqueName) - } - if provider.Provider != testProvider1 { - t.Errorf("Expected unique provider name to be: 'provider1', got: '%s'", provider.UniqueName) - } - } else { - t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider1") - } - - if provider, exists := providers["provider2"]; exists { - if provider.UniqueName != "provider2" { - t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) - } - if provider.Provider != testProvider2 { - t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) - } - } else { - t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider2") - } - -} - -func TestMultiProvider_ProviderByNameMethod(t *testing.T) { - testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() - - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } + providers := make(ProviderMap) + providers["provider1"] = testProvider1 + providers["provider2"] = testProvider2 - providers := mp.ProvidersByName() + mp, err := NewMultiProvider(providers, strategies.StrategyFirstMatch) + require.NoError(t, err) - if len(providers) != 2 { - t.Errorf("Expected there to be '2' providers as passed but got: '%d'", len(providers)) - } - if provider, exists := mp.ProviderByName("provider2"); exists { - if provider.UniqueName != "provider2" { - t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) - } - if provider.Provider != testProvider2 { - t.Errorf("Expected unique provider name to be: 'provider2', got: '%s'", provider.UniqueName) - } - } else { - t.Errorf("Expected there to be a provider with the key of '%s', but none was found.", "provider1") - } + p := mp.ProvidersByName() + require.Len(t, p, 2) + require.Contains(t, maps.Keys(p), "provider1") + assert.Equal(t, p["provider1"], testProvider1) + require.Contains(t, maps.Keys(p), "provider2") + assert.Equal(t, p["provider2"], testProvider2) } func TestMultiProvider_MetaData(t *testing.T) { - initializations := 0 - shutdowns := 0 - testProvider1 := oft.NewTestProvider() - testProvider2 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test2") - - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } - - expectedJSON, err := json.Marshal(MultiMetadata{ - Name: "multiprovider", - OriginalMetadata: map[string]openfeature.Metadata{ - "provider1": openfeature.Metadata{Name: "NoopProvider"}, - "provider2": openfeature.Metadata{Name: "test2"}, - }, + ctrl := gomock.NewController(t) + testProvider2 := strategies.NewMockFeatureProvider(ctrl) + testProvider2.EXPECT().Metadata().Return(of.Metadata{ + Name: "MockProvider", }) - if err != nil { - t.Errorf("Error in JSON marshal of the expected answer, '%s'", err) - } - expectedMetadata := openfeature.Metadata{Name: string(expectedJSON)} + providers := make(ProviderMap) + providers["provider1"] = testProvider1 + providers["provider2"] = testProvider2 - if mp.Metadata() != expectedMetadata { - t.Errorf("Expected to see the aggregated metadata of all passed providers: '%s', got: '%s'", expectedMetadata, mp.Metadata()) - } + mp, err := NewMultiProvider(providers, strategies.StrategyFirstSuccess) + require.NoError(t, err) + + metadata := mp.Metadata() + require.NotZero(t, metadata) + assert.Equal(t, "MultiProvider{provider1:NoopProvider, provider2:MockProvider}", metadata.Name) } func TestMultiProvider_Init(t *testing.T) { - initializations := 0 - shutdowns := 0 + ctrl := gomock.NewController(t) - testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test1") + testProvider1 := strategies.NewMockFeatureProvider(ctrl) testProvider2 := oft.NewTestProvider() - testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 1, 0, "test3") + testProvider3 := strategies.NewMockFeatureProvider(ctrl) - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } + providers := make(ProviderMap) + providers["provider1"] = testProvider1 + providers["provider2"] = testProvider2 + providers["provider3"] = testProvider3 - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, { - Provider: testProvider3, - UniqueName: "provider3", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } + mp, err := NewMultiProvider(providers, strategies.StrategyFirstMatch) + require.NoError(t, err) attributes := map[string]interface{}{ "foo": "bar", @@ -274,53 +134,24 @@ func TestMultiProvider_Init(t *testing.T) { evalCtx := openfeature.NewTargetlessEvaluationContext(attributes) err = mp.Init(evalCtx) - if err != nil { - t.Errorf("Expected the initialization process to be successful, got error: '%s'", err) - } - - if mp.status != openfeature.ReadyState { - t.Errorf("Expected the state of the multiprovider to be in 'Error', got: '%s'", mp.status) - } - - if initializations == 0 { - t.Errorf("Expected there to be initializations, but none were ran.") - } - - if initializations != 2 { - t.Errorf("Expected there to be '2' init steps ran, but got: '%d'.", initializations) - } - + require.NoError(t, err) + assert.Equal(t, of.ReadyState, mp.status) } func TestMultiProvider_InitErrorWithProvider(t *testing.T) { - initializations := 0 - shutdowns := 0 + ctrl := gomock.NewController(t) - testProvider1 := oft.NewTestProvider() - testProvider2 := NewMockProvider(&initializations, &shutdowns, "test error 1 end", 0, 0, "test2") - testProvider3 := NewMockProvider(&initializations, &shutdowns, "test error 2 end", 0, 0, "test3") + testProvider1 := strategies.NewMockFeatureProvider(ctrl) + testProvider2 := oft.NewTestProvider() + testProvider3 := strategies.NewMockFeatureProvider(ctrl) - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } + providers := make(ProviderMap) + providers["provider1"] = testProvider1 + providers["provider2"] = testProvider2 + providers["provider3"] = testProvider3 - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, { - Provider: testProvider3, - UniqueName: "provider3", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } + mp, err := NewMultiProvider(providers, strategies.StrategyFirstMatch) + require.NoError(t, err) attributes := map[string]interface{}{ "foo": "bar", @@ -328,222 +159,35 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { evalCtx := openfeature.NewTargetlessEvaluationContext(attributes) err = mp.Init(evalCtx) - if err == nil { - t.Errorf("Expected the initialization process to throw an error.") - } + require.Error(t, err) - var errors []errs.StateErr + var errors []mperrs.StateErr fullErr := err.Error() fullErrArr := strings.SplitAfterN(fullErr, "end", 2) errJSON := fullErrArr[1] errMsg := fullErrArr[0] + assert.Contains(t, errMsg, "Provider errors occurred:") - if !strings.Contains(errMsg, "Provider errors occurred:") { - t.Errorf("Expected the first line of error message to contain: '%s', got: '%s'", "Provider errors occurred:", errMsg) - } - - if err = json.Unmarshal([]byte(errJSON), &errors); err != nil { - t.Errorf("Failed to unmarshal error details: %v", err) - } - - if len(errors) != 2 { - t.Errorf("Expected there to be '2' errors found, got: '%d'", len(errors)) - } - - if mp.status != openfeature.ErrorState { - t.Errorf("Expected the state of the multiprovider to be in 'Error', got: '%s'", mp.status) - } + err = json.Unmarshal([]byte(errJSON), &errors) + require.NoError(t, err) + assert.Len(t, errors, 2) + assert.Equal(t, of.ErrorState, mp.status) } func TestMultiProvider_Shutdown(t *testing.T) { - initializations := 0 - shutdowns := 0 + ctrl := gomock.NewController(t) - testProvider1 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test1") + testProvider1 := strategies.NewMockFeatureProvider(ctrl) testProvider2 := oft.NewTestProvider() - testProvider3 := NewMockProvider(&initializations, &shutdowns, "", 0, 2, "test3") + testProvider3 := strategies.NewMockFeatureProvider(ctrl) - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider1", - }, { - Provider: testProvider2, - UniqueName: "provider2", - }, { - Provider: testProvider3, - UniqueName: "provider3", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } + providers := make(ProviderMap) + providers["provider1"] = testProvider1 + providers["provider2"] = testProvider2 + providers["provider3"] = testProvider3 + mp, err := NewMultiProvider(providers, strategies.StrategyFirstMatch) + require.NoError(t, err) mp.Shutdown() - - if shutdowns == 0 { - t.Errorf("Expected there to be shutdowns, but none were ran.") - } - - if shutdowns != 2 { - t.Errorf("Expected there to be '2' shutdown steps ran, but got: '%d'.", shutdowns) - } -} - -func TestNewMultiProvider_ProviderUniqueNames(t *testing.T) { - initializations := 0 - shutdowns := 0 - - testProvider1 := oft.NewTestProvider() - testProvider2 := NewMockProvider(&initializations, &shutdowns, "", 0, 0, "test2") - - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - }, { - Provider: testProvider2, - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } - - providerEntries := mp.Providers() - - if providerEntries[0].UniqueName != "NoopProvider" { - t.Errorf("Expected unique provider name to be: 'NoopProvider', got: '%s'", providerEntries[0].UniqueName) - } - - if providerEntries[1].UniqueName != "test2" { - t.Errorf("Expected unique provider name to be: 'test2', got: '%s'", providerEntries[1].UniqueName) - } - - if len(providerEntries) != 2 { - t.Errorf("Expected there to be 2 provider entries, got: '%d'", len(providerEntries)) - } -} - -func TestNewMultiProvider_DuplicateProviderGenerateUniqueNames(t *testing.T) { - testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() - testProvider3 := oft.NewTestProvider() - testProvider4 := oft.NewTestProvider() - - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - }, { - Provider: testProvider2, - }, { - Provider: testProvider3, - }, { - Provider: testProvider4, - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } - - providerEntries := mp.Providers() - - if len(providerEntries) != 4 { - t.Errorf("Expected there to be 4 provider entries, got: '%d'", len(providerEntries)) - } - - if providerEntries[0].UniqueName != "NoopProvider-1" { - t.Errorf("Expected unique provider name to be: 'NoopProvider-1', got: '%s'", providerEntries[0].UniqueName) - } - if providerEntries[1].UniqueName != "NoopProvider-2" { - t.Errorf("Expected unique provider name to be: 'NoopProvider-2', got: '%s'", providerEntries[1].UniqueName) - } - if providerEntries[2].UniqueName != "NoopProvider-3" { - t.Errorf("Expected unique provider name to be: 'NoopProvider-3', got: '%s'", providerEntries[2].UniqueName) - } - if providerEntries[3].UniqueName != "NoopProvider-4" { - t.Errorf("Expected unique provider name to be: 'NoopProvider-4', got: '%s'", providerEntries[3].UniqueName) - } - -} -func TestNewMultiProvider_ProvidersUsePassedNames(t *testing.T) { - testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() - - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - mp, err := NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "theFirst", - }, { - Provider: testProvider2, - UniqueName: "theSecond", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err != nil { - t.Errorf("Expected the multiprovider to successfully make an instance, '%s'", err) - } - - providerEntries := mp.Providers() - - if providerEntries[0].UniqueName != "theFirst" { - t.Errorf("Expected unique provider name to be: 'theFirst', got: '%s'", providerEntries[0].UniqueName) - } - if providerEntries[1].UniqueName != "theSecond" { - t.Errorf("Expected unique provider name to be: 'theSecond', got: '%s'", providerEntries[1].UniqueName) - } - - if len(providerEntries) != 2 { - t.Errorf("Expected there to be 2 provider entries, got: '%d'", len(providerEntries)) - } -} - -func TestNewMultiProvider_ProvidersErrorNameNotUnique(t *testing.T) { - testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() - - defaultLogger, err := hooks.NewLoggingHook(false) - if err != nil { - t.Errorf("Issue setting up logger,'%s'", err) - } - - _, err = NewMultiProvider([]UniqueNameProvider{ - { - Provider: testProvider1, - UniqueName: "provider", - }, { - Provider: testProvider2, - UniqueName: "provider", - }, - }, strategies.BaseEvaluationStrategy{}, defaultLogger) - - if err == nil { - t.Errorf("Expected the multiprovider to have an error") - } - - if err.Error() != "provider names must be unique" { - t.Errorf("Expected the multiprovider to have an error of: '%s', got: '%s'", errUniqueName, err.Error()) - } } From b0b627e9e9e69b17b46a632cf1d1f02ba141d5ef Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Sat, 19 Apr 2025 14:02:30 -0400 Subject: [PATCH 31/68] doc: Update README Signed-off-by: Jordan Blacker --- providers/multi-provider/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/providers/multi-provider/README.md b/providers/multi-provider/README.md index 005fc2d5d..41adc9060 100644 --- a/providers/multi-provider/README.md +++ b/providers/multi-provider/README.md @@ -27,13 +27,13 @@ go get github.com/open-feature/go-sdk ```go import ( "github.com/open-feature/go-sdk/openfeature" - "github.com/open-feature/go-sdk/openfeature/hooks" + mp "github.com/open-feature/go-sdk-contrib/providers/multi-provider" ) -namedProviderA := UniqueNameProvider{ Provider: myProvider, UniqueName: "Provider A" } -namedProviderB := UniqueNameProvider{ Provider: myOtherProvider, UniqueName: "Provider B" } - -provider, err := NewMultiProvider([]UniqueNameProvider{namedProviderA, namedProviderB}, StrategyFirstMatch, hooks.NewLoggingHook(false)) +providers := make(mp.ProviderMap) +providers["providerA"] = providerA +providers["providerB"] = providerB +provider, err := mp.NewMultiProvider(providers, mp.StrategyFirstMatch, WithLogger(myLogger)) openfeature.SetProvider(provider) ``` From faa8a38128c87d2572847c4d3440cc0439d64019 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Sat, 19 Apr 2025 14:02:48 -0400 Subject: [PATCH 32/68] cleanup: Remove unused public strategy package Signed-off-by: Jordan Blacker --- .../strategies/base_strategy_test.go | 94 ------------- .../strategies/base_stratgey.go | 127 ------------------ 2 files changed, 221 deletions(-) delete mode 100644 providers/multi-provider/strategies/base_strategy_test.go delete mode 100644 providers/multi-provider/strategies/base_stratgey.go diff --git a/providers/multi-provider/strategies/base_strategy_test.go b/providers/multi-provider/strategies/base_strategy_test.go deleted file mode 100644 index ec387f52a..000000000 --- a/providers/multi-provider/strategies/base_strategy_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package strategies - -import ( - "testing" - - "github.com/open-feature/go-sdk/openfeature" - "github.com/stretchr/testify/assert" -) - -func TestShouldEvaluateThisProvider(t *testing.T) { - strategy := BaseEvaluationStrategy{} - - tests := []struct { - status openfeature.State - shouldEval bool - }{ - {openfeature.NotReadyState, false}, - {openfeature.FatalState, false}, - {openfeature.ReadyState, true}, - } - - for _, test := range tests { - ctx := StrategyPerProviderContext{ - Status: test.status, - } - result := strategy.ShouldEvaluateThisProvider(ctx, openfeature.EvaluationContext{}) - assert.Equal(t, test.shouldEval, result) - } -} - -func TestShouldEvaluateNextProvider(t *testing.T) { - strategy := BaseEvaluationStrategy{} - ctx := StrategyPerProviderContext{} - result := ResolutionDetail[openfeature.Type]{} - assert.True(t, strategy.ShouldEvaluateNextProvider(ctx, openfeature.EvaluationContext{}, result)) -} - -func TestDetermineFinalResultPanics(t *testing.T) { - strategy := BaseEvaluationStrategy{} - - assert.Panics(t, func() { - strategy.DetermineFinalResult(StrategyEvaluationContext{}, openfeature.EvaluationContext{}, nil) - }) -} - -func TestHasError(t *testing.T) { - noError := ResolutionDetail[openfeature.Type]{} - withError := ResolutionDetail[openfeature.Type]{ - ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewGeneralResolutionError("something broke"), - }, - } - - assert.False(t, HasError(noError)) - assert.True(t, HasError(withError)) -} - -func TestHasErrorWithCode(t *testing.T) { - resWithCode := ResolutionDetail[openfeature.Type]{ - ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewParseErrorResolutionError("bad parse"), - }, - } - assert.True(t, HasErrorWithCode(resWithCode, openfeature.ParseErrorCode)) - assert.False(t, HasErrorWithCode(resWithCode, openfeature.GeneralCode)) -} - -func TestCollectProviderErrors(t *testing.T) { - err1 := openfeature.NewParseErrorResolutionError("bad parse") - err2 := openfeature.NewFlagNotFoundResolutionError("what flag") - - resolutions := []ResolutionDetail[openfeature.Type]{ - {ProviderName: "prov1"}, - {ProviderName: "prov2", ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ResolutionError: err1}}, - {ProviderName: "prov3", ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ResolutionError: err2}}, - } - - final := CollectProviderErrors(resolutions) - assert.Len(t, final.Errors, 2) - assert.Equal(t, "prov2", final.Errors[0].ProviderName) - assert.Equal(t, "prov3", final.Errors[1].ProviderName) -} - -func TestResolutionToFinal(t *testing.T) { - res := ResolutionDetail[openfeature.Type]{ - Value: openfeature.Boolean, - ProviderName: "myProvider", - } - - final := ResolutionToFinal(res) - assert.Equal(t, "myProvider", final.ProviderName) - assert.Equal(t, res, final.Details) - assert.Empty(t, final.Errors) -} diff --git a/providers/multi-provider/strategies/base_stratgey.go b/providers/multi-provider/strategies/base_stratgey.go deleted file mode 100644 index 2e5429735..000000000 --- a/providers/multi-provider/strategies/base_stratgey.go +++ /dev/null @@ -1,127 +0,0 @@ -package strategies - -import ( - "strings" - - "github.com/open-feature/go-sdk/openfeature" -) - -// RunModeValue indicates whether providers are evaluated sequentially or in parallel. -type RunModeValue string - -const ( - RunModeSequential = "sequential" - RunModeParallel = "parallel" -) - -// StrategyEvaluationContext contains flag-wide info. -type StrategyEvaluationContext struct { - FlagKey string - FlagType openfeature.Type -} - -// StrategyPerProviderContext -type StrategyPerProviderContext struct { - StrategyEvaluationContext - Provider openfeature.FeatureProvider - ProviderName string - Status openfeature.State -} - -type ProviderError struct { - ProviderName string - Error openfeature.ResolutionError -} - -type ResolutionDetail[T openfeature.Type] struct { - Value T - ProviderName string - Provider openfeature.FeatureProvider - openfeature.ProviderResolutionDetail -} - -type FinalResult[T openfeature.Type] struct { - Provider openfeature.FeatureProvider - ProviderName string - Details ResolutionDetail[T] - Errors []ProviderError -} - -// EvaluationStrategy is the base functions needed for a strategy -type EvaluationStrategy interface { - // ShouldEvaluateThisProvider determines if the provider should be evaluated - ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool - - // ShouldEvaluateNextProvider determines whether the next provider should be evaluated - ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result ResolutionDetail[openfeature.Type]) bool - - // DetermineFinalResult decides the final result from the evaluated providers - DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []ResolutionDetail[openfeature.Type]) FinalResult[openfeature.Type] -} - -// BaseEvaluationStrategy Provides default implementations for the methods that can be used to create user defined strategies. -// DetermineFinalResult must be fully implemented for the user to create custom strategies -// ShouldEvaluateThisProvider & ShouldEvaluateNextProvider have default implementations -type BaseEvaluationStrategy struct { - RunMode RunModeValue -} - -// ShouldEvaluateThisProvider checks if the provider should be evaluated -func (s *BaseEvaluationStrategy) ShouldEvaluateThisProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext) bool { - if strategyContext.Status == openfeature.NotReadyState || strategyContext.Status == openfeature.FatalState { - return false - } - return true -} - -// ShouldEvaluateNextProvider checks if the next provider should be evaluated based on the result of the previous provider -func (s *BaseEvaluationStrategy) ShouldEvaluateNextProvider(strategyContext StrategyPerProviderContext, evalContext openfeature.EvaluationContext, result ResolutionDetail[openfeature.Type]) bool { - return true -} - -// DetermineFinalResult needs to be implemented by the user to properly define custom strategy -func (s *BaseEvaluationStrategy) DetermineFinalResult(strategyContext StrategyEvaluationContext, evalContext openfeature.EvaluationContext, results []ResolutionDetail[openfeature.Type]) FinalResult[openfeature.Type] { - panic("DetermineFinalResult must be implemented by the custom strategy") -} - -// HasError helper function used to determine if a resolution has an error -func HasError[T openfeature.Type](resolution ResolutionDetail[T]) bool { - return resolution.ResolutionError != (openfeature.ResolutionError{}) -} - -// HasErrorWithCode helper function to determine if a resolution has a specific error code -func HasErrorWithCode[T openfeature.Type](resolution ResolutionDetail[T], code openfeature.ErrorCode) bool { - if !HasError(resolution) { - return false - } - - return strings.HasPrefix(resolution.ResolutionError.Error(), string(code)) -} - -// CollectProviderErrors helper function to collate the errors to add to the final result struct -func CollectProviderErrors[T openfeature.Type](resolutions []ResolutionDetail[T]) FinalResult[T] { - var errs []ProviderError - - for _, resolution := range resolutions { - if HasError(resolution) { - errs = append(errs, ProviderError{ - ProviderName: resolution.ProviderName, - Error: resolution.ResolutionError, - }) - } - } - - return FinalResult[T]{ - Errors: errs, - } -} - -// ResolutionToFinal converts successful resolution to final result -func ResolutionToFinal[T openfeature.Type](resolution ResolutionDetail[T]) FinalResult[T] { - return FinalResult[T]{ - Provider: resolution.Provider, - ProviderName: resolution.ProviderName, - Details: resolution, - Errors: []ProviderError{}, - } -} From 4f6ccb79dbd1d4f9177a9504659af66cee015d5a Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Sat, 19 Apr 2025 18:34:57 -0400 Subject: [PATCH 33/68] fix: correct test names for first_success_test Signed-off-by: Jordan Blacker --- .../internal/strategies/first_success_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/providers/multi-provider/internal/strategies/first_success_test.go b/providers/multi-provider/internal/strategies/first_success_test.go index 170f29ac0..be5810594 100644 --- a/providers/multi-provider/internal/strategies/first_success_test.go +++ b/providers/multi-provider/internal/strategies/first_success_test.go @@ -6,18 +6,18 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { // TODO } -func Test_FirstStrategy_StringEvaluation(t *testing.T) { +func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { // TODO } -func Test_FirstStrategy_IntEvaluation(t *testing.T) { +func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { // TODO } -func Test_FirstStrategy_FloatEvaluation(t *testing.T) { +func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { // TODO } -func Test_FirstStrategy_ObjectEvaluation(t *testing.T) { +func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { // TODO } From 44d66634a87e97782f9c3a812d0faa1fc3d5c2c2 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Sat, 19 Apr 2025 18:36:15 -0400 Subject: [PATCH 34/68] feat: implement comparison strategy Signed-off-by: Jordan Blacker --- .../internal/strategies/comparison.go | 249 ++++++++++++++++++ .../internal/strategies/comparison_test.go | 23 ++ .../internal/strategies/strategies.go | 27 +- providers/multi-provider/pkg/providers.go | 15 +- 4 files changed, 302 insertions(+), 12 deletions(-) create mode 100644 providers/multi-provider/internal/strategies/comparison.go create mode 100644 providers/multi-provider/internal/strategies/comparison_test.go diff --git a/providers/multi-provider/internal/strategies/comparison.go b/providers/multi-provider/internal/strategies/comparison.go new file mode 100644 index 000000000..197891ac0 --- /dev/null +++ b/providers/multi-provider/internal/strategies/comparison.go @@ -0,0 +1,249 @@ +package strategies + +import ( + "cmp" + "context" + "errors" + of "github.com/open-feature/go-sdk/openfeature" + "golang.org/x/sync/errgroup" + "strings" +) + +type ( + ComparisonStrategy struct { + providers []*NamedProvider + fallbackProvider of.FeatureProvider + } + + evaluator[R resultConstraint] func(ctx context.Context, p *NamedProvider) resultWrapper[R] +) + +var _ Strategy = (*ComparisonStrategy)(nil) + +func NewComparisonStrategy(providers []*NamedProvider, fallbackProvider of.FeatureProvider) *ComparisonStrategy { + return &ComparisonStrategy{ + providers: providers, + fallbackProvider: fallbackProvider, + } +} + +func (c ComparisonStrategy) Name() EvaluationStrategy { + return StrategyComparison +} + +func (c ComparisonStrategy) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.BoolResolutionDetail] { + result := p.Provider.BooleanEvaluation(ctx, flag, defaultValue, evalCtx) + return resultWrapper[of.BoolResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + results, metadata := evaluateComparison[of.BoolResolutionDetail, bool](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) + if len(results) == 1 { + results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name + return *results[0].result + } + + reason := ReasonAggregated + if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { + reason = ReasonAggregatedFallback + } + + return of.BoolResolutionDetail{ + Value: results[0].result.Value, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Reason: reason, + Variant: results[0].detail.Variant, + FlagMetadata: metadata, + }, + } +} + +func (c ComparisonStrategy) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail { + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.StringResolutionDetail] { + result := p.Provider.StringEvaluation(ctx, flag, defaultValue, evalCtx) + return resultWrapper[of.StringResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + results, metadata := evaluateComparison[of.StringResolutionDetail, string](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) + if len(results) == 1 { + results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name + return *results[0].result + } + + reason := ReasonAggregated + if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { + reason = ReasonAggregatedFallback + } + + return of.StringResolutionDetail{ + Value: results[0].result.Value, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Reason: reason, + Variant: results[0].detail.Variant, + FlagMetadata: metadata, + }, + } +} + +func (c ComparisonStrategy) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.FloatResolutionDetail] { + result := p.Provider.FloatEvaluation(ctx, flag, defaultValue, evalCtx) + return resultWrapper[of.FloatResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + results, metadata := evaluateComparison[of.FloatResolutionDetail, float64](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) + if len(results) == 1 { + results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name + return *results[0].result + } + + reason := ReasonAggregated + if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { + reason = ReasonAggregatedFallback + } + + return of.FloatResolutionDetail{ + Value: results[0].result.Value, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Reason: reason, + Variant: results[0].detail.Variant, + FlagMetadata: metadata, + }, + } +} + +func (c ComparisonStrategy) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.IntResolutionDetail] { + result := p.Provider.IntEvaluation(ctx, flag, defaultValue, evalCtx) + return resultWrapper[of.IntResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + results, metadata := evaluateComparison[of.IntResolutionDetail, int64](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) + if len(results) == 1 { + results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name + return *results[0].result + } + + reason := ReasonAggregated + if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { + reason = ReasonAggregatedFallback + } + + return of.IntResolutionDetail{ + Value: results[0].result.Value, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Reason: reason, + Variant: results[0].detail.Variant, + FlagMetadata: metadata, + }, + } +} + +func (c ComparisonStrategy) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { + metadata := make(of.FlagMetadata) + metadata[MetadataStrategyUsed] = StrategyComparison + metadata[MetadataSuccessfulProviderName] = "none" + metadata[MetadataFallbackUsed] = false + + return of.InterfaceResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.NewGeneralResolutionError(ErrAggregationNotAllowedText), + Reason: of.DefaultReason, + Variant: "", + FlagMetadata: metadata, + }, + } +} + +func evaluateComparison[R resultConstraint, DV bool | string | int64 | float64](ctx context.Context, providers []*NamedProvider, e evaluator[R], fallbackProvider of.FeatureProvider, defaultVal DV) ([]resultWrapper[R], of.FlagMetadata) { + resultChan := make(chan resultWrapper[R]) + ctx, cancel := context.WithCancel(ctx) + defer cancel() + errGrp, ctx := errgroup.WithContext(ctx) + for _, provider := range providers { + p := provider + errGrp.Go(func() error { + result := e(ctx, p) + notFound := result.detail.ResolutionDetail().ErrorCode == of.FlagNotFoundCode + if result.detail.Error() != nil { + return &providerError{ + providerName: p.Name, + err: result.detail.Error(), + } + } + result.name = p.Name + if !notFound { + resultChan <- result + } + return nil + }) + } + + if err := errGrp.Wait(); err != nil { + result := buildDefaultResult[R, DV](StrategyComparison, defaultVal, err) + return []resultWrapper[R]{result}, result.detail.FlagMetadata + } + + // Evaluate Results Are Equal + agreement := true + var resultVal DV + results := make([]resultWrapper[R], 0, len(providers)) + metadata := make(of.FlagMetadata) + metadata[MetadataFallbackUsed] = false + metadata[MetadataStrategyUsed] = StrategyComparison + success := make([]string, 0, len(providers)) + for r := range resultChan { + results = append(results, r) + current := *r.value.(*DV) + resultVal = cmp.Or(resultVal, current) + agreement = agreement && (resultVal == current) + if !agreement { + break + } + metadata[r.name] = r.detail.FlagMetadata + success = append(success, r.name) + } + metadata[MetadataSuccessfulProviderName+"s"] = strings.Join(success, ", ") + + if agreement { + return results, metadata + } + + if fallbackProvider != nil { + fallbackResult := e(ctx, &NamedProvider{Name: "fallback", Provider: fallbackProvider}) + metadata = fallbackResult.detail.FlagMetadata + metadata[MetadataStrategyUsed] = StrategyComparison + metadata[MetadataFallbackUsed] = true + + return []resultWrapper[R]{fallbackResult}, metadata + } + + defaultResult := buildDefaultResult[R, DV](StrategyComparison, defaultVal, errors.New("no fallback provider configured")) + metadata = defaultResult.detail.FlagMetadata + metadata[MetadataSuccessfulProviderName] = "none" + metadata[MetadataFallbackUsed] = false + metadata[MetadataStrategyUsed] = StrategyComparison + return []resultWrapper[R]{defaultResult}, metadata + +} diff --git a/providers/multi-provider/internal/strategies/comparison_test.go b/providers/multi-provider/internal/strategies/comparison_test.go new file mode 100644 index 000000000..101f22332 --- /dev/null +++ b/providers/multi-provider/internal/strategies/comparison_test.go @@ -0,0 +1,23 @@ +package strategies + +import "testing" + +func Test_ComparisonStrategy_BooleanEvaluation(t *testing.T) { + // TODO +} + +func Test_ComparisonStrategy_StringEvaluation(t *testing.T) { + // TODO +} + +func Test_ComparisonStrategy_IntEvaluation(t *testing.T) { + // TODO +} + +func Test_ComparisonStrategy_FloatEvaluation(t *testing.T) { + // TODO +} + +func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { + +} diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index 0178a15d7..8c222dbe9 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -8,10 +8,15 @@ import ( ) const ( - MetadataSuccessfulProviderName = "multiprovider-successful-provider-name" - MetadataStrategyUsed = "multiprovider-strategy-used" - StrategyFirstMatch = "strategy-first-match" - StrategyFirstSuccess = "strategy-first-success" + MetadataSuccessfulProviderName = "multiprovider-successful-provider-name" + MetadataStrategyUsed = "multiprovider-strategy-used" + MetadataFallbackUsed = "multiprovider-fallback-used" + StrategyFirstMatch = "strategy-first-match" + StrategyFirstSuccess = "strategy-first-success" + StrategyComparison = "strategy-comparison" + ReasonAggregated of.Reason = "AGGREGATED" + ReasonAggregatedFallback of.Reason = "AGGREGATED_FALLBACK" + ErrAggregationNotAllowedText = "object evaluation not allowed for non-comparable types" ) type ( @@ -34,15 +39,29 @@ type ( Provider of.FeatureProvider } + providerError struct { + providerName string + err error + } + resultConstraint interface { of.BoolResolutionDetail | of.IntResolutionDetail | of.StringResolutionDetail | of.FloatResolutionDetail | of.InterfaceResolutionDetail } resultWrapper[R resultConstraint] struct { + name string result *R + value any + detail of.ProviderResolutionDetail } ) +var _ error = (*providerError)(nil) + +func (p providerError) Error() string { + return p.providerName + ": " + p.err.Error() +} + // buildDefaultResult Creates a default result using reflection via generics func buildDefaultResult[R resultConstraint, DV bool | string | int64 | float64 | interface{}](strategy EvaluationStrategy, defaultValue DV, err error) resultWrapper[R] { result := *new(R) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 0c789ac95..51625b91f 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -26,7 +26,7 @@ type ( Configuration struct { useFallback bool - fallbackProvider *strategies.NamedProvider + fallbackProvider of.FeatureProvider logger *slog.Logger publishEvents bool metadata *of.Metadata @@ -87,15 +87,12 @@ func WithLogger(l *slog.Logger) Option { func WithFallbackProvider(p of.FeatureProvider, name string) Option { return func(conf *Configuration) { - conf.fallbackProvider = &strategies.NamedProvider{ - Provider: p, - Name: name, - } + conf.fallbackProvider = p conf.useFallback = true } } -func WithNamedFallbackProvider(p *strategies.NamedProvider) Option { +func WithNamedFallbackProvider(p of.FeatureProvider) Option { return func(conf *Configuration) { conf.fallbackProvider = p conf.useFallback = true @@ -158,9 +155,11 @@ func NewMultiProvider(providerMap ProviderMap, evaluationStrategy EvaluationStra var strategy strategies.Strategy switch evaluationStrategy { case StrategyFirstMatch: - strategy = strategies.NewFirstMatchStrategy(multiProvider.Providers(), logger, eventChannel) + strategy = strategies.NewFirstMatchStrategy(multiProvider.Providers()) case StrategyFirstSuccess: - strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers(), logger, eventChannel) + strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers()) + case StrategyComparison: + strategy = strategies.NewComparisonStrategy(multiProvider.Providers(), config.fallbackProvider) default: return nil, fmt.Errorf("%s is an unknown evalutation strategy", strategy) } From 9edebd1fb23ff2935775ab65ff55237c9c5c8390 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Tue, 22 Apr 2025 19:46:58 -0400 Subject: [PATCH 35/68] fix: Patch bug where metadata would not be generated in a stable way Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 8 ++++++-- providers/multi-provider/pkg/providers_test.go | 8 +++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 51625b91f..7fe3e8d09 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -6,6 +6,8 @@ import ( "fmt" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" "log/slog" + "maps" + "slices" "sync" mperr "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/errors" @@ -67,8 +69,10 @@ func (m ProviderMap) AsNamedProviderSlice() []*strategies.NamedProvider { func (m ProviderMap) buildMetadata() of.Metadata { var separator string metaName := "MultiProvider {" - for name, provider := range m { - metaName = fmt.Sprintf("%s%s%s: %s", metaName, separator, name, provider.Metadata().Name) + names := slices.Collect(maps.Keys(m)) + slices.Sort(names) + for _, name := range names { + metaName = fmt.Sprintf("%s%s%s: %s", metaName, separator, name, m[name].Metadata().Name) if separator == "" { separator = ", " } diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 3f2c94f2d..7e4024534 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -110,15 +110,17 @@ func TestMultiProvider_MetaData(t *testing.T) { metadata := mp.Metadata() require.NotZero(t, metadata) - assert.Equal(t, "MultiProvider{provider1:NoopProvider, provider2:MockProvider}", metadata.Name) + assert.Equal(t, "MultiProvider {provider1: NoopProvider, provider2: MockProvider}", metadata.Name) } func TestMultiProvider_Init(t *testing.T) { ctrl := gomock.NewController(t) testProvider1 := strategies.NewMockFeatureProvider(ctrl) + testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider2 := oft.NewTestProvider() testProvider3 := strategies.NewMockFeatureProvider(ctrl) + testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) providers := make(ProviderMap) providers["provider1"] = testProvider1 @@ -142,8 +144,10 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { ctrl := gomock.NewController(t) testProvider1 := strategies.NewMockFeatureProvider(ctrl) + testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider2 := oft.NewTestProvider() testProvider3 := strategies.NewMockFeatureProvider(ctrl) + testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) providers := make(ProviderMap) providers["provider1"] = testProvider1 @@ -179,8 +183,10 @@ func TestMultiProvider_Shutdown(t *testing.T) { ctrl := gomock.NewController(t) testProvider1 := strategies.NewMockFeatureProvider(ctrl) + testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider2 := oft.NewTestProvider() testProvider3 := strategies.NewMockFeatureProvider(ctrl) + testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) providers := make(ProviderMap) providers["provider1"] = testProvider1 From 6ca60ed684bd45e8e70fab68776177de675fde46 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Tue, 22 Apr 2025 20:56:42 -0400 Subject: [PATCH 36/68] refactor: Move options to new file Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/options.go | 31 ++++++++++++++++++++++ providers/multi-provider/pkg/providers.go | 32 ----------------------- 2 files changed, 31 insertions(+), 32 deletions(-) create mode 100644 providers/multi-provider/pkg/options.go diff --git a/providers/multi-provider/pkg/options.go b/providers/multi-provider/pkg/options.go new file mode 100644 index 000000000..ef09e1022 --- /dev/null +++ b/providers/multi-provider/pkg/options.go @@ -0,0 +1,31 @@ +package multiprovider + +import ( + of "github.com/open-feature/go-sdk/openfeature" + "log/slog" +) + +func WithLogger(l *slog.Logger) Option { + return func(conf *Configuration) { + conf.logger = l + } +} + +func WithFallbackProvider(p of.FeatureProvider) Option { + return func(conf *Configuration) { + conf.fallbackProvider = p + conf.useFallback = true + } +} + +func WithEventPublishing() Option { + return func(conf *Configuration) { + conf.publishEvents = true + } +} + +func WithoutEventPublishing() Option { + return func(conf *Configuration) { + conf.publishEvents = false + } +} diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 7fe3e8d09..7d8807cd4 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -83,38 +83,6 @@ func (m ProviderMap) buildMetadata() of.Metadata { } } -func WithLogger(l *slog.Logger) Option { - return func(conf *Configuration) { - conf.logger = l - } -} - -func WithFallbackProvider(p of.FeatureProvider, name string) Option { - return func(conf *Configuration) { - conf.fallbackProvider = p - conf.useFallback = true - } -} - -func WithNamedFallbackProvider(p of.FeatureProvider) Option { - return func(conf *Configuration) { - conf.fallbackProvider = p - conf.useFallback = true - } -} - -func WithEventPublishing() Option { - return func(conf *Configuration) { - conf.publishEvents = true - } -} - -func WithoutEventPublishing() Option { - return func(conf *Configuration) { - conf.publishEvents = false - } -} - // NewMultiProvider returns the unified interface of multiple providers for interaction. func NewMultiProvider(providerMap ProviderMap, evaluationStrategy EvaluationStrategy, options ...Option) (*MultiProvider, error) { if len(providerMap) == 0 { From bdb4a3910f9b61934bef2cbdbbf6ce83062b1234 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Tue, 22 Apr 2025 20:58:49 -0400 Subject: [PATCH 37/68] refactor: Rewrite aggregate errors & simplify Init method Also fixed some synchronization issues while I was at it Signed-off-by: Jordan Blacker --- .../internal/errors/aggregate-errors.go | 40 ------------- .../pkg/errors/aggregate-errors.go | 56 +++++++++++++++++++ providers/multi-provider/pkg/providers.go | 47 +++++++--------- .../multi-provider/pkg/providers_test.go | 34 +++++------ 4 files changed, 90 insertions(+), 87 deletions(-) delete mode 100644 providers/multi-provider/internal/errors/aggregate-errors.go create mode 100644 providers/multi-provider/pkg/errors/aggregate-errors.go diff --git a/providers/multi-provider/internal/errors/aggregate-errors.go b/providers/multi-provider/internal/errors/aggregate-errors.go deleted file mode 100644 index a4d803311..000000000 --- a/providers/multi-provider/internal/errors/aggregate-errors.go +++ /dev/null @@ -1,40 +0,0 @@ -package errors - -import ( - "encoding/json" - "fmt" -) - -// StateErr is how the error in the Init stage of a provider is reported. -type StateErr struct { - ProviderName string `json:"source"` - Err error `json:"-"` - ErrMessage string `json:"error"` -} - -func (e *StateErr) Error() string { - return fmt.Sprintf("Provider %s had an error: %v", e.ProviderName, e.Err) -} - -type AggregateError struct { - Message string `json:"message"` - Errors []StateErr `json:"errors"` -} - -func (ae *AggregateError) Error() string { - errorsJSON, err := json.Marshal(ae.Errors) - if err != nil { - return fmt.Sprintf("Error in json marshal of errors, %s", err) - } - - return fmt.Sprintf("%s\n%s", ae.Message, string(errorsJSON)) - -} - -func (ae *AggregateError) Construct(providerErrors []StateErr) { - // Show first error message for convenience, but all errors in the object - msg := fmt.Sprintf("Provider errors occurred: %s: %v", providerErrors[0].ProviderName, providerErrors[0].Err) - - ae.Message = msg - ae.Errors = providerErrors -} diff --git a/providers/multi-provider/pkg/errors/aggregate-errors.go b/providers/multi-provider/pkg/errors/aggregate-errors.go new file mode 100644 index 000000000..035baa078 --- /dev/null +++ b/providers/multi-provider/pkg/errors/aggregate-errors.go @@ -0,0 +1,56 @@ +package errors + +import ( + "fmt" + "golang.org/x/exp/maps" + "strings" +) + +type ( + // ProviderError is how the error in the Init stage of a provider is reported. + ProviderError struct { + Err error + ProviderName string + } + + // AggregateError map that contains up to one error per provider within the multi-provider + AggregateError map[string]ProviderError +) + +var ( + _ error = (*ProviderError)(nil) + _ error = (AggregateError)(nil) +) + +func (e *ProviderError) Error() string { + return fmt.Sprintf("Provider %s: %s", e.ProviderName, e.Err.Error()) +} + +// NewAggregateError Creates a new AggregateError +func NewAggregateError(providerErrors []ProviderError) *AggregateError { + err := make(AggregateError) + for _, se := range providerErrors { + err[se.ProviderName] = se + } + return &err +} + +func (ae AggregateError) Error() string { + size := len(ae) + switch size { + case 0: + return "" + case 1: + for _, err := range ae { + return err.Error() + } + default: + errs := make([]string, 0, size) + for _, err := range maps.Values(ae) { + errs = append(errs, err.Error()) + } + return strings.Join(errs, ", ") + } + + return "" // This will never occur, switch is exhaustive +} diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 7d8807cd4..8eed9757b 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -5,12 +5,13 @@ import ( "errors" "fmt" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" + "golang.org/x/sync/errgroup" "log/slog" "maps" "slices" "sync" - mperr "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/errors" + mperr "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/errors" of "github.com/open-feature/go-sdk/openfeature" ) @@ -190,55 +191,47 @@ func (mp *MultiProvider) ObjectEvaluation(ctx context.Context, flag string, defa // Init will run the initialize method for all of provides and aggregate the errors. func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { - var wg sync.WaitGroup - errChan := make(chan mperr.StateErr) + var eg errgroup.Group for name, provider := range mp.providers { - wg.Add(1) - go func(p of.FeatureProvider, name string) { - defer wg.Done() - if stateHandle, ok := provider.(of.StateHandler); ok { - if initErr := stateHandle.Init(evalCtx); initErr != nil { - errChan <- mperr.StateErr{ProviderName: name, Err: initErr, ErrMessage: initErr.Error()} + eg.Go(func() error { + stateHandle, ok := provider.(of.StateHandler) + if !ok { + return nil + } + if err := stateHandle.Init(evalCtx); err != nil { + return &mperr.ProviderError{ + Err: err, + ProviderName: name, } } - }(provider, name) - } - go func() { - wg.Wait() - close(errChan) - }() - - errs := make([]mperr.StateErr, 0, 1) - for err := range errChan { - errs = append(errs, err) + return nil + }) } - if len(errs) > 0 { - var aggErr mperr.AggregateError - aggErr.Construct(errs) - mp.mu.RLock() + if err := eg.Wait(); err != nil { + mp.mu.Lock() defer mp.mu.Unlock() mp.status = of.ErrorState - return &aggErr + return err } - mp.mu.RLock() + mp.mu.Lock() defer mp.mu.Unlock() mp.status = of.ReadyState - return nil } func (mp *MultiProvider) Status() of.State { + mp.mu.RLock() + defer mp.mu.RUnlock() return mp.status } func (mp *MultiProvider) Shutdown() { var wg sync.WaitGroup - for _, provider := range mp.providers { wg.Add(1) go func(p of.FeatureProvider) { diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 7e4024534..91a464ba1 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -1,21 +1,18 @@ package multiprovider import ( - "encoding/json" + "errors" "fmt" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "golang.org/x/exp/maps" + "maps" "regexp" - "strings" "testing" "time" - mperrs "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/errors" - "github.com/open-feature/go-sdk/openfeature" oft "github.com/open-feature/go-sdk/openfeature/testing" ) @@ -142,12 +139,21 @@ func TestMultiProvider_Init(t *testing.T) { func TestMultiProvider_InitErrorWithProvider(t *testing.T) { ctrl := gomock.NewController(t) + errProvider := strategies.NewMockFeatureProvider(ctrl) + errProvider.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) + errHandler := strategies.NewMockStateHandler(ctrl) + errHandler.EXPECT().Init(gomock.Any()).Return(errors.New("test error")) + testProvider3 := struct { + of.FeatureProvider + of.StateHandler + }{ + errProvider, + errHandler, + } testProvider1 := strategies.NewMockFeatureProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider2 := oft.NewTestProvider() - testProvider3 := strategies.NewMockFeatureProvider(ctrl) - testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) providers := make(ProviderMap) providers["provider1"] = testProvider1 @@ -163,19 +169,7 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { evalCtx := openfeature.NewTargetlessEvaluationContext(attributes) err = mp.Init(evalCtx) - require.Error(t, err) - - var errors []mperrs.StateErr - - fullErr := err.Error() - fullErrArr := strings.SplitAfterN(fullErr, "end", 2) - errJSON := fullErrArr[1] - errMsg := fullErrArr[0] - assert.Contains(t, errMsg, "Provider errors occurred:") - - err = json.Unmarshal([]byte(errJSON), &errors) - require.NoError(t, err) - assert.Len(t, errors, 2) + require.Errorf(t, err, "Provider provider1: test error") assert.Equal(t, of.ErrorState, mp.status) } From a743757fe78140ba55c18c666c9c6a7bdbc3e35e Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 16:18:49 -0400 Subject: [PATCH 38/68] fix: Correct broken cleanErrorMessage helper func Signed-off-by: Jordan Blacker --- .../internal/strategies/strategies.go | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index 8c222dbe9..7e3b495ef 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -109,3 +109,24 @@ func setFlagMetadata(strategyUsed EvaluationStrategy, successProviderName string metadata[MetadataStrategyUsed] = strategyUsed return metadata } + +func cleanErrorMessage(msg string) string { + codeRegex := strings.Join([]string{ + string(of.ProviderNotReadyCode), + string(of.ProviderFatalCode), + string(of.FlagNotFoundCode), + string(of.ParseErrorCode), + string(of.TypeMismatchCode), + string(of.TargetingKeyMissingCode), + string(of.GeneralCode), + }, "|") + re := regexp.MustCompile("(?:" + codeRegex + "): (.*)") + matches := re.FindSubmatch([]byte(msg)) + matchCount := len(matches) + switch matchCount { + case 0, 1: + return msg + default: + return strings.TrimSpace(string(matches[1])) + } +} From 41d824755ff8ea46e41136de50fc89ce61136ee7 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 16:19:05 -0400 Subject: [PATCH 39/68] feat: Add tag merge helper method Signed-off-by: Jordan Blacker --- .../internal/strategies/strategies.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index 7e3b495ef..9ec0c044a 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -130,3 +130,22 @@ func cleanErrorMessage(msg string) string { return strings.TrimSpace(string(matches[1])) } } + +// mergeFlagTags Merges flag metadata together into a single FlagMetadata instance by performing a shallow merge +func mergeFlagTags(tags ...of.FlagMetadata) of.FlagMetadata { + size := len(tags) + switch size { + case 0: + return make(of.FlagMetadata) + case 1: + return tags[0] + default: + merged := make(of.FlagMetadata) + for _, t := range tags { + for key, value := range t { + merged[key] = value + } + } + return merged + } +} From 35f321414492d323d8e4592561e4f2a5a7ed0d7e Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 16:20:35 -0400 Subject: [PATCH 40/68] refactor: Move mock file to mocks package Signed-off-by: Jordan Blacker --- .../internal/mocks/provider_mock.go | 264 ++++++++++++++++++ .../internal/strategies/first_match_test.go | 7 +- .../multi-provider/pkg/providers_test.go | 17 +- 3 files changed, 277 insertions(+), 11 deletions(-) create mode 100644 providers/multi-provider/internal/mocks/provider_mock.go diff --git a/providers/multi-provider/internal/mocks/provider_mock.go b/providers/multi-provider/internal/mocks/provider_mock.go new file mode 100644 index 000000000..77f1573ec --- /dev/null +++ b/providers/multi-provider/internal/mocks/provider_mock.go @@ -0,0 +1,264 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go +// +// Generated by this command: +// +// mockgen -source=github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go -package=strategies -destination=./providers/multi-provider/internal/strategies/provider_mock.go +// + +// Package strategies is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + openfeature "github.com/open-feature/go-sdk/openfeature" + gomock "go.uber.org/mock/gomock" +) + +// MockFeatureProvider is a mock of FeatureProvider interface. +type MockFeatureProvider struct { + ctrl *gomock.Controller + recorder *MockFeatureProviderMockRecorder + isgomock struct{} +} + +// MockFeatureProviderMockRecorder is the mock recorder for MockFeatureProvider. +type MockFeatureProviderMockRecorder struct { + mock *MockFeatureProvider +} + +// NewMockFeatureProvider creates a new mock instance. +func NewMockFeatureProvider(ctrl *gomock.Controller) *MockFeatureProvider { + mock := &MockFeatureProvider{ctrl: ctrl} + mock.recorder = &MockFeatureProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockFeatureProvider) EXPECT() *MockFeatureProviderMockRecorder { + return m.recorder +} + +// BooleanEvaluation mocks base method. +func (m *MockFeatureProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BooleanEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.BoolResolutionDetail) + return ret0 +} + +// BooleanEvaluation indicates an expected call of BooleanEvaluation. +func (mr *MockFeatureProviderMockRecorder) BooleanEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BooleanEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).BooleanEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// FloatEvaluation mocks base method. +func (m *MockFeatureProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FloatEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.FloatResolutionDetail) + return ret0 +} + +// FloatEvaluation indicates an expected call of FloatEvaluation. +func (mr *MockFeatureProviderMockRecorder) FloatEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).FloatEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// Hooks mocks base method. +func (m *MockFeatureProvider) Hooks() []openfeature.Hook { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Hooks") + ret0, _ := ret[0].([]openfeature.Hook) + return ret0 +} + +// Hooks indicates an expected call of Hooks. +func (mr *MockFeatureProviderMockRecorder) Hooks() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hooks", reflect.TypeOf((*MockFeatureProvider)(nil).Hooks)) +} + +// IntEvaluation mocks base method. +func (m *MockFeatureProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.IntResolutionDetail) + return ret0 +} + +// IntEvaluation indicates an expected call of IntEvaluation. +func (mr *MockFeatureProviderMockRecorder) IntEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).IntEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// Metadata mocks base method. +func (m *MockFeatureProvider) Metadata() openfeature.Metadata { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Metadata") + ret0, _ := ret[0].(openfeature.Metadata) + return ret0 +} + +// Metadata indicates an expected call of Metadata. +func (mr *MockFeatureProviderMockRecorder) Metadata() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockFeatureProvider)(nil).Metadata)) +} + +// ObjectEvaluation mocks base method. +func (m *MockFeatureProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ObjectEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.InterfaceResolutionDetail) + return ret0 +} + +// ObjectEvaluation indicates an expected call of ObjectEvaluation. +func (mr *MockFeatureProviderMockRecorder) ObjectEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).ObjectEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// StringEvaluation mocks base method. +func (m *MockFeatureProvider) StringEvaluation(ctx context.Context, flag, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StringEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.StringResolutionDetail) + return ret0 +} + +// StringEvaluation indicates an expected call of StringEvaluation. +func (mr *MockFeatureProviderMockRecorder) StringEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).StringEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// MockStateHandler is a mock of StateHandler interface. +type MockStateHandler struct { + ctrl *gomock.Controller + recorder *MockStateHandlerMockRecorder + isgomock struct{} +} + +// MockStateHandlerMockRecorder is the mock recorder for MockStateHandler. +type MockStateHandlerMockRecorder struct { + mock *MockStateHandler +} + +// NewMockStateHandler creates a new mock instance. +func NewMockStateHandler(ctrl *gomock.Controller) *MockStateHandler { + mock := &MockStateHandler{ctrl: ctrl} + mock.recorder = &MockStateHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStateHandler) EXPECT() *MockStateHandlerMockRecorder { + return m.recorder +} + +// Init mocks base method. +func (m *MockStateHandler) Init(evaluationContext openfeature.EvaluationContext) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Init", evaluationContext) + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init. +func (mr *MockStateHandlerMockRecorder) Init(evaluationContext any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockStateHandler)(nil).Init), evaluationContext) +} + +// Shutdown mocks base method. +func (m *MockStateHandler) Shutdown() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Shutdown") +} + +// Shutdown indicates an expected call of Shutdown. +func (mr *MockStateHandlerMockRecorder) Shutdown() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockStateHandler)(nil).Shutdown)) +} + +// MockTracker is a mock of Tracker interface. +type MockTracker struct { + ctrl *gomock.Controller + recorder *MockTrackerMockRecorder + isgomock struct{} +} + +// MockTrackerMockRecorder is the mock recorder for MockTracker. +type MockTrackerMockRecorder struct { + mock *MockTracker +} + +// NewMockTracker creates a new mock instance. +func NewMockTracker(ctrl *gomock.Controller) *MockTracker { + mock := &MockTracker{ctrl: ctrl} + mock.recorder = &MockTrackerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockTracker) EXPECT() *MockTrackerMockRecorder { + return m.recorder +} + +// Track mocks base method. +func (m *MockTracker) Track(ctx context.Context, trackingEventName string, evaluationContext openfeature.EvaluationContext, details openfeature.TrackingEventDetails) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Track", ctx, trackingEventName, evaluationContext, details) +} + +// Track indicates an expected call of Track. +func (mr *MockTrackerMockRecorder) Track(ctx, trackingEventName, evaluationContext, details any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Track", reflect.TypeOf((*MockTracker)(nil).Track), ctx, trackingEventName, evaluationContext, details) +} + +// MockEventHandler is a mock of EventHandler interface. +type MockEventHandler struct { + ctrl *gomock.Controller + recorder *MockEventHandlerMockRecorder + isgomock struct{} +} + +// MockEventHandlerMockRecorder is the mock recorder for MockEventHandler. +type MockEventHandlerMockRecorder struct { + mock *MockEventHandler +} + +// NewMockEventHandler creates a new mock instance. +func NewMockEventHandler(ctrl *gomock.Controller) *MockEventHandler { + mock := &MockEventHandler{ctrl: ctrl} + mock.recorder = &MockEventHandlerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEventHandler) EXPECT() *MockEventHandlerMockRecorder { + return m.recorder +} + +// EventChannel mocks base method. +func (m *MockEventHandler) EventChannel() <-chan openfeature.Event { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EventChannel") + ret0, _ := ret[0].(<-chan openfeature.Event) + return ret0 +} + +// EventChannel indicates an expected call of EventChannel. +func (mr *MockEventHandlerMockRecorder) EventChannel() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EventChannel", reflect.TypeOf((*MockEventHandler)(nil).EventChannel)) +} diff --git a/providers/multi-provider/internal/strategies/first_match_test.go b/providers/multi-provider/internal/strategies/first_match_test.go index 4bcc1f2bd..de1cb1fbc 100644 --- a/providers/multi-provider/internal/strategies/first_match_test.go +++ b/providers/multi-provider/internal/strategies/first_match_test.go @@ -3,17 +3,18 @@ package strategies import ( "context" "fmt" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "testing" ) -func createMockProviders(ctrl *gomock.Controller, count int) ([]*NamedProvider, map[string]*MockFeatureProvider) { +func createMockProviders(ctrl *gomock.Controller, count int) ([]*NamedProvider, map[string]*mocks.MockFeatureProvider) { providers := make([]*NamedProvider, 0, count) - providerMocks := make(map[string]*MockFeatureProvider) + providerMocks := make(map[string]*mocks.MockFeatureProvider) for index := range count { - provider := NewMockFeatureProvider(ctrl) + provider := mocks.NewMockFeatureProvider(ctrl) namedProvider := NamedProvider{ Provider: provider, Name: fmt.Sprintf("%d", index), diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 91a464ba1..878b41ecf 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -3,6 +3,7 @@ package multiprovider import ( "errors" "fmt" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" @@ -93,7 +94,7 @@ func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { func TestMultiProvider_MetaData(t *testing.T) { testProvider1 := oft.NewTestProvider() ctrl := gomock.NewController(t) - testProvider2 := strategies.NewMockFeatureProvider(ctrl) + testProvider2 := mocks.NewMockFeatureProvider(ctrl) testProvider2.EXPECT().Metadata().Return(of.Metadata{ Name: "MockProvider", }) @@ -113,10 +114,10 @@ func TestMultiProvider_MetaData(t *testing.T) { func TestMultiProvider_Init(t *testing.T) { ctrl := gomock.NewController(t) - testProvider1 := strategies.NewMockFeatureProvider(ctrl) + testProvider1 := mocks.NewMockFeatureProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider2 := oft.NewTestProvider() - testProvider3 := strategies.NewMockFeatureProvider(ctrl) + testProvider3 := mocks.NewMockFeatureProvider(ctrl) testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) providers := make(ProviderMap) @@ -139,9 +140,9 @@ func TestMultiProvider_Init(t *testing.T) { func TestMultiProvider_InitErrorWithProvider(t *testing.T) { ctrl := gomock.NewController(t) - errProvider := strategies.NewMockFeatureProvider(ctrl) + errProvider := mocks.NewMockFeatureProvider(ctrl) errProvider.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) - errHandler := strategies.NewMockStateHandler(ctrl) + errHandler := mocks.NewMockStateHandler(ctrl) errHandler.EXPECT().Init(gomock.Any()).Return(errors.New("test error")) testProvider3 := struct { of.FeatureProvider @@ -151,7 +152,7 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { errHandler, } - testProvider1 := strategies.NewMockFeatureProvider(ctrl) + testProvider1 := mocks.NewMockFeatureProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider2 := oft.NewTestProvider() @@ -176,10 +177,10 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { func TestMultiProvider_Shutdown(t *testing.T) { ctrl := gomock.NewController(t) - testProvider1 := strategies.NewMockFeatureProvider(ctrl) + testProvider1 := mocks.NewMockFeatureProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) testProvider2 := oft.NewTestProvider() - testProvider3 := strategies.NewMockFeatureProvider(ctrl) + testProvider3 := mocks.NewMockFeatureProvider(ctrl) testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) providers := make(ProviderMap) From 66fd61900f7c19f76cadf7cd25d229f673aadaea Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 16:21:33 -0400 Subject: [PATCH 41/68] refactor: Rewrite first success implementation to be more go-y - Rewrote using multiple go-routines & channels instead of a mutex - Added tests - Added timeout functionality & option Signed-off-by: Jordan Blacker --- .../internal/strategies/first_success.go | 195 +++--- .../internal/strategies/first_success_test.go | 589 +++++++++++++++++- providers/multi-provider/pkg/options.go | 7 + providers/multi-provider/pkg/providers.go | 9 +- 4 files changed, 713 insertions(+), 87 deletions(-) diff --git a/providers/multi-provider/internal/strategies/first_success.go b/providers/multi-provider/internal/strategies/first_success.go index 3d20cb7d2..95c732b2c 100644 --- a/providers/multi-provider/internal/strategies/first_success.go +++ b/providers/multi-provider/internal/strategies/first_success.go @@ -2,20 +2,22 @@ package strategies import ( "context" - "errors" of "github.com/open-feature/go-sdk/openfeature" - "sync" + "time" + + mperr "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/errors" ) type FirstSuccessStrategy struct { providers []*NamedProvider + timeout time.Duration } var _ Strategy = (*FirstSuccessStrategy)(nil) // NewFirstSuccessStrategy Creates a new FirstSuccessStrategy instance as a Strategy -func NewFirstSuccessStrategy(providers []*NamedProvider) Strategy { - return &FirstSuccessStrategy{providers: providers} +func NewFirstSuccessStrategy(providers []*NamedProvider, timeout time.Duration) Strategy { + return &FirstSuccessStrategy{providers: providers, timeout: timeout} } func (f *FirstSuccessStrategy) Name() EvaluationStrategy { @@ -23,102 +25,135 @@ func (f *FirstSuccessStrategy) Name() EvaluationStrategy { } func (f *FirstSuccessStrategy) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { - return *evaluateFirstSuccess[of.BoolResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.BoolResolutionDetail] { + result := p.Provider.BooleanEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.BoolResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + result, metadata := evaluateFirstSuccess[of.BoolResolutionDetail](ctx, f.providers, evalFunc, defaultValue, f.timeout) + r := *result.result + r.ProviderResolutionDetail.FlagMetadata = mergeFlagTags(r.ProviderResolutionDetail.FlagMetadata, metadata) + return r } func (f *FirstSuccessStrategy) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail { - return *evaluateFirstSuccess[of.StringResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.StringResolutionDetail] { + result := p.Provider.StringEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.StringResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + result, metadata := evaluateFirstSuccess[of.StringResolutionDetail](ctx, f.providers, evalFunc, defaultValue, f.timeout) + r := *result.result + r.ProviderResolutionDetail.FlagMetadata = mergeFlagTags(r.ProviderResolutionDetail.FlagMetadata, metadata) + return r } func (f *FirstSuccessStrategy) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { - return *evaluateFirstSuccess[of.FloatResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.FloatResolutionDetail] { + result := p.Provider.FloatEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.FloatResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + result, metadata := evaluateFirstSuccess[of.FloatResolutionDetail](ctx, f.providers, evalFunc, defaultValue, f.timeout) + r := *result.result + r.ProviderResolutionDetail.FlagMetadata = mergeFlagTags(r.ProviderResolutionDetail.FlagMetadata, metadata) + return r } func (f *FirstSuccessStrategy) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { - return *evaluateFirstSuccess[of.IntResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.IntResolutionDetail] { + result := p.Provider.IntEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.IntResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + result, metadata := evaluateFirstSuccess[of.IntResolutionDetail](ctx, f.providers, evalFunc, defaultValue, f.timeout) + r := *result.result + r.ProviderResolutionDetail.FlagMetadata = mergeFlagTags(r.ProviderResolutionDetail.FlagMetadata, metadata) + return r } func (f *FirstSuccessStrategy) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { - return *evaluateFirstSuccess[of.InterfaceResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.InterfaceResolutionDetail] { + result := p.Provider.ObjectEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.InterfaceResolutionDetail]{ + result: &result, + name: p.Name, + value: result.Value, + detail: result.ProviderResolutionDetail, + } + } + result, metadata := evaluateFirstSuccess[of.InterfaceResolutionDetail](ctx, f.providers, evalFunc, defaultValue, f.timeout) + r := *result.result + r.ProviderResolutionDetail.FlagMetadata = mergeFlagTags(r.ProviderResolutionDetail.FlagMetadata, metadata) + return r } -func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []*NamedProvider, flag string, defaultValue DV, evalCtx of.FlattenedContext) resultWrapper[R] { - var ( - mutex sync.Mutex - wg sync.WaitGroup - result *resultWrapper[R] - ) - errChan := make(chan error) - ctx, cancel := context.WithCancel(ctx) +func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []*NamedProvider, e evaluator[R], defaultVal DV, timeout time.Duration) (resultWrapper[R], of.FlagMetadata) { + metadata := make(of.FlagMetadata) + metadata[MetadataStrategyUsed] = StrategyFirstSuccess + errChan := make(chan mperr.ProviderError, len(providers)) + finishChan := make(chan *resultWrapper[R], len(providers)) + ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() for _, provider := range providers { - wg.Add(1) - go func(p *NamedProvider) { - defer wg.Done() - switch any(defaultValue).(type) { - case bool: - r := provider.Provider.BooleanEvaluation(ctx, flag, any(defaultValue).(bool), evalCtx) - if r.Error() == nil { - mutex.Lock() - result = &resultWrapper[R]{result: any(r).(*R)} - cancel() - mutex.Unlock() - } - errChan <- r.Error() - case string: - r := provider.Provider.StringEvaluation(ctx, flag, any(defaultValue).(string), evalCtx) - if r.Error() == nil { - mutex.Lock() - result = &resultWrapper[R]{result: any(r).(*R)} - cancel() - mutex.Unlock() - } - errChan <- r.Error() - case int64: - r := provider.Provider.IntEvaluation(ctx, flag, any(defaultValue).(int64), evalCtx) - if r.Error() == nil { - mutex.Lock() - result = &resultWrapper[R]{result: any(r).(*R)} - cancel() - mutex.Unlock() - } - errChan <- r.Error() - case float64: - r := provider.Provider.FloatEvaluation(ctx, flag, any(defaultValue).(float64), evalCtx) - if r.Error() == nil { - mutex.Lock() - result = &resultWrapper[R]{result: any(r).(*R)} - cancel() - mutex.Unlock() - } - errChan <- r.Error() - default: - r := provider.Provider.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) - if r.Error() == nil { - mutex.Lock() - result = &resultWrapper[R]{result: any(r).(*R)} - cancel() - mutex.Unlock() + go func(c context.Context, p *NamedProvider) { + resultChan := make(chan *resultWrapper[R]) + go func() { + r := e(c, provider) + resultChan <- &r + }() + + select { + case <-c.Done(): + return + case r := <-resultChan: + if r.detail.Error() != nil { + errChan <- mperr.ProviderError{ + Err: r.detail.ResolutionError, + ProviderName: p.Name, + } + return } - errChan <- r.Error() + finishChan <- r } - }(provider) - } - go func() { - wg.Wait() - close(errChan) - }() - - errs := make([]error, 0, 1) - for e := range errChan { - errs = append(errs, e) + }(ctx, provider) } - if result != nil { - return *result - } + errs := make([]mperr.ProviderError, 0, len(providers)) + for { + if len(errs) == len(providers) { + err := mperr.NewAggregateError(errs) + r := buildDefaultResult[R](StrategyFirstSuccess, defaultVal, err) + return r, r.detail.FlagMetadata + } - err := errors.Join(errs...) - return buildDefaultResult[R](StrategyFirstSuccess, defaultValue, err) + select { + case result := <-finishChan: + metadata[MetadataSuccessfulProviderName] = result.name + cancel() + return *result, metadata + case err := <-errChan: + errs = append(errs, err) + case <-ctx.Done(): + r := buildDefaultResult[R](StrategyFirstSuccess, defaultVal, ctx.Err()) + return r, r.detail.FlagMetadata + } + } } diff --git a/providers/multi-provider/internal/strategies/first_success_test.go b/providers/multi-provider/internal/strategies/first_success_test.go index be5810594..2877bdfdb 100644 --- a/providers/multi-provider/internal/strategies/first_success_test.go +++ b/providers/multi-provider/internal/strategies/first_success_test.go @@ -1,23 +1,600 @@ package strategies -import "testing" +import ( + "context" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" + of "github.com/open-feature/go-sdk/openfeature" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" + "time" +) + +const TestFlag = "test-flag" + +func configureFlags[R bool | int64 | float64 | string | interface{}](provider *mocks.MockFeatureProvider, resultVal R, state bool, error bool, delay time.Duration) { + var rErr of.ResolutionError + var variant string + var reason of.Reason + if error { + rErr = of.NewGeneralResolutionError("test error") + reason = of.DisabledReason + } + if state { + variant = "on" + } else { + variant = "off" + } + details := of.ProviderResolutionDetail{ + ResolutionError: rErr, + Reason: reason, + Variant: variant, + FlagMetadata: make(of.FlagMetadata), + } + + switch any(resultVal).(type) { + case bool: + provider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { + time.Sleep(delay) + return of.BoolResolutionDetail{ + Value: any(resultVal).(bool), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + case string: + provider.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal string, evalCtx of.FlattenedContext) of.StringResolutionDetail { + time.Sleep(delay) + return of.StringResolutionDetail{ + Value: any(resultVal).(string), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + case int64: + provider.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { + time.Sleep(delay) + return of.IntResolutionDetail{ + Value: any(resultVal).(int64), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + case float64: + provider.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { + time.Sleep(delay) + return of.FloatResolutionDetail{ + Value: any(resultVal).(float64), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + default: + provider.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal any, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { + time.Sleep(delay) + return of.InterfaceResolutionDetail{ + Value: resultVal, + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + } +} func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { - // TODO + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider, true, true, false, 0*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, 2*time.Second) + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("first success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, true, true, false, 5*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, false, false, true, 50*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("second success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, true, true, false, 500*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, false, false, true, 5*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("all errors", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, false, false, true, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, false, false, true, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider3, false, false, true, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.False(t, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.DefaultReason, result.Reason) + }) } func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { - // TODO + successVal := "success" + defaultVal := "default" + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider, successVal, true, false, 0*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, 2*time.Second) + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("first success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("second success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("all errors", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.DefaultReason, result.Reason) + }) } func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { - // TODO + successVal := int64(150) + defaultVal := int64(0) + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider, successVal, true, false, 0*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, 2*time.Second) + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("first success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("second success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("all errors", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.DefaultReason, result.Reason) + }) } func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { - // TODO + successVal := float64(15.5) + defaultVal := float64(0) + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider, successVal, true, false, 0*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, 2*time.Second) + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("first success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("second success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("all errors", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.DefaultReason, result.Reason) + }) } func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { - // TODO + successVal := struct{ Field string }{Field: "test"} + defaultVal := struct{}{} + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider, successVal, true, false, 0*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, 2*time.Second) + result := strategy.ObjectEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("first success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.ObjectEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("second success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "success-provider", + Provider: provider1, + }, + { + Name: "failure-provider", + Provider: provider2, + }, + }, 2*time.Second) + + result := strategy.ObjectEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + }) + + t.Run("all errors", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.ObjectEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.DefaultReason, result.Reason) + }) } diff --git a/providers/multi-provider/pkg/options.go b/providers/multi-provider/pkg/options.go index ef09e1022..2575a1456 100644 --- a/providers/multi-provider/pkg/options.go +++ b/providers/multi-provider/pkg/options.go @@ -3,6 +3,7 @@ package multiprovider import ( of "github.com/open-feature/go-sdk/openfeature" "log/slog" + "time" ) func WithLogger(l *slog.Logger) Option { @@ -11,6 +12,12 @@ func WithLogger(l *slog.Logger) Option { } } +func WithTimeout(d time.Duration) Option { + return func(conf *Configuration) { + conf.timeout = d + } +} + func WithFallbackProvider(p of.FeatureProvider) Option { return func(conf *Configuration) { conf.fallbackProvider = p diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 8eed9757b..f8be822e7 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -10,6 +10,7 @@ import ( "maps" "slices" "sync" + "time" mperr "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/errors" @@ -33,6 +34,7 @@ type ( logger *slog.Logger publishEvents bool metadata *of.Metadata + timeout time.Duration hooks []of.Hook // Not implemented yet } @@ -125,12 +127,17 @@ func NewMultiProvider(providerMap ProviderMap, evaluationStrategy EvaluationStra metadata: providerMap.buildMetadata(), } + var zeroDuration time.Duration + if config.timeout == zeroDuration { + config.timeout = 5 * time.Second + } + var strategy strategies.Strategy switch evaluationStrategy { case StrategyFirstMatch: strategy = strategies.NewFirstMatchStrategy(multiProvider.Providers()) case StrategyFirstSuccess: - strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers()) + strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers(), config.timeout) case StrategyComparison: strategy = strategies.NewComparisonStrategy(multiProvider.Providers(), config.fallbackProvider) default: From 4cab3c92e12e884deb8dabd12ab011546bf8a5ca Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 16:52:53 -0400 Subject: [PATCH 42/68] fix: correct bug in default resolution func Signed-off-by: Jordan Blacker --- providers/multi-provider/internal/strategies/strategies.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index 9ec0c044a..7566ec463 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -98,7 +98,7 @@ func buildDefaultResult[R resultConstraint, DV bool | string | int64 | float64 | result = any(r).(R) } - return resultWrapper[R]{result: &result} + return resultWrapper[R]{result: &result, detail: details} } func setFlagMetadata(strategyUsed EvaluationStrategy, successProviderName string, metadata of.FlagMetadata) of.FlagMetadata { From 67306b46867741a5f74034999a3008ec090f9c81 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 16:53:08 -0400 Subject: [PATCH 43/68] refactor: Use evaluator for first_match Signed-off-by: Jordan Blacker --- .../internal/strategies/first_match.go | 130 ++++++++++-------- .../internal/strategies/first_match_test.go | 6 +- 2 files changed, 76 insertions(+), 60 deletions(-) diff --git a/providers/multi-provider/internal/strategies/first_match.go b/providers/multi-provider/internal/strategies/first_match.go index 18c5209c9..e58a77810 100644 --- a/providers/multi-provider/internal/strategies/first_match.go +++ b/providers/multi-provider/internal/strategies/first_match.go @@ -21,81 +21,95 @@ func (f *FirstMatchStrategy) Name() EvaluationStrategy { } func (f *FirstMatchStrategy) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { - return *evaluateFirstMatch[of.BoolResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.BoolResolutionDetail] { + r := p.Provider.BooleanEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.BoolResolutionDetail]{ + result: &r, + name: p.Name, + value: r.Value, + detail: r.ProviderResolutionDetail, + } + } + result := evaluateFirstMatch[of.BoolResolutionDetail](ctx, f.providers, evalFunc, defaultValue) + result.result.ProviderResolutionDetail = result.detail + return *result.result } func (f *FirstMatchStrategy) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx of.FlattenedContext) of.StringResolutionDetail { - return *evaluateFirstMatch[of.StringResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.StringResolutionDetail] { + r := p.Provider.StringEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.StringResolutionDetail]{ + result: &r, + name: p.Name, + value: r.Value, + detail: r.ProviderResolutionDetail, + } + } + result := evaluateFirstMatch[of.StringResolutionDetail](ctx, f.providers, evalFunc, defaultValue) + result.result.ProviderResolutionDetail = result.detail + return *result.result } func (f *FirstMatchStrategy) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { - return *evaluateFirstMatch[of.FloatResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.FloatResolutionDetail] { + r := p.Provider.FloatEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.FloatResolutionDetail]{ + result: &r, + name: p.Name, + value: r.Value, + detail: r.ProviderResolutionDetail, + } + } + result := evaluateFirstMatch[of.FloatResolutionDetail](ctx, f.providers, evalFunc, defaultValue) + result.result.ProviderResolutionDetail = result.detail + return *result.result } func (f *FirstMatchStrategy) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { - return *evaluateFirstMatch[of.IntResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.IntResolutionDetail] { + r := p.Provider.IntEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.IntResolutionDetail]{ + result: &r, + name: p.Name, + value: r.Value, + detail: r.ProviderResolutionDetail, + } + } + result := evaluateFirstMatch[of.IntResolutionDetail](ctx, f.providers, evalFunc, defaultValue) + result.result.ProviderResolutionDetail = result.detail + return *result.result } func (f *FirstMatchStrategy) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { - return *evaluateFirstMatch[of.InterfaceResolutionDetail](ctx, f.providers, flag, defaultValue, evalCtx).result + evalFunc := func(c context.Context, p *NamedProvider) resultWrapper[of.InterfaceResolutionDetail] { + r := p.Provider.ObjectEvaluation(c, flag, defaultValue, evalCtx) + return resultWrapper[of.InterfaceResolutionDetail]{ + result: &r, + name: p.Name, + value: r.Value, + detail: r.ProviderResolutionDetail, + } + } + result := evaluateFirstMatch[of.InterfaceResolutionDetail](ctx, f.providers, evalFunc, defaultValue) + result.result.ProviderResolutionDetail = result.detail + return *result.result } -func evaluateFirstMatch[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []*NamedProvider, flag string, defaultValue DV, evalCtx of.FlattenedContext) resultWrapper[R] { +func evaluateFirstMatch[R resultConstraint, DV bool | string | int64 | float64 | interface{}](ctx context.Context, providers []*NamedProvider, e evaluator[R], defaultVal DV) resultWrapper[R] { for _, provider := range providers { - switch any(defaultValue).(type) { - case bool: - r := provider.Provider.BooleanEvaluation(ctx, flag, any(defaultValue).(bool), evalCtx) - if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { - continue - } else if r.Error() != nil { - return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) - } - rp := &r - rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) - return resultWrapper[R]{result: any(rp).(*R)} - case string: - r := provider.Provider.StringEvaluation(ctx, flag, any(defaultValue).(string), evalCtx) - if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { - continue - } else if r.Error() != nil { - return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) - } - rp := &r - rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) - return resultWrapper[R]{result: any(rp).(*R)} - case int64: - r := provider.Provider.IntEvaluation(ctx, flag, any(defaultValue).(int64), evalCtx) - if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { - continue - } else if r.Error() != nil { - return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) - } - rp := &r - rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) - return resultWrapper[R]{result: any(rp).(*R)} - case float64: - r := provider.Provider.FloatEvaluation(ctx, flag, any(defaultValue).(float64), evalCtx) - if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { - continue - } else if r.Error() != nil { - return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) - } - rp := &r - rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) - return resultWrapper[R]{result: any(rp).(*R)} - default: - r := provider.Provider.ObjectEvaluation(ctx, flag, defaultValue, evalCtx) - if r.Error() != nil && r.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { - continue - } else if r.Error() != nil { - return buildDefaultResult[R](StrategyFirstMatch, defaultValue, r.Error()) - } - rp := &r - rp.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.FlagMetadata) - return resultWrapper[R]{result: any(rp).(*R)} + r := e(ctx, provider) + if r.detail.Error() != nil && r.detail.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { + continue + } + if r.detail.Error() != nil { + return buildDefaultResult[R](StrategyFirstMatch, defaultVal, r.detail.Error()) } + + // success! + r.detail.FlagMetadata = setFlagMetadata(StrategyFirstMatch, provider.Name, r.detail.FlagMetadata) + return r } // Build a default result if no matches are found - return buildDefaultResult[R](StrategyFirstMatch, defaultValue, nil) + return buildDefaultResult[R](StrategyFirstMatch, defaultVal, nil) } diff --git a/providers/multi-provider/internal/strategies/first_match_test.go b/providers/multi-provider/internal/strategies/first_match_test.go index de1cb1fbc..6061f16e1 100644 --- a/providers/multi-provider/internal/strategies/first_match_test.go +++ b/providers/multi-provider/internal/strategies/first_match_test.go @@ -27,9 +27,8 @@ func createMockProviders(ctrl *gomock.Controller, count int) ([]*NamedProvider, } func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { - ctrl := gomock.NewController(t) - t.Run("Single Provider Match", func(t *testing.T) { + ctrl := gomock.NewController(t) providers, mocks := createMockProviders(ctrl, 1) mocks[providers[0].Name].EXPECT(). BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). @@ -43,6 +42,7 @@ func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { }) t.Run("Default Resolution", func(t *testing.T) { + ctrl := gomock.NewController(t) providers, mocks := createMockProviders(ctrl, 1) mocks[providers[0].Name].EXPECT(). BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). @@ -62,6 +62,7 @@ func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { }) t.Run("Evaluation stops after match", func(t *testing.T) { + ctrl := gomock.NewController(t) providers, mocks := createMockProviders(ctrl, 5) mocks[providers[0].Name].EXPECT(). BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). @@ -89,6 +90,7 @@ func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { }) t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { + ctrl := gomock.NewController(t) providers, mocks := createMockProviders(ctrl, 5) mocks[providers[0].Name].EXPECT(). BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). From 7248cf8eeb639460c8be54cf7209196311c609fb Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 16:55:38 -0400 Subject: [PATCH 44/68] refactor: Rename test helper Signed-off-by: Jordan Blacker --- .../internal/strategies/first_success_test.go | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/providers/multi-provider/internal/strategies/first_success_test.go b/providers/multi-provider/internal/strategies/first_success_test.go index 2877bdfdb..f95310165 100644 --- a/providers/multi-provider/internal/strategies/first_success_test.go +++ b/providers/multi-provider/internal/strategies/first_success_test.go @@ -12,7 +12,7 @@ import ( const TestFlag = "test-flag" -func configureFlags[R bool | int64 | float64 | string | interface{}](provider *mocks.MockFeatureProvider, resultVal R, state bool, error bool, delay time.Duration) { +func configureFirstSuccessProvider[R bool | int64 | float64 | string | interface{}](provider *mocks.MockFeatureProvider, resultVal R, state bool, error bool, delay time.Duration) { var rErr of.ResolutionError var variant string var reason of.Reason @@ -80,7 +80,7 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider, true, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, true, true, false, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -99,9 +99,9 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, true, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, true, true, false, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, false, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, false, false, true, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -124,9 +124,9 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, true, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, true, true, false, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, false, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, false, false, true, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -149,11 +149,11 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, false, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, false, false, true, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, false, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, false, false, true, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider3, false, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, false, false, true, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -185,7 +185,7 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -204,9 +204,9 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -229,9 +229,9 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -254,11 +254,11 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -290,7 +290,7 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -309,9 +309,9 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -334,9 +334,9 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -359,11 +359,11 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -395,7 +395,7 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -414,9 +414,9 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -439,9 +439,9 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -464,11 +464,11 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -500,7 +500,7 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -519,9 +519,9 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -544,9 +544,9 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -569,11 +569,11 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFlags(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { From 9228c94760f607020b96aa1b02416dd67904efa5 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 17:00:21 -0400 Subject: [PATCH 45/68] refactor: Alias package to prevent shadowing Signed-off-by: Jordan Blacker --- .../internal/strategies/first_match_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/providers/multi-provider/internal/strategies/first_match_test.go b/providers/multi-provider/internal/strategies/first_match_test.go index 6061f16e1..d811c2734 100644 --- a/providers/multi-provider/internal/strategies/first_match_test.go +++ b/providers/multi-provider/internal/strategies/first_match_test.go @@ -3,18 +3,18 @@ package strategies import ( "context" "fmt" - "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" + m "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" of "github.com/open-feature/go-sdk/openfeature" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "testing" ) -func createMockProviders(ctrl *gomock.Controller, count int) ([]*NamedProvider, map[string]*mocks.MockFeatureProvider) { +func createMockProviders(ctrl *gomock.Controller, count int) ([]*NamedProvider, map[string]*m.MockFeatureProvider) { providers := make([]*NamedProvider, 0, count) - providerMocks := make(map[string]*mocks.MockFeatureProvider) + providerMocks := make(map[string]*m.MockFeatureProvider) for index := range count { - provider := mocks.NewMockFeatureProvider(ctrl) + provider := m.NewMockFeatureProvider(ctrl) namedProvider := NamedProvider{ Provider: provider, Name: fmt.Sprintf("%d", index), From 19ccefe1198b9e38ebeacd01650d8bb3d032107a Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 20:59:50 -0400 Subject: [PATCH 46/68] refactor: Modify comparison implementation with comp functions Signed-off-by: Jordan Blacker --- .../internal/strategies/comparison.go | 264 +++- .../internal/strategies/comparison_test.go | 1404 ++++++++++++++++- .../internal/strategies/strategies.go | 2 + 3 files changed, 1580 insertions(+), 90 deletions(-) diff --git a/providers/multi-provider/internal/strategies/comparison.go b/providers/multi-provider/internal/strategies/comparison.go index 197891ac0..6fa2c22e7 100644 --- a/providers/multi-provider/internal/strategies/comparison.go +++ b/providers/multi-provider/internal/strategies/comparison.go @@ -6,16 +6,23 @@ import ( "errors" of "github.com/open-feature/go-sdk/openfeature" "golang.org/x/sync/errgroup" + "slices" "strings" ) +const ( + MetadataIsDefault = "multiprovider-is-default-result" + MetadataNoneFound = "multiprovider-flag-not-found-all-providers" + MetadataEvaluationError = "multiprovider-comparison-first-error" +) + type ( ComparisonStrategy struct { providers []*NamedProvider fallbackProvider of.FeatureProvider } - evaluator[R resultConstraint] func(ctx context.Context, p *NamedProvider) resultWrapper[R] + comparator[R bool | string | int64 | float64] func(values []R) bool ) var _ Strategy = (*ComparisonStrategy)(nil) @@ -41,22 +48,28 @@ func (c ComparisonStrategy) BooleanEvaluation(ctx context.Context, flag string, detail: result.ProviderResolutionDetail, } } - results, metadata := evaluateComparison[of.BoolResolutionDetail, bool](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) - if len(results) == 1 { - results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name - return *results[0].result - } + compFunc := func(values []bool) bool { + current := values[0] + match := true + for i, v := range values { + if i == 0 { + continue + } + if current != v { + match = false + break + } + } - reason := ReasonAggregated - if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { - reason = ReasonAggregatedFallback + return match } + results, metadata := evaluateComparison[of.BoolResolutionDetail, bool](ctx, c.providers, evalFunc, compFunc, c.fallbackProvider, defaultValue) return of.BoolResolutionDetail{ Value: results[0].result.Value, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.ResolutionError{}, - Reason: reason, + ResolutionError: comparisonResolutionError(metadata), + Reason: comparisonResolutionReason(metadata), Variant: results[0].detail.Variant, FlagMetadata: metadata, }, @@ -73,22 +86,28 @@ func (c ComparisonStrategy) StringEvaluation(ctx context.Context, flag string, d detail: result.ProviderResolutionDetail, } } - results, metadata := evaluateComparison[of.StringResolutionDetail, string](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) - if len(results) == 1 { - results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name - return *results[0].result - } + compFunc := func(values []string) bool { + current := values[0] + match := true + for i, v := range values { + if i == 0 { + continue + } + if current != v { + match = false + break + } + } - reason := ReasonAggregated - if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { - reason = ReasonAggregatedFallback + return match } + results, metadata := evaluateComparison[of.StringResolutionDetail, string](ctx, c.providers, evalFunc, compFunc, c.fallbackProvider, defaultValue) return of.StringResolutionDetail{ Value: results[0].result.Value, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.ResolutionError{}, - Reason: reason, + ResolutionError: comparisonResolutionError(metadata), + Reason: comparisonResolutionReason(metadata), Variant: results[0].detail.Variant, FlagMetadata: metadata, }, @@ -105,22 +124,28 @@ func (c ComparisonStrategy) FloatEvaluation(ctx context.Context, flag string, de detail: result.ProviderResolutionDetail, } } - results, metadata := evaluateComparison[of.FloatResolutionDetail, float64](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) - if len(results) == 1 { - results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name - return *results[0].result - } + compFunc := func(values []float64) bool { + current := values[0] + match := true + for i, v := range values { + if i == 0 { + continue + } + if current != v { + match = false + break + } + } - reason := ReasonAggregated - if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { - reason = ReasonAggregatedFallback + return match } + results, metadata := evaluateComparison[of.FloatResolutionDetail, float64](ctx, c.providers, evalFunc, compFunc, c.fallbackProvider, defaultValue) return of.FloatResolutionDetail{ Value: results[0].result.Value, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.ResolutionError{}, - Reason: reason, + ResolutionError: comparisonResolutionError(metadata), + Reason: comparisonResolutionReason(metadata), Variant: results[0].detail.Variant, FlagMetadata: metadata, }, @@ -137,28 +162,59 @@ func (c ComparisonStrategy) IntEvaluation(ctx context.Context, flag string, defa detail: result.ProviderResolutionDetail, } } - results, metadata := evaluateComparison[of.IntResolutionDetail, int64](ctx, c.providers, evalFunc, c.fallbackProvider, defaultValue) - if len(results) == 1 { - results[0].result.FlagMetadata[MetadataSuccessfulProviderName] = results[0].name - return *results[0].result - } + compFunc := func(values []int64) bool { + current := values[0] + match := true + for i, v := range values { + if i == 0 { + continue + } + if current != v { + match = false + break + } + } - reason := ReasonAggregated - if fallbackUsed, ok := metadata[MetadataFallbackUsed].(bool); fallbackUsed && ok { - reason = ReasonAggregatedFallback + return match } - + results, metadata := evaluateComparison[of.IntResolutionDetail, int64](ctx, c.providers, evalFunc, compFunc, c.fallbackProvider, defaultValue) return of.IntResolutionDetail{ Value: results[0].result.Value, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.ResolutionError{}, - Reason: reason, + ResolutionError: comparisonResolutionError(metadata), + Reason: comparisonResolutionReason(metadata), Variant: results[0].detail.Variant, FlagMetadata: metadata, }, } } +func comparisonResolutionReason(metadata of.FlagMetadata) of.Reason { + reason := ReasonAggregated + if fallbackUsed, err := metadata.GetBool(MetadataFallbackUsed); fallbackUsed && err == nil { + reason = ReasonAggregatedFallback + } else if defaultUsed, err := metadata.GetBool(MetadataIsDefault); defaultUsed && err == nil { + reason = of.DefaultReason + } + return reason +} + +func comparisonResolutionError(metadata of.FlagMetadata) of.ResolutionError { + if isDefault, err := metadata.GetBool(MetadataIsDefault); err != nil || !isDefault { + return of.ResolutionError{} + } + + if notFound, err := metadata.GetBool(MetadataNoneFound); err == nil && notFound { + return of.NewFlagNotFoundResolutionError("not found in any providers") + } + + if evalErr, err := metadata.GetString(MetadataEvaluationError); evalErr != "" && err != nil { + return of.NewGeneralResolutionError(evalErr) + } + + return of.NewGeneralResolutionError("comparison failure") +} + func (c ComparisonStrategy) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { metadata := make(of.FlagMetadata) metadata[MetadataStrategyUsed] = StrategyComparison @@ -176,74 +232,114 @@ func (c ComparisonStrategy) ObjectEvaluation(ctx context.Context, flag string, d } } -func evaluateComparison[R resultConstraint, DV bool | string | int64 | float64](ctx context.Context, providers []*NamedProvider, e evaluator[R], fallbackProvider of.FeatureProvider, defaultVal DV) ([]resultWrapper[R], of.FlagMetadata) { - resultChan := make(chan resultWrapper[R]) - ctx, cancel := context.WithCancel(ctx) - defer cancel() +func evaluateComparison[R resultConstraint, DV bool | string | int64 | float64](ctx context.Context, providers []*NamedProvider, e evaluator[R], comp comparator[DV], fallbackProvider of.FeatureProvider, defaultVal DV) ([]resultWrapper[R], of.FlagMetadata) { + if len(providers) == 1 { + result := e(ctx, providers[0]) + metadata := setFlagMetadata(StrategyComparison, cmp.Or(result.name, providers[0].Name), make(of.FlagMetadata)) + metadata[MetadataFallbackUsed] = false + return []resultWrapper[R]{result}, metadata + } + + resultChan := make(chan *resultWrapper[R], len(providers)) + notFoundChan := make(chan interface{}) errGrp, ctx := errgroup.WithContext(ctx) for _, provider := range providers { - p := provider errGrp.Go(func() error { - result := e(ctx, p) - notFound := result.detail.ResolutionDetail().ErrorCode == of.FlagNotFoundCode - if result.detail.Error() != nil { - return &providerError{ - providerName: p.Name, - err: result.detail.Error(), + localChan := make(chan *resultWrapper[R]) + + go func(c context.Context, p *NamedProvider) { + result := e(c, p) + localChan <- &result + }(ctx, provider) + + select { + case r := <-localChan: + notFound := r.detail.ResolutionDetail().ErrorCode == of.FlagNotFoundCode + if !notFound && r.detail.Error() != nil { + return &providerError{ + providerName: r.name, + err: r.detail.Error(), + } } + if !notFound { + resultChan <- r + } else { + notFoundChan <- struct{}{} + } + return nil + case <-ctx.Done(): + return nil } - result.name = p.Name - if !notFound { - resultChan <- result - } - return nil }) } - if err := errGrp.Wait(); err != nil { - result := buildDefaultResult[R, DV](StrategyComparison, defaultVal, err) - return []resultWrapper[R]{result}, result.detail.FlagMetadata + results := make([]resultWrapper[R], 0, len(providers)) + resultValues := make([]DV, 0, len(providers)) + notFoundCount := 0 + for { + select { + case <-ctx.Done(): + // Error occurred + result := buildDefaultResult[R, DV](StrategyComparison, defaultVal, ctx.Err()) + metadata := result.detail.FlagMetadata + metadata[MetadataFallbackUsed] = false + metadata[MetadataIsDefault] = true + metadata[MetadataEvaluationError] = ctx.Err().Error() + return []resultWrapper[R]{result}, metadata + case r := <-resultChan: + results = append(results, *r) + resultValues = append(resultValues, r.value.(DV)) + if (len(results) + notFoundCount) == len(providers) { + goto continueComparison + } + case <-notFoundChan: + notFoundCount += 1 + if notFoundCount == len(providers) { + result := buildDefaultResult[R, DV](StrategyComparison, defaultVal, ctx.Err()) + metadata := result.detail.FlagMetadata + metadata[MetadataFallbackUsed] = false + metadata[MetadataIsDefault] = true + return []resultWrapper[R]{result}, metadata + } + if (len(results) + notFoundCount) == len(providers) { + goto continueComparison + } + } } - +continueComparison: // Evaluate Results Are Equal - agreement := true - var resultVal DV - results := make([]resultWrapper[R], 0, len(providers)) metadata := make(of.FlagMetadata) - metadata[MetadataFallbackUsed] = false metadata[MetadataStrategyUsed] = StrategyComparison - success := make([]string, 0, len(providers)) - for r := range resultChan { - results = append(results, r) - current := *r.value.(*DV) - resultVal = cmp.Or(resultVal, current) - agreement = agreement && (resultVal == current) - if !agreement { - break - } - metadata[r.name] = r.detail.FlagMetadata - success = append(success, r.name) - } - metadata[MetadataSuccessfulProviderName+"s"] = strings.Join(success, ", ") - + agreement := comp(resultValues) if agreement { + metadata[MetadataFallbackUsed] = false + metadata[MetadataIsDefault] = false + success := make([]string, 0, len(providers)) + for _, r := range results { + metadata[r.name] = r.detail.FlagMetadata + success = append(success, r.name) + } + // maintain stable order of metadata results + slices.Sort(success) + metadata[MetadataSuccessfulProviderName+"s"] = strings.Join(success, ", ") return results, metadata } if fallbackProvider != nil { fallbackResult := e(ctx, &NamedProvider{Name: "fallback", Provider: fallbackProvider}) metadata = fallbackResult.detail.FlagMetadata - metadata[MetadataStrategyUsed] = StrategyComparison metadata[MetadataFallbackUsed] = true + metadata[MetadataIsDefault] = false + metadata[MetadataSuccessfulProviderName] = "fallback" + metadata[MetadataStrategyUsed] = StrategyComparison return []resultWrapper[R]{fallbackResult}, metadata } defaultResult := buildDefaultResult[R, DV](StrategyComparison, defaultVal, errors.New("no fallback provider configured")) metadata = defaultResult.detail.FlagMetadata - metadata[MetadataSuccessfulProviderName] = "none" metadata[MetadataFallbackUsed] = false - metadata[MetadataStrategyUsed] = StrategyComparison - return []resultWrapper[R]{defaultResult}, metadata + metadata[MetadataIsDefault] = true + return []resultWrapper[R]{defaultResult}, metadata } diff --git a/providers/multi-provider/internal/strategies/comparison_test.go b/providers/multi-provider/internal/strategies/comparison_test.go index 101f22332..0a8eeab94 100644 --- a/providers/multi-provider/internal/strategies/comparison_test.go +++ b/providers/multi-provider/internal/strategies/comparison_test.go @@ -1,23 +1,1415 @@ package strategies -import "testing" +import ( + "context" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" + of "github.com/open-feature/go-sdk/openfeature" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "testing" +) + +const ( + TestErrorNone = 0 + TestErrorNotFound = 1 + TestErrorError = 2 +) + +func configureComparisonProvider[R bool | int64 | float64 | string | interface{}](provider *mocks.MockFeatureProvider, resultVal R, state bool, error int) { + var rErr of.ResolutionError + var variant string + var reason of.Reason + switch error { + case TestErrorError: + rErr = of.NewGeneralResolutionError("test error") + reason = of.DisabledReason + case TestErrorNotFound: + rErr = of.NewFlagNotFoundResolutionError("not found") + reason = of.DefaultReason + } + if state { + variant = "on" + } else { + variant = "off" + } + details := of.ProviderResolutionDetail{ + ResolutionError: rErr, + Reason: reason, + Variant: variant, + FlagMetadata: make(of.FlagMetadata), + } + + switch any(resultVal).(type) { + case bool: + provider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal bool, evalCtx of.FlattenedContext) of.BoolResolutionDetail { + return of.BoolResolutionDetail{ + Value: any(resultVal).(bool), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + case string: + provider.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal string, evalCtx of.FlattenedContext) of.StringResolutionDetail { + return of.StringResolutionDetail{ + Value: any(resultVal).(string), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + case int64: + provider.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal int64, evalCtx of.FlattenedContext) of.IntResolutionDetail { + return of.IntResolutionDetail{ + Value: any(resultVal).(int64), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + case float64: + provider.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal float64, evalCtx of.FlattenedContext) of.FloatResolutionDetail { + return of.FloatResolutionDetail{ + Value: any(resultVal).(float64), + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + default: + provider.EXPECT().ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(c context.Context, flag string, defaultVal any, evalCtx of.FlattenedContext) of.InterfaceResolutionDetail { + return of.InterfaceResolutionDetail{ + Value: resultVal, + ProviderResolutionDetail: details, + } + }).MaxTimes(1) + } +} func Test_ComparisonStrategy_BooleanEvaluation(t *testing.T) { - // TODO + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + configureComparisonProvider(provider, true, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("two success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, true, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, true, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, true, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, true, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, true, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2, test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, false, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, false, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, true, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, false, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, false, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, true, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, true, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3, test-provider4", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure uses fallback", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.BoolResolutionDetail{ + Value: true, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, false, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, false, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, true, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure with not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.BoolResolutionDetail{ + Value: true, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, false, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, false, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, true, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, false, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.True(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("not found all providers", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, false, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, false, true, TestErrorNotFound) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.False(t, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("non FLAG_NOT_FOUND error causes default", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, true, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, false, true, TestErrorError) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.False(t, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + + }) } func Test_ComparisonStrategy_StringEvaluation(t *testing.T) { - // TODO + successVal := "success" + defaultVal := "default" + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + configureComparisonProvider(provider, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("two success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, successVal, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2, test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3, test-provider4", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure uses fallback", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.StringResolutionDetail{ + Value: successVal, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure with not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.StringResolutionDetail{ + Value: successVal, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, defaultVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("not found all providers", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("non FLAG_NOT_FOUND error causes default", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorError) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) } func Test_ComparisonStrategy_IntEvaluation(t *testing.T) { - // TODO + successVal := int64(1234) + defaultVal := int64(0) + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + configureComparisonProvider(provider, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("two success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, successVal, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2, test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3, test-provider4", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure uses fallback", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.IntResolutionDetail{ + Value: successVal, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("not found all providers", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure with not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.IntResolutionDetail{ + Value: successVal, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, defaultVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("non FLAG_NOT_FOUND error causes default", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorError) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) } func Test_ComparisonStrategy_FloatEvaluation(t *testing.T) { - // TODO + successVal := float64(1234) + defaultVal := float64(0) + t.Run("single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider := mocks.NewMockFeatureProvider(ctrl) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + configureComparisonProvider(provider, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider", + Provider: provider, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "test-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("two success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, successVal, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider1, test-provider2, test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with single success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("multiple not found with multiple success", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Equal(t, "test-provider3, test-provider4", result.FlagMetadata[MetadataSuccessfulProviderName+"s"]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure uses fallback", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.FloatResolutionDetail{ + Value: successVal, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNone) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("comparison failure with not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(of.FloatResolutionDetail{ + Value: successVal, + ProviderResolutionDetail: of.ProviderResolutionDetail{ + ResolutionError: of.ResolutionError{}, + Variant: "on", + Reason: "", + FlagMetadata: make(of.FlagMetadata), + }, + }) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider3, successVal, true, TestErrorNone) + provider4 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider4, defaultVal, true, TestErrorNone) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + { + Name: "test-provider3", + Provider: provider3, + }, + { + Name: "test-provider4", + Provider: provider4, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, successVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "fallback", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.True(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("not found all providers", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, defaultVal, true, TestErrorNotFound) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorNotFound) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Contains(t, result.FlagMetadata, MetadataFallbackUsed) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) + + t.Run("non FLAG_NOT_FOUND error causes default", func(t *testing.T) { + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorError) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) + }) } -func Test_ComparisonStrategy_ObjectEvaluation(t *testing.T) { +func Test_ComparisonStrategy_ObjectEvaluation_AlwaysReturnsDefault(t *testing.T) { + successVal := struct{ Name string }{Name: "test"} + defaultVal := struct{}{} + ctrl := gomock.NewController(t) + fallback := mocks.NewMockFeatureProvider(ctrl) + fallback.EXPECT().FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider1, successVal, true, TestErrorNone) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureComparisonProvider(provider2, defaultVal, true, TestErrorError) + + strategy := NewComparisonStrategy([]*NamedProvider{ + { + Name: "test-provider1", + Provider: provider1, + }, + { + Name: "test-provider2", + Provider: provider2, + }, + }, fallback) + result := strategy.ObjectEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, of.DefaultReason, result.Reason) + assert.Equal(t, of.NewGeneralResolutionError(ErrAggregationNotAllowedText), result.ResolutionError) + assert.Contains(t, result.FlagMetadata, MetadataStrategyUsed) + assert.Equal(t, StrategyComparison, result.FlagMetadata[MetadataStrategyUsed]) + assert.NotContains(t, result.FlagMetadata, MetadataSuccessfulProviderName+"s") + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.False(t, result.FlagMetadata[MetadataFallbackUsed].(bool)) } diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index 7566ec463..e5c2db5eb 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -54,6 +54,8 @@ type ( value any detail of.ProviderResolutionDetail } + + evaluator[R resultConstraint] func(ctx context.Context, p *NamedProvider) resultWrapper[R] ) var _ error = (*providerError)(nil) From 649078f871289fc66c6f30e09e248abb1218ecb9 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Wed, 23 Apr 2025 21:20:22 -0400 Subject: [PATCH 47/68] doc: Update README Signed-off-by: Jordan Blacker --- providers/multi-provider/README.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/providers/multi-provider/README.md b/providers/multi-provider/README.md index 41adc9060..df58bcc1f 100644 --- a/providers/multi-provider/README.md +++ b/providers/multi-provider/README.md @@ -36,17 +36,24 @@ providers["providerB"] = providerB provider, err := mp.NewMultiProvider(providers, mp.StrategyFirstMatch, WithLogger(myLogger)) openfeature.SetProvider(provider) ``` - + +# Options + +- `WithTimeout` - the duration is used for the total timeout across parallel operations. If none is set it will default +to 5 seconds. This is not supported for `FirstMatch` yet, which executes sequentially +- `WithFallbackProvider` - Used for setting a fallback provider for the `Comparison` strategy +- `WithLogger` - Provides slog support # Strategies There are multiple strategies that can be used to determine the result returned to the caller. A strategy must be set at initialization time. -There are 2 strategies available currently: +There are 3 strategies available currently: - _First Match_ - _First Success_ +- _Comparison_ ## First Match Strategy @@ -59,4 +66,18 @@ value along with setting the error details if a detailed request is issued. (all The First Success strategy works by calling each provider in **parallel**. The first provider that returns a response with no errors is returned and all other calls are cancelled. If no provider provides a successful result the default -value will be returned to the caller. \ No newline at end of file +value will be returned to the caller. + +## Comparison + +The Comparison strategy works by calling each provider in **parallel**. All results are collected from each provider and +then the resolved results are compared to each other. If they all agree then that value is returned. If not and a fallback +provider is specified then the fallback will be executed. If no fallback is configured then the default value will be +returned. If a provider returns `FLAG_NOT_FOUND` that is not included in the comparison. If all providers +return not found then the default value is returned. Finally, if any provider returns an error other than `FLAG_NOT_FOUND` +the evaluation immediately stops and that error result is returned. This strategy does NOT support `ObjectEvaluation` + +# Not Yet Implemented + +- Hooks support +- Event support \ No newline at end of file From 4414d9dec49194e0c3eaca5f2135ed60012c0b00 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 10:23:21 -0400 Subject: [PATCH 48/68] fix: Remove duplicated error type Signed-off-by: Jordan Blacker --- .../multi-provider/internal/strategies/comparison.go | 7 ++++--- .../multi-provider/internal/strategies/strategies.go | 11 ----------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/providers/multi-provider/internal/strategies/comparison.go b/providers/multi-provider/internal/strategies/comparison.go index 6fa2c22e7..3b1c4df45 100644 --- a/providers/multi-provider/internal/strategies/comparison.go +++ b/providers/multi-provider/internal/strategies/comparison.go @@ -4,6 +4,7 @@ import ( "cmp" "context" "errors" + mperr "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/errors" of "github.com/open-feature/go-sdk/openfeature" "golang.org/x/sync/errgroup" "slices" @@ -256,9 +257,9 @@ func evaluateComparison[R resultConstraint, DV bool | string | int64 | float64]( case r := <-localChan: notFound := r.detail.ResolutionDetail().ErrorCode == of.FlagNotFoundCode if !notFound && r.detail.Error() != nil { - return &providerError{ - providerName: r.name, - err: r.detail.Error(), + return &mperr.ProviderError{ + ProviderName: r.name, + Err: r.detail.Error(), } } if !notFound { diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index e5c2db5eb..74e1e4728 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -39,11 +39,6 @@ type ( Provider of.FeatureProvider } - providerError struct { - providerName string - err error - } - resultConstraint interface { of.BoolResolutionDetail | of.IntResolutionDetail | of.StringResolutionDetail | of.FloatResolutionDetail | of.InterfaceResolutionDetail } @@ -58,12 +53,6 @@ type ( evaluator[R resultConstraint] func(ctx context.Context, p *NamedProvider) resultWrapper[R] ) -var _ error = (*providerError)(nil) - -func (p providerError) Error() string { - return p.providerName + ": " + p.err.Error() -} - // buildDefaultResult Creates a default result using reflection via generics func buildDefaultResult[R resultConstraint, DV bool | string | int64 | float64 | interface{}](strategy EvaluationStrategy, defaultValue DV, err error) resultWrapper[R] { result := *new(R) From 366e6695c76526afa9d7df6998894c2fe4b131e5 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 10:24:32 -0400 Subject: [PATCH 49/68] fix: Add missing imports to strategies due to merge resolution issues Signed-off-by: Jordan Blacker --- providers/multi-provider/internal/strategies/strategies.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index 74e1e4728..cd12401cc 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -5,6 +5,8 @@ import ( "context" of "github.com/open-feature/go-sdk/openfeature" "reflect" + "regexp" + "strings" ) const ( From d52aa412b2655eb759331d6d7232bf26d545db6e Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 10:25:55 -0400 Subject: [PATCH 50/68] fix: Patch bug related to extracting providers from MultiProvider struct Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 11 ++++++++++- providers/multi-provider/pkg/providers_test.go | 7 +++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index f8be822e7..788906109 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -69,6 +69,15 @@ func (m ProviderMap) AsNamedProviderSlice() []*strategies.NamedProvider { return s } +// Size The size of the map. This operates in O(n) time. +func (m ProviderMap) Size() int { + count := 0 + for range m { + count += 1 + } + return count +} + func (m ProviderMap) buildMetadata() of.Metadata { var separator string metaName := "MultiProvider {" @@ -165,7 +174,7 @@ func (mp *MultiProvider) Metadata() of.Metadata { return mp.metadata } -// Hooks returns a collection of of.Hook defined by this provider +// Hooks returns a collection of.Hook defined by this provider func (mp *MultiProvider) Hooks() []of.Hook { // Hooks that should be included with the provider return []of.Hook{} diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 878b41ecf..b4cf4d98f 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -9,7 +9,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "maps" "regexp" "testing" "time" @@ -84,10 +83,10 @@ func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { p := mp.ProvidersByName() - require.Len(t, p, 2) - require.Contains(t, maps.Keys(p), "provider1") + assert.Equal(t, 2, p.Size()) + require.Contains(t, p, "provider1") assert.Equal(t, p["provider1"], testProvider1) - require.Contains(t, maps.Keys(p), "provider2") + require.Contains(t, p, "provider2") assert.Equal(t, p["provider2"], testProvider2) } From 4415ee3a3c1a60e37bd6ed7f3e459ed08a435af8 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 10:26:23 -0400 Subject: [PATCH 51/68] cleanup: Remove unused components from provider tests Signed-off-by: Jordan Blacker --- .../multi-provider/pkg/providers_test.go | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index b4cf4d98f..483e1f87e 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -2,55 +2,18 @@ package multiprovider import ( "errors" - "fmt" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" + "github.com/open-feature/go-sdk/openfeature" of "github.com/open-feature/go-sdk/openfeature" + oft "github.com/open-feature/go-sdk/openfeature/testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "regexp" "testing" - "time" - - "github.com/open-feature/go-sdk/openfeature" - oft "github.com/open-feature/go-sdk/openfeature/testing" ) -// MockProvider utilizes openfeature's TestProvider to add testable Init & Shutdown methods to test the MultiProvider functionality -type MockProvider struct { - oft.TestProvider - InitCount *int - ShutCount *int - TestErr string - InitDelay int - ShutDelay int - MockMeta string -} - -func (m *MockProvider) Init(evalCtx openfeature.EvaluationContext) error { - if m.TestErr != "" { - return fmt.Errorf(m.TestErr) - } - - if m.InitDelay != 0 { - time.Sleep(time.Duration(m.InitDelay) * time.Millisecond) - } - *m.InitCount += 1 - return nil -} - -func (m *MockProvider) Shutdown() { - if m.ShutDelay != 0 { - time.Sleep(time.Duration(m.ShutDelay) * time.Millisecond) - } - *m.ShutCount += 1 -} - -func (m *MockProvider) Metadata() openfeature.Metadata { - return openfeature.Metadata{Name: m.MockMeta} -} - func TestMultiProvider_ProvidersMethod(t *testing.T) { testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() From 02aa95341b81f067b189285723aefcba75ea6637 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 10:41:10 -0400 Subject: [PATCH 52/68] fix: Correct bug related to FirstStrategy error handling (not flag not found errors) Signed-off-by: Jordan Blacker --- .../internal/strategies/first_match.go | 6 ++- .../internal/strategies/first_match_test.go | 43 ++++++++++++------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/providers/multi-provider/internal/strategies/first_match.go b/providers/multi-provider/internal/strategies/first_match.go index e58a77810..67602fffa 100644 --- a/providers/multi-provider/internal/strategies/first_match.go +++ b/providers/multi-provider/internal/strategies/first_match.go @@ -102,7 +102,11 @@ func evaluateFirstMatch[R resultConstraint, DV bool | string | int64 | float64 | continue } if r.detail.Error() != nil { - return buildDefaultResult[R](StrategyFirstMatch, defaultVal, r.detail.Error()) + r.detail.FlagMetadata = mergeFlagTags(r.detail.FlagMetadata, of.FlagMetadata{ + MetadataSuccessfulProviderName: "none", + MetadataStrategyUsed: StrategyFirstMatch, + }) + return r } // success! diff --git a/providers/multi-provider/internal/strategies/first_match_test.go b/providers/multi-provider/internal/strategies/first_match_test.go index d811c2734..f52d99188 100644 --- a/providers/multi-provider/internal/strategies/first_match_test.go +++ b/providers/multi-provider/internal/strategies/first_match_test.go @@ -49,7 +49,7 @@ func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { Return(of.BoolResolutionDetail{ Value: false, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.NewFlagNotFoundResolutionError("not found"), + ResolutionError: of.NewFlagNotFoundResolutionError("not found in any provider"), }, }) strategy := NewFirstMatchStrategy(providers) @@ -92,12 +92,14 @@ func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { ctrl := gomock.NewController(t) providers, mocks := createMockProviders(ctrl, 5) + expectedErr := of.NewGeneralResolutionError("something went wrong") mocks[providers[0].Name].EXPECT(). BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(of.BoolResolutionDetail{ Value: false, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.NewGeneralResolutionError("something went wrong"), + ResolutionError: expectedErr, + Reason: of.ErrorReason, }, }) for i, p := range providers { @@ -109,8 +111,8 @@ func Test_FirstMatchStrategy_BooleanEvaluation(t *testing.T) { strategy := NewFirstMatchStrategy(providers) result := strategy.BooleanEvaluation(context.Background(), "test-string", false, of.FlattenedContext{}) assert.False(t, result.Value) - assert.Equal(t, of.DefaultReason, result.Reason) - assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, of.ErrorReason, result.Reason) + assert.Equal(t, expectedErr.Error(), result.ResolutionError.Error()) assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) }) @@ -180,12 +182,14 @@ func Test_FirstMatchStrategy_StringEvaluation(t *testing.T) { t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { providers, mocks := createMockProviders(ctrl, 5) + expectedErr := of.NewGeneralResolutionError("something went wrong") mocks[providers[0].Name].EXPECT(). StringEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(of.StringResolutionDetail{ Value: "", ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.NewGeneralResolutionError("something went wrong"), + ResolutionError: expectedErr, + Reason: of.ErrorReason, }, }) for i, p := range providers { @@ -197,8 +201,8 @@ func Test_FirstMatchStrategy_StringEvaluation(t *testing.T) { strategy := NewFirstMatchStrategy(providers) result := strategy.StringEvaluation(context.Background(), "test-string", "", of.FlattenedContext{}) assert.Equal(t, "", result.Value) - assert.Equal(t, of.DefaultReason, result.Reason) - assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, of.ErrorReason, result.Reason) + assert.Equal(t, expectedErr.Error(), result.ResolutionError.Error()) assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) }) @@ -268,12 +272,14 @@ func Test_FirstMatchStrategy_IntEvaluation(t *testing.T) { t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { providers, mocks := createMockProviders(ctrl, 5) + expectedErr := of.NewGeneralResolutionError("something went wrong") mocks[providers[0].Name].EXPECT(). IntEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(of.IntResolutionDetail{ Value: 123, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.NewGeneralResolutionError("something went wrong"), + ResolutionError: expectedErr, + Reason: of.ErrorReason, }, }) for i, p := range providers { @@ -285,8 +291,8 @@ func Test_FirstMatchStrategy_IntEvaluation(t *testing.T) { strategy := NewFirstMatchStrategy(providers) result := strategy.IntEvaluation(context.Background(), "test-string", 123, of.FlattenedContext{}) assert.Equal(t, int64(123), result.Value) - assert.Equal(t, of.DefaultReason, result.Reason) - assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, of.ErrorReason, result.Reason) + assert.Equal(t, expectedErr.Error(), result.ResolutionError.Error()) assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) }) @@ -356,12 +362,14 @@ func Test_FirstMatchStrategy_FloatEvaluation(t *testing.T) { t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { providers, mocks := createMockProviders(ctrl, 5) + expectedErr := of.NewGeneralResolutionError("something went wrong") mocks[providers[0].Name].EXPECT(). FloatEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(of.FloatResolutionDetail{ Value: 123.0, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.NewGeneralResolutionError("something went wrong"), + ResolutionError: expectedErr, + Reason: of.ErrorReason, }, }) for i, p := range providers { @@ -373,8 +381,8 @@ func Test_FirstMatchStrategy_FloatEvaluation(t *testing.T) { strategy := NewFirstMatchStrategy(providers) result := strategy.FloatEvaluation(context.Background(), "test-string", 123, of.FlattenedContext{}) assert.Equal(t, 123.0, result.Value) - assert.Equal(t, of.DefaultReason, result.Reason) - assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, of.ErrorReason, result.Reason) + assert.Equal(t, expectedErr.Error(), result.ResolutionError.Error()) assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) }) @@ -404,6 +412,7 @@ func Test_FirstMatchStrategy_ObjectEvaluation(t *testing.T) { Value: struct{}{}, ProviderResolutionDetail: of.ProviderResolutionDetail{ ResolutionError: of.NewFlagNotFoundResolutionError("not found"), + Reason: of.DefaultReason, }, }) strategy := NewFirstMatchStrategy(providers) @@ -444,12 +453,14 @@ func Test_FirstMatchStrategy_ObjectEvaluation(t *testing.T) { t.Run("Evaluation stops after first error that is not a FLAG_NOT_FOUND error", func(t *testing.T) { providers, mocks := createMockProviders(ctrl, 5) + expectedErr := of.NewGeneralResolutionError("something went wrong") mocks[providers[0].Name].EXPECT(). ObjectEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(of.InterfaceResolutionDetail{ Value: struct{}{}, ProviderResolutionDetail: of.ProviderResolutionDetail{ - ResolutionError: of.NewGeneralResolutionError("something went wrong"), + ResolutionError: expectedErr, + Reason: of.ErrorReason, }, }) for i, p := range providers { @@ -461,8 +472,8 @@ func Test_FirstMatchStrategy_ObjectEvaluation(t *testing.T) { strategy := NewFirstMatchStrategy(providers) result := strategy.ObjectEvaluation(context.Background(), "test-string", struct{}{}, of.FlattenedContext{}) assert.Equal(t, struct{}{}, result.Value) - assert.Equal(t, of.DefaultReason, result.Reason) - assert.Equal(t, of.NewGeneralResolutionError("something went wrong").Error(), result.ResolutionError.Error()) + assert.Equal(t, of.ErrorReason, result.Reason) + assert.Equal(t, expectedErr.Error(), result.ResolutionError.Error()) assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) assert.Equal(t, StrategyFirstMatch, result.FlagMetadata[MetadataStrategyUsed]) }) From e60c3acdaafa6dddfca62acf5605fcbf310a9b43 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 11:28:30 -0400 Subject: [PATCH 53/68] fix: Correct default result build The wrong ResolutionError was being set most of the time, this corrects that issue along with allowing for a nil error to be passed in Signed-off-by: Jordan Blacker --- .../internal/strategies/strategies.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/internal/strategies/strategies.go index cd12401cc..88e00d139 100644 --- a/providers/multi-provider/internal/strategies/strategies.go +++ b/providers/multi-provider/internal/strategies/strategies.go @@ -58,9 +58,18 @@ type ( // buildDefaultResult Creates a default result using reflection via generics func buildDefaultResult[R resultConstraint, DV bool | string | int64 | float64 | interface{}](strategy EvaluationStrategy, defaultValue DV, err error) resultWrapper[R] { result := *new(R) + var rErr of.ResolutionError + var reason of.Reason + if err != nil { + rErr = of.NewGeneralResolutionError(cleanErrorMessage(err.Error())) + reason = of.ErrorReason + } else { + rErr = of.NewFlagNotFoundResolutionError("not found in any provider") + reason = of.DefaultReason + } details := of.ProviderResolutionDetail{ - ResolutionError: of.NewFlagNotFoundResolutionError(err.Error()), - Reason: of.DefaultReason, + ResolutionError: rErr, + Reason: reason, FlagMetadata: of.FlagMetadata{MetadataSuccessfulProviderName: "none", MetadataStrategyUsed: strategy}, } switch reflect.TypeOf(result).Name() { From 72ab3a6844bcb0099ad5e89a5035615bbeb37abf Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 11:28:53 -0400 Subject: [PATCH 54/68] fix: Handle flag not found separately from error for FirstSuccess strategy Signed-off-by: Jordan Blacker --- .../internal/strategies/first_success.go | 21 +- .../internal/strategies/first_success_test.go | 253 +++++++++++++++--- 2 files changed, 228 insertions(+), 46 deletions(-) diff --git a/providers/multi-provider/internal/strategies/first_success.go b/providers/multi-provider/internal/strategies/first_success.go index 95c732b2c..10b4a2fbb 100644 --- a/providers/multi-provider/internal/strategies/first_success.go +++ b/providers/multi-provider/internal/strategies/first_success.go @@ -108,6 +108,7 @@ func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 metadata := make(of.FlagMetadata) metadata[MetadataStrategyUsed] = StrategyFirstSuccess errChan := make(chan mperr.ProviderError, len(providers)) + notFoundChan := make(chan interface{}) finishChan := make(chan *resultWrapper[R], len(providers)) ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() @@ -123,7 +124,10 @@ func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 case <-c.Done(): return case r := <-resultChan: - if r.detail.Error() != nil { + if r.detail.Error() != nil && r.detail.ResolutionDetail().ErrorCode == of.FlagNotFoundCode { + notFoundChan <- struct{}{} + return + } else if r.detail.Error() != nil { errChan <- mperr.ProviderError{ Err: r.detail.ResolutionError, ProviderName: p.Name, @@ -137,6 +141,7 @@ func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 } errs := make([]mperr.ProviderError, 0, len(providers)) + notFoundCount := 0 for { if len(errs) == len(providers) { err := mperr.NewAggregateError(errs) @@ -151,8 +156,20 @@ func evaluateFirstSuccess[R resultConstraint, DV bool | string | int64 | float64 return *result, metadata case err := <-errChan: errs = append(errs, err) + case <-notFoundChan: + notFoundCount += 1 + if notFoundCount == len(providers) { + r := buildDefaultResult[R](StrategyFirstSuccess, defaultVal, nil) + return r, r.detail.FlagMetadata + } case <-ctx.Done(): - r := buildDefaultResult[R](StrategyFirstSuccess, defaultVal, ctx.Err()) + var err error + if len(errs) > 0 { + err = mperr.NewAggregateError(errs) + } else { + err = ctx.Err() + } + r := buildDefaultResult[R](StrategyFirstSuccess, defaultVal, err) return r, r.detail.FlagMetadata } } diff --git a/providers/multi-provider/internal/strategies/first_success_test.go b/providers/multi-provider/internal/strategies/first_success_test.go index f95310165..3ca12742f 100644 --- a/providers/multi-provider/internal/strategies/first_success_test.go +++ b/providers/multi-provider/internal/strategies/first_success_test.go @@ -12,14 +12,19 @@ import ( const TestFlag = "test-flag" -func configureFirstSuccessProvider[R bool | int64 | float64 | string | interface{}](provider *mocks.MockFeatureProvider, resultVal R, state bool, error bool, delay time.Duration) { +func configureFirstSuccessProvider[R bool | int64 | float64 | string | interface{}](provider *mocks.MockFeatureProvider, resultVal R, state bool, error int, delay time.Duration) { var rErr of.ResolutionError var variant string var reason of.Reason - if error { + switch error { + case TestErrorError: rErr = of.NewGeneralResolutionError("test error") - reason = of.DisabledReason + reason = of.ErrorReason + case TestErrorNotFound: + rErr = of.NewFlagNotFoundResolutionError("test not found") + reason = of.DefaultReason } + if state { variant = "on" } else { @@ -80,7 +85,7 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider, true, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, true, true, TestErrorNone, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -99,9 +104,9 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, true, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, true, true, TestErrorNone, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, false, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, false, false, TestErrorError, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -124,9 +129,9 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, true, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, true, true, TestErrorNone, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, false, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, false, false, TestErrorError, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -146,14 +151,46 @@ func Test_FirstSuccessStrategy_BooleanEvaluation(t *testing.T) { assert.Equal(t, "success-provider", result.FlagMetadata[MetadataSuccessfulProviderName]) }) - t.Run("all errors", func(t *testing.T) { + t.Run("all errors (not including flag not found)", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, false, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, false, false, TestErrorError, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, false, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, false, false, TestErrorError, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider3, false, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, false, false, TestErrorError, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.BooleanEvaluation(context.Background(), TestFlag, false, of.FlattenedContext{}) + assert.False(t, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.ErrorReason, result.Reason) + }) + + t.Run("all not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider1, false, false, TestErrorNotFound, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider2, false, false, TestErrorNotFound, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider3, false, false, TestErrorNotFound, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -185,7 +222,7 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, TestErrorNone, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -204,9 +241,9 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -229,9 +266,9 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -254,11 +291,43 @@ func Test_FirstSuccessStrategy_StringEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorError, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorError, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.StringEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.ErrorReason, result.Reason) + }) + + t.Run("all not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorNotFound, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorNotFound, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorNotFound, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -290,7 +359,7 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, TestErrorNone, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -309,9 +378,9 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -334,9 +403,9 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -359,11 +428,43 @@ func Test_FirstSuccessStrategy_IntEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorError, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorError, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.IntEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.ErrorReason, result.Reason) + }) + + t.Run("all not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorNotFound, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorNotFound, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorNotFound, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -395,7 +496,7 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, TestErrorNone, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -414,9 +515,9 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -439,9 +540,9 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -464,11 +565,43 @@ func Test_FirstSuccessStrategy_FloatEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorError, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorError, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.FloatEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.ErrorReason, result.Reason) + }) + + t.Run("all not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorNotFound, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorNotFound, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorNotFound, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -500,7 +633,7 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("single success", func(t *testing.T) { ctrl := gomock.NewController(t) provider := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider, successVal, true, false, 0*time.Millisecond) + configureFirstSuccessProvider(provider, successVal, true, TestErrorNone, 0*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -519,9 +652,9 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("first success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 5*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 5*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 50*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -544,9 +677,9 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("second success", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, successVal, true, false, 500*time.Millisecond) + configureFirstSuccessProvider(provider1, successVal, true, TestErrorNone, 500*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 5*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 5*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { @@ -569,11 +702,43 @@ func Test_FirstSuccessStrategy_ObjectEvaluation(t *testing.T) { t.Run("all errors", func(t *testing.T) { ctrl := gomock.NewController(t) provider1 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider1, defaultVal, false, true, 50*time.Millisecond) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorError, 50*time.Millisecond) + provider2 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorError, 40*time.Millisecond) + provider3 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorError, 30*time.Millisecond) + + strategy := NewFirstSuccessStrategy([]*NamedProvider{ + { + Name: "provider1", + Provider: provider1, + }, + { + Name: "provider2", + Provider: provider2, + }, + { + Name: "provider3", + Provider: provider3, + }, + }, 2*time.Second) + + result := strategy.ObjectEvaluation(context.Background(), TestFlag, defaultVal, of.FlattenedContext{}) + assert.Equal(t, defaultVal, result.Value) + assert.Equal(t, StrategyFirstSuccess, result.FlagMetadata[MetadataStrategyUsed]) + assert.Contains(t, result.FlagMetadata, MetadataSuccessfulProviderName) + assert.Equal(t, "none", result.FlagMetadata[MetadataSuccessfulProviderName]) + assert.Equal(t, of.ErrorReason, result.Reason) + }) + + t.Run("all not found", func(t *testing.T) { + ctrl := gomock.NewController(t) + provider1 := mocks.NewMockFeatureProvider(ctrl) + configureFirstSuccessProvider(provider1, defaultVal, false, TestErrorNotFound, 50*time.Millisecond) provider2 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider2, defaultVal, false, true, 40*time.Millisecond) + configureFirstSuccessProvider(provider2, defaultVal, false, TestErrorNotFound, 40*time.Millisecond) provider3 := mocks.NewMockFeatureProvider(ctrl) - configureFirstSuccessProvider(provider3, defaultVal, false, true, 30*time.Millisecond) + configureFirstSuccessProvider(provider3, defaultVal, false, TestErrorNotFound, 30*time.Millisecond) strategy := NewFirstSuccessStrategy([]*NamedProvider{ { From 03836c74c262a1a7aa5416d55914a75862b0565c Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 11:34:55 -0400 Subject: [PATCH 55/68] fix: Move strategies outside of internal Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 2 +- providers/multi-provider/pkg/providers_test.go | 2 +- .../multi-provider/{internal => pkg}/strategies/comparison.go | 0 .../{internal => pkg}/strategies/comparison_test.go | 0 .../multi-provider/{internal => pkg}/strategies/first_match.go | 0 .../{internal => pkg}/strategies/first_match_test.go | 0 .../{internal => pkg}/strategies/first_success.go | 0 .../{internal => pkg}/strategies/first_success_test.go | 0 .../multi-provider/{internal => pkg}/strategies/strategies.go | 0 9 files changed, 2 insertions(+), 2 deletions(-) rename providers/multi-provider/{internal => pkg}/strategies/comparison.go (100%) rename providers/multi-provider/{internal => pkg}/strategies/comparison_test.go (100%) rename providers/multi-provider/{internal => pkg}/strategies/first_match.go (100%) rename providers/multi-provider/{internal => pkg}/strategies/first_match_test.go (100%) rename providers/multi-provider/{internal => pkg}/strategies/first_success.go (100%) rename providers/multi-provider/{internal => pkg}/strategies/first_success_test.go (100%) rename providers/multi-provider/{internal => pkg}/strategies/strategies.go (100%) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 788906109..5c6e4f575 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -4,7 +4,7 @@ import ( "context" "errors" "fmt" - "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/strategies" "golang.org/x/sync/errgroup" "log/slog" "maps" diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index 483e1f87e..e0f5c8157 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -3,7 +3,7 @@ package multiprovider import ( "errors" "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/mocks" - "github.com/open-feature/go-sdk-contrib/providers/multi-provider/internal/strategies" + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/strategies" "github.com/open-feature/go-sdk/openfeature" of "github.com/open-feature/go-sdk/openfeature" oft "github.com/open-feature/go-sdk/openfeature/testing" diff --git a/providers/multi-provider/internal/strategies/comparison.go b/providers/multi-provider/pkg/strategies/comparison.go similarity index 100% rename from providers/multi-provider/internal/strategies/comparison.go rename to providers/multi-provider/pkg/strategies/comparison.go diff --git a/providers/multi-provider/internal/strategies/comparison_test.go b/providers/multi-provider/pkg/strategies/comparison_test.go similarity index 100% rename from providers/multi-provider/internal/strategies/comparison_test.go rename to providers/multi-provider/pkg/strategies/comparison_test.go diff --git a/providers/multi-provider/internal/strategies/first_match.go b/providers/multi-provider/pkg/strategies/first_match.go similarity index 100% rename from providers/multi-provider/internal/strategies/first_match.go rename to providers/multi-provider/pkg/strategies/first_match.go diff --git a/providers/multi-provider/internal/strategies/first_match_test.go b/providers/multi-provider/pkg/strategies/first_match_test.go similarity index 100% rename from providers/multi-provider/internal/strategies/first_match_test.go rename to providers/multi-provider/pkg/strategies/first_match_test.go diff --git a/providers/multi-provider/internal/strategies/first_success.go b/providers/multi-provider/pkg/strategies/first_success.go similarity index 100% rename from providers/multi-provider/internal/strategies/first_success.go rename to providers/multi-provider/pkg/strategies/first_success.go diff --git a/providers/multi-provider/internal/strategies/first_success_test.go b/providers/multi-provider/pkg/strategies/first_success_test.go similarity index 100% rename from providers/multi-provider/internal/strategies/first_success_test.go rename to providers/multi-provider/pkg/strategies/first_success_test.go diff --git a/providers/multi-provider/internal/strategies/strategies.go b/providers/multi-provider/pkg/strategies/strategies.go similarity index 100% rename from providers/multi-provider/internal/strategies/strategies.go rename to providers/multi-provider/pkg/strategies/strategies.go From 72bc105232580072cd4c3d3d17e7bbbd6e2d3dbb Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 12:17:52 -0400 Subject: [PATCH 56/68] feat: Add Makefile Signed-off-by: Jordan Blacker --- providers/multi-provider/Makefile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 providers/multi-provider/Makefile diff --git a/providers/multi-provider/Makefile b/providers/multi-provider/Makefile new file mode 100644 index 000000000..86ee60830 --- /dev/null +++ b/providers/multi-provider/Makefile @@ -0,0 +1,9 @@ +.PHONY: generate test +GOPATH_LOC = ${GOPATH} + +generate: + go generate ./... + mockgen -source=${GOPATH}/github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go -package=mocks -destination=./internal/mocks/provider_mock.go + +test: + go test ./... \ No newline at end of file From 87fdedbeb611be9d3819383590026ec0cdfed048 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 12:18:51 -0400 Subject: [PATCH 57/68] chore: generate mocks Signed-off-by: Jordan Blacker --- .../internal/mocks/provider_mock.go | 2 +- .../internal/mocks/strategy_mock.go | 127 ++++++++++++++++++ .../pkg/strategies/strategies.go | 4 +- 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 providers/multi-provider/internal/mocks/strategy_mock.go diff --git a/providers/multi-provider/internal/mocks/provider_mock.go b/providers/multi-provider/internal/mocks/provider_mock.go index 77f1573ec..d984d81e8 100644 --- a/providers/multi-provider/internal/mocks/provider_mock.go +++ b/providers/multi-provider/internal/mocks/provider_mock.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -source=github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go -package=strategies -destination=./providers/multi-provider/internal/strategies/provider_mock.go +// mockgen -source=github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go -package=mocks -destination=./internal/mocks/provider_mock.go // // Package strategies is a generated GoMock package. diff --git a/providers/multi-provider/internal/mocks/strategy_mock.go b/providers/multi-provider/internal/mocks/strategy_mock.go new file mode 100644 index 000000000..a2e64887a --- /dev/null +++ b/providers/multi-provider/internal/mocks/strategy_mock.go @@ -0,0 +1,127 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: strategies.go +// +// Generated by this command: +// +// mockgen -source=strategies.go -destination=../../internal/mocks/strategy_mock.go -package=mocks +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/strategies" + openfeature "github.com/open-feature/go-sdk/openfeature" + gomock "go.uber.org/mock/gomock" +) + +// MockStrategy is a mock of Strategy interface. +type MockStrategy struct { + ctrl *gomock.Controller + recorder *MockStrategyMockRecorder + isgomock struct{} +} + +// MockStrategyMockRecorder is the mock recorder for MockStrategy. +type MockStrategyMockRecorder struct { + mock *MockStrategy +} + +// NewMockStrategy creates a new mock instance. +func NewMockStrategy(ctrl *gomock.Controller) *MockStrategy { + mock := &MockStrategy{ctrl: ctrl} + mock.recorder = &MockStrategyMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStrategy) EXPECT() *MockStrategyMockRecorder { + return m.recorder +} + +// BooleanEvaluation mocks base method. +func (m *MockStrategy) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BooleanEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.BoolResolutionDetail) + return ret0 +} + +// BooleanEvaluation indicates an expected call of BooleanEvaluation. +func (mr *MockStrategyMockRecorder) BooleanEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BooleanEvaluation", reflect.TypeOf((*MockStrategy)(nil).BooleanEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// FloatEvaluation mocks base method. +func (m *MockStrategy) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FloatEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.FloatResolutionDetail) + return ret0 +} + +// FloatEvaluation indicates an expected call of FloatEvaluation. +func (mr *MockStrategyMockRecorder) FloatEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatEvaluation", reflect.TypeOf((*MockStrategy)(nil).FloatEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// IntEvaluation mocks base method. +func (m *MockStrategy) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IntEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.IntResolutionDetail) + return ret0 +} + +// IntEvaluation indicates an expected call of IntEvaluation. +func (mr *MockStrategyMockRecorder) IntEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntEvaluation", reflect.TypeOf((*MockStrategy)(nil).IntEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// Name mocks base method. +func (m *MockStrategy) Name() strategies.EvaluationStrategy { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Name") + ret0, _ := ret[0].(strategies.EvaluationStrategy) + return ret0 +} + +// Name indicates an expected call of Name. +func (mr *MockStrategyMockRecorder) Name() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockStrategy)(nil).Name)) +} + +// ObjectEvaluation mocks base method. +func (m *MockStrategy) ObjectEvaluation(ctx context.Context, flag string, defaultValue any, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ObjectEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.InterfaceResolutionDetail) + return ret0 +} + +// ObjectEvaluation indicates an expected call of ObjectEvaluation. +func (mr *MockStrategyMockRecorder) ObjectEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectEvaluation", reflect.TypeOf((*MockStrategy)(nil).ObjectEvaluation), ctx, flag, defaultValue, evalCtx) +} + +// StringEvaluation mocks base method. +func (m *MockStrategy) StringEvaluation(ctx context.Context, flag, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StringEvaluation", ctx, flag, defaultValue, evalCtx) + ret0, _ := ret[0].(openfeature.StringResolutionDetail) + return ret0 +} + +// StringEvaluation indicates an expected call of StringEvaluation. +func (mr *MockStrategyMockRecorder) StringEvaluation(ctx, flag, defaultValue, evalCtx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringEvaluation", reflect.TypeOf((*MockStrategy)(nil).StringEvaluation), ctx, flag, defaultValue, evalCtx) +} diff --git a/providers/multi-provider/pkg/strategies/strategies.go b/providers/multi-provider/pkg/strategies/strategies.go index 88e00d139..7f6ead9b7 100644 --- a/providers/multi-provider/pkg/strategies/strategies.go +++ b/providers/multi-provider/pkg/strategies/strategies.go @@ -1,4 +1,6 @@ -// Package strategy Resolution strategies are defined within this package +// Package strategies Resolution strategies are defined within this package +// +//go:generate go run go.uber.org/mock/mockgen -source=strategies.go -destination=../../internal/mocks/strategy_mock.go -package=mocks package strategies import ( From 0c4306cc2c2957e68589fc5a10aad4ae91e707c5 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 12:23:31 -0400 Subject: [PATCH 58/68] fix: Correct mock generation & regenerate Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/strategies/strategies.go | 2 +- .../mocks => pkg/strategies}/strategy_mock.go | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) rename providers/multi-provider/{internal/mocks => pkg/strategies}/strategy_mock.go (93%) diff --git a/providers/multi-provider/pkg/strategies/strategies.go b/providers/multi-provider/pkg/strategies/strategies.go index 7f6ead9b7..3efda055e 100644 --- a/providers/multi-provider/pkg/strategies/strategies.go +++ b/providers/multi-provider/pkg/strategies/strategies.go @@ -1,6 +1,6 @@ // Package strategies Resolution strategies are defined within this package // -//go:generate go run go.uber.org/mock/mockgen -source=strategies.go -destination=../../internal/mocks/strategy_mock.go -package=mocks +//go:generate go run go.uber.org/mock/mockgen -source=strategies.go -destination=../../pkg/strategies/strategy_mock.go -package=strategies package strategies import ( diff --git a/providers/multi-provider/internal/mocks/strategy_mock.go b/providers/multi-provider/pkg/strategies/strategy_mock.go similarity index 93% rename from providers/multi-provider/internal/mocks/strategy_mock.go rename to providers/multi-provider/pkg/strategies/strategy_mock.go index a2e64887a..2bf1ad1a5 100644 --- a/providers/multi-provider/internal/mocks/strategy_mock.go +++ b/providers/multi-provider/pkg/strategies/strategy_mock.go @@ -3,17 +3,16 @@ // // Generated by this command: // -// mockgen -source=strategies.go -destination=../../internal/mocks/strategy_mock.go -package=mocks +// mockgen -source=strategies.go -destination=../../pkg/strategies/strategy_mock.go -package=strategies // -// Package mocks is a generated GoMock package. -package mocks +// Package strategies is a generated GoMock package. +package strategies import ( context "context" reflect "reflect" - strategies "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/strategies" openfeature "github.com/open-feature/go-sdk/openfeature" gomock "go.uber.org/mock/gomock" ) @@ -85,10 +84,10 @@ func (mr *MockStrategyMockRecorder) IntEvaluation(ctx, flag, defaultValue, evalC } // Name mocks base method. -func (m *MockStrategy) Name() strategies.EvaluationStrategy { +func (m *MockStrategy) Name() EvaluationStrategy { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Name") - ret0, _ := ret[0].(strategies.EvaluationStrategy) + ret0, _ := ret[0].(EvaluationStrategy) return ret0 } From f953aac7a1ad0874283d58d1a7e230ce359042ba Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 12:25:08 -0400 Subject: [PATCH 59/68] feat: Add support for custom strategies Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/options.go | 8 +++ providers/multi-provider/pkg/providers.go | 10 ++++ .../multi-provider/pkg/providers_test.go | 53 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/providers/multi-provider/pkg/options.go b/providers/multi-provider/pkg/options.go index 2575a1456..07159b086 100644 --- a/providers/multi-provider/pkg/options.go +++ b/providers/multi-provider/pkg/options.go @@ -1,6 +1,7 @@ package multiprovider import ( + "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/strategies" of "github.com/open-feature/go-sdk/openfeature" "log/slog" "time" @@ -25,6 +26,13 @@ func WithFallbackProvider(p of.FeatureProvider) Option { } } +// WithCustomStrategy sets a custom strategy. This must be used in conjunction with StrategyCustom +func WithCustomStrategy(s strategies.Strategy) Option { + return func(conf *Configuration) { + conf.customStrategy = s + } +} + func WithEventPublishing() Option { return func(conf *Configuration) { conf.publishEvents = true diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 5c6e4f575..24d05789a 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -31,6 +31,7 @@ type ( Configuration struct { useFallback bool fallbackProvider of.FeatureProvider + customStrategy strategies.Strategy logger *slog.Logger publishEvents bool metadata *of.Metadata @@ -54,6 +55,9 @@ const ( // Otherwise, the value from the designated fallback provider's response will be returned. The fallback provider // will be assigned to the first provider registered. (NOT YET IMPLEMENTED, SUBJECT TO CHANGE) StrategyComparison EvaluationStrategy = "comparison" + // StrategyCustom allows for using a custom Strategy implementation. If this is set you MUST use the WithCustomStrategy + // option to set it + StrategyCustom EvaluationStrategy = "strategy-custom" ) var _ of.FeatureProvider = (*MultiProvider)(nil) @@ -149,6 +153,12 @@ func NewMultiProvider(providerMap ProviderMap, evaluationStrategy EvaluationStra strategy = strategies.NewFirstSuccessStrategy(multiProvider.Providers(), config.timeout) case StrategyComparison: strategy = strategies.NewComparisonStrategy(multiProvider.Providers(), config.fallbackProvider) + case StrategyCustom: + if config.customStrategy != nil { + strategy = config.customStrategy + } else { + return nil, fmt.Errorf("A custom strategy must be set via an option if StrategyCustom is set") + } default: return nil, fmt.Errorf("%s is an unknown evalutation strategy", strategy) } diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index e0f5c8157..d0792c294 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -33,6 +33,59 @@ func TestMultiProvider_ProvidersMethod(t *testing.T) { assert.NotNil(t, p[1].Provider) } +func TestMultiProvider_NewMultiProvider(t *testing.T) { + t.Run("nil providerMap returns an error", func(t *testing.T) { + _, err := NewMultiProvider(nil, strategies.StrategyFirstMatch) + require.Errorf(t, err, "providerMap cannot be nil or empty") + }) + + t.Run("naming a provider the empty string returns an error", func(t *testing.T) { + providers := make(ProviderMap) + providers[""] = oft.NewTestProvider() + _, err := NewMultiProvider(providers, strategies.StrategyFirstMatch) + require.Errorf(t, err, "provider name cannot be the empty string") + }) + + t.Run("nil provider within map returns an error", func(t *testing.T) { + providers := make(ProviderMap) + providers["provider1"] = nil + _, err := NewMultiProvider(providers, strategies.StrategyFirstMatch) + require.Errorf(t, err, "provider provider1 cannot be nil") + }) + + t.Run("unknown evaluation strategy returns an error", func(t *testing.T) { + providers := make(ProviderMap) + providers["provider1"] = oft.NewTestProvider() + _, err := NewMultiProvider(providers, "unknown") + require.Errorf(t, err, "unknown is an unknown evaluation strategy") + }) + + t.Run("setting custom strategy without custom strategy option returns error", func(t *testing.T) { + providers := make(ProviderMap) + providers["provider1"] = oft.NewTestProvider() + _, err := NewMultiProvider(providers, StrategyCustom) + require.Errorf(t, err, "A custom strategy must be set via an option if StrategyCustom is set") + }) + + t.Run("success", func(t *testing.T) { + providers := make(ProviderMap) + providers["provider1"] = oft.NewTestProvider() + mp, err := NewMultiProvider(providers, StrategyComparison) + require.NoError(t, err) + assert.NotZero(t, mp) + }) + + t.Run("success with custom provider", func(t *testing.T) { + providers := make(ProviderMap) + providers["provider1"] = oft.NewTestProvider() + ctrl := gomock.NewController(t) + strategy := strategies.NewMockStrategy(ctrl) + mp, err := NewMultiProvider(providers, StrategyCustom, WithCustomStrategy(strategy)) + require.NoError(t, err) + assert.NotZero(t, mp) + }) +} + func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { testProvider1 := oft.NewTestProvider() testProvider2 := oft.NewTestProvider() From 6c43c6bdf72d6dd59e1cce6ab46dc1a593b0e3ab Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 12:34:55 -0400 Subject: [PATCH 60/68] doc: document options Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/options.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/providers/multi-provider/pkg/options.go b/providers/multi-provider/pkg/options.go index 07159b086..bbc38154b 100644 --- a/providers/multi-provider/pkg/options.go +++ b/providers/multi-provider/pkg/options.go @@ -7,18 +7,21 @@ import ( "time" ) +// WithLogger Sets a logger to be used with slog for internal logging func WithLogger(l *slog.Logger) Option { return func(conf *Configuration) { conf.logger = l } } +// WithTimeout Set a timeout for the total runtime for evaluation of parallel strategies func WithTimeout(d time.Duration) Option { return func(conf *Configuration) { conf.timeout = d } } +// WithFallbackProvider Sets a fallback provider when using the StrategyComparison func WithFallbackProvider(p of.FeatureProvider) Option { return func(conf *Configuration) { conf.fallbackProvider = p @@ -33,12 +36,14 @@ func WithCustomStrategy(s strategies.Strategy) Option { } } +// WithEventPublishing Enables event publishing (Not Yet Implemented) func WithEventPublishing() Option { return func(conf *Configuration) { conf.publishEvents = true } } +// WithoutEventPublishing Disables event publishing (this is the default, but included for explicit usage) func WithoutEventPublishing() Option { return func(conf *Configuration) { conf.publishEvents = false From 11daf4f21c9daeb05385d2267b5cc5778a64bfb1 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Thu, 24 Apr 2025 12:44:32 -0400 Subject: [PATCH 61/68] doc: Document public methods in providers.go Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 24d05789a..1d27ac4ec 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -18,6 +18,7 @@ import ( ) type ( + // MultiProvider Provider used for combining multiple providers MultiProvider struct { providers ProviderMap metadata of.Metadata @@ -28,6 +29,7 @@ type ( logger *slog.Logger } + // Configuration MultiProvider's internal configuration Configuration struct { useFallback bool fallbackProvider of.FeatureProvider @@ -41,8 +43,10 @@ type ( // EvaluationStrategy Defines a strategy to use for resolving the result from multiple providers EvaluationStrategy = string - ProviderMap map[string]of.FeatureProvider - Option func(*Configuration) + // ProviderMap A map where the keys are names of providers and the values are the providers themselves + ProviderMap map[string]of.FeatureProvider + // Option Function used for setting Configuration via the options pattern + Option func(*Configuration) ) const ( @@ -62,8 +66,7 @@ const ( var _ of.FeatureProvider = (*MultiProvider)(nil) -// MultiProvider implements of `FeatureProvider` in a way to accept an array of providers. - +// AsNamedProviderSlice Converts the map into a slice of NamedProvider instances func (m ProviderMap) AsNamedProviderSlice() []*strategies.NamedProvider { s := make([]*strategies.NamedProvider, 0, len(m)) for name, provider := range m { @@ -167,14 +170,17 @@ func NewMultiProvider(providerMap ProviderMap, evaluationStrategy EvaluationStra return multiProvider, nil } +// Providers Returns slice of providers wrapped in NamedProvider structs func (mp *MultiProvider) Providers() []*strategies.NamedProvider { return mp.providers.AsNamedProviderSlice() } +// ProvidersByName Returns the internal ProviderMap of the MultiProvider func (mp *MultiProvider) ProvidersByName() ProviderMap { return mp.providers } +// EvaluationStrategy The current set strategy func (mp *MultiProvider) EvaluationStrategy() string { return mp.strategy.Name() } @@ -250,12 +256,14 @@ func (mp *MultiProvider) Init(evalCtx of.EvaluationContext) error { return nil } +// Status the current status of the MultiProvider func (mp *MultiProvider) Status() of.State { mp.mu.RLock() defer mp.mu.RUnlock() return mp.status } +// Shutdown Shuts down all internal providers func (mp *MultiProvider) Shutdown() { var wg sync.WaitGroup for _, provider := range mp.providers { @@ -271,6 +279,7 @@ func (mp *MultiProvider) Shutdown() { wg.Wait() } +// EventChannel the channel emits are emitted on func (mp *MultiProvider) EventChannel() <-chan of.Event { return mp.events } From 74f5ad14827de17c61b58c6fd4991765e46882c0 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Fri, 25 Apr 2025 19:04:34 -0400 Subject: [PATCH 62/68] refactor: downgrade go-sdk Signed-off-by: Jordan Blacker --- providers/multi-provider/go.mod | 5 +++-- providers/multi-provider/go.sum | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/providers/multi-provider/go.mod b/providers/multi-provider/go.mod index 91a059021..b5d510526 100644 --- a/providers/multi-provider/go.mod +++ b/providers/multi-provider/go.mod @@ -2,9 +2,10 @@ module github.com/open-feature/go-sdk-contrib/providers/multi-provider go 1.23.0 -require github.com/open-feature/go-sdk v1.14.1 - require ( + github.com/open-feature/go-sdk v1.13.1 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 +) github.com/go-logr/logr v1.4.2 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect ) diff --git a/providers/multi-provider/go.sum b/providers/multi-provider/go.sum index ac0b45fa2..ad6d288ea 100644 --- a/providers/multi-provider/go.sum +++ b/providers/multi-provider/go.sum @@ -4,6 +4,8 @@ github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= +github.com/open-feature/go-sdk v1.13.1 h1:RJbS70eyi7Jd3Zm5bFnaahNKNDXn+RAVnctpGu+uPis= +github.com/open-feature/go-sdk v1.13.1/go.mod h1:O8r4mhgeRIsjJ0ZBXlnE0BtbT/79W44gQceR7K8KYgo= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= From 3c3abc7bc55739d4abd6e0826aa760861b4e3350 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Fri, 25 Apr 2025 19:04:53 -0400 Subject: [PATCH 63/68] chore: go mod tidy Signed-off-by: Jordan Blacker --- providers/multi-provider/go.mod | 9 ++++++++- providers/multi-provider/go.sum | 20 ++++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/providers/multi-provider/go.mod b/providers/multi-provider/go.mod index b5d510526..a03020f08 100644 --- a/providers/multi-provider/go.mod +++ b/providers/multi-provider/go.mod @@ -4,8 +4,15 @@ go 1.23.0 require ( github.com/open-feature/go-sdk v1.13.1 + github.com/stretchr/testify v1.9.0 + go.uber.org/mock v0.5.1 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/sync v0.7.0 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/providers/multi-provider/go.sum b/providers/multi-provider/go.sum index ad6d288ea..794fea8ad 100644 --- a/providers/multi-provider/go.sum +++ b/providers/multi-provider/go.sum @@ -1,12 +1,24 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/open-feature/go-sdk v1.14.1 h1:jcxjCIG5Up3XkgYwWN5Y/WWfc6XobOhqrIwjyDBsoQo= -github.com/open-feature/go-sdk v1.14.1/go.mod h1:t337k0VB/t/YxJ9S0prT30ISUHwYmUd/jhUZgFcOvGg= github.com/open-feature/go-sdk v1.13.1 h1:RJbS70eyi7Jd3Zm5bFnaahNKNDXn+RAVnctpGu+uPis= github.com/open-feature/go-sdk v1.13.1/go.mod h1:O8r4mhgeRIsjJ0ZBXlnE0BtbT/79W44gQceR7K8KYgo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs= +go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 8846c52efb81cc1042da5d8c9e91a01d11f8d21e Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Fri, 25 Apr 2025 19:05:22 -0400 Subject: [PATCH 64/68] chore: fix makefile & rebuild mocks Signed-off-by: Jordan Blacker --- providers/multi-provider/Makefile | 3 +- .../internal/mocks/provider_mock.go | 44 +++++-------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/providers/multi-provider/Makefile b/providers/multi-provider/Makefile index 86ee60830..941e647cb 100644 --- a/providers/multi-provider/Makefile +++ b/providers/multi-provider/Makefile @@ -3,7 +3,8 @@ GOPATH_LOC = ${GOPATH} generate: go generate ./... - mockgen -source=${GOPATH}/github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go -package=mocks -destination=./internal/mocks/provider_mock.go + go mod download + mockgen -source=${GOPATH}/pkg/mod/github.com/open-feature/go-sdk@v1.13.1/openfeature/provider.go -package=mocks -destination=./internal/mocks/provider_mock.go test: go test ./... \ No newline at end of file diff --git a/providers/multi-provider/internal/mocks/provider_mock.go b/providers/multi-provider/internal/mocks/provider_mock.go index d984d81e8..2f0675dd3 100644 --- a/providers/multi-provider/internal/mocks/provider_mock.go +++ b/providers/multi-provider/internal/mocks/provider_mock.go @@ -1,12 +1,12 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go +// Source: /Users/jordanblacker/go/pkg/mod/github.com/open-feature/go-sdk@v1.13.1/openfeature/provider.go // // Generated by this command: // -// mockgen -source=github.com/open-feature/go-sdk@v1.14.1/openfeature/provider.go -package=mocks -destination=./internal/mocks/provider_mock.go +// mockgen -source=/Users/jordanblacker/go/pkg/mod/github.com/open-feature/go-sdk@v1.13.1/openfeature/provider.go -package=mocks -destination=./internal/mocks/provider_mock.go // -// Package strategies is a generated GoMock package. +// Package mocks is a generated GoMock package. package mocks import ( @@ -189,40 +189,18 @@ func (mr *MockStateHandlerMockRecorder) Shutdown() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockStateHandler)(nil).Shutdown)) } -// MockTracker is a mock of Tracker interface. -type MockTracker struct { - ctrl *gomock.Controller - recorder *MockTrackerMockRecorder - isgomock struct{} -} - -// MockTrackerMockRecorder is the mock recorder for MockTracker. -type MockTrackerMockRecorder struct { - mock *MockTracker -} - -// NewMockTracker creates a new mock instance. -func NewMockTracker(ctrl *gomock.Controller) *MockTracker { - mock := &MockTracker{ctrl: ctrl} - mock.recorder = &MockTrackerMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockTracker) EXPECT() *MockTrackerMockRecorder { - return m.recorder -} - -// Track mocks base method. -func (m *MockTracker) Track(ctx context.Context, trackingEventName string, evaluationContext openfeature.EvaluationContext, details openfeature.TrackingEventDetails) { +// Status mocks base method. +func (m *MockStateHandler) Status() openfeature.State { m.ctrl.T.Helper() - m.ctrl.Call(m, "Track", ctx, trackingEventName, evaluationContext, details) + ret := m.ctrl.Call(m, "Status") + ret0, _ := ret[0].(openfeature.State) + return ret0 } -// Track indicates an expected call of Track. -func (mr *MockTrackerMockRecorder) Track(ctx, trackingEventName, evaluationContext, details any) *gomock.Call { +// Status indicates an expected call of Status. +func (mr *MockStateHandlerMockRecorder) Status() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Track", reflect.TypeOf((*MockTracker)(nil).Track), ctx, trackingEventName, evaluationContext, details) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockStateHandler)(nil).Status)) } // MockEventHandler is a mock of EventHandler interface. From 225a1bc04d3ceadfb4f797722fe79e6007437a65 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Fri, 25 Apr 2025 19:08:07 -0400 Subject: [PATCH 65/68] fix: repair code post downgrade - Replace openfeature's test provider from v1.14 with the `InMemoryProvider` - Comment out ProviderFatalCode from `strategies.cleanErrorMessage` Signed-off-by: Jordan Blacker --- .../multi-provider/pkg/providers_test.go | 28 +++++++++---------- .../pkg/strategies/strategies.go | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index d0792c294..cc3f77c38 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -6,7 +6,7 @@ import ( "github.com/open-feature/go-sdk-contrib/providers/multi-provider/pkg/strategies" "github.com/open-feature/go-sdk/openfeature" of "github.com/open-feature/go-sdk/openfeature" - oft "github.com/open-feature/go-sdk/openfeature/testing" + imp "github.com/open-feature/go-sdk/openfeature/memprovider" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" @@ -15,8 +15,8 @@ import ( ) func TestMultiProvider_ProvidersMethod(t *testing.T) { - testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() + testProvider1 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) + testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) providers := make(ProviderMap) providers["provider1"] = testProvider1 @@ -41,7 +41,7 @@ func TestMultiProvider_NewMultiProvider(t *testing.T) { t.Run("naming a provider the empty string returns an error", func(t *testing.T) { providers := make(ProviderMap) - providers[""] = oft.NewTestProvider() + providers[""] = imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) _, err := NewMultiProvider(providers, strategies.StrategyFirstMatch) require.Errorf(t, err, "provider name cannot be the empty string") }) @@ -55,21 +55,21 @@ func TestMultiProvider_NewMultiProvider(t *testing.T) { t.Run("unknown evaluation strategy returns an error", func(t *testing.T) { providers := make(ProviderMap) - providers["provider1"] = oft.NewTestProvider() + providers["provider1"] = imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) _, err := NewMultiProvider(providers, "unknown") require.Errorf(t, err, "unknown is an unknown evaluation strategy") }) t.Run("setting custom strategy without custom strategy option returns error", func(t *testing.T) { providers := make(ProviderMap) - providers["provider1"] = oft.NewTestProvider() + providers["provider1"] = imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) _, err := NewMultiProvider(providers, StrategyCustom) require.Errorf(t, err, "A custom strategy must be set via an option if StrategyCustom is set") }) t.Run("success", func(t *testing.T) { providers := make(ProviderMap) - providers["provider1"] = oft.NewTestProvider() + providers["provider1"] = imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) mp, err := NewMultiProvider(providers, StrategyComparison) require.NoError(t, err) assert.NotZero(t, mp) @@ -77,7 +77,7 @@ func TestMultiProvider_NewMultiProvider(t *testing.T) { t.Run("success with custom provider", func(t *testing.T) { providers := make(ProviderMap) - providers["provider1"] = oft.NewTestProvider() + providers["provider1"] = imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) ctrl := gomock.NewController(t) strategy := strategies.NewMockStrategy(ctrl) mp, err := NewMultiProvider(providers, StrategyCustom, WithCustomStrategy(strategy)) @@ -87,8 +87,8 @@ func TestMultiProvider_NewMultiProvider(t *testing.T) { } func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { - testProvider1 := oft.NewTestProvider() - testProvider2 := oft.NewTestProvider() + testProvider1 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) + testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) providers := make(ProviderMap) providers["provider1"] = testProvider1 @@ -107,7 +107,7 @@ func TestMultiProvider_ProvidersByNamesMethod(t *testing.T) { } func TestMultiProvider_MetaData(t *testing.T) { - testProvider1 := oft.NewTestProvider() + testProvider1 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) ctrl := gomock.NewController(t) testProvider2 := mocks.NewMockFeatureProvider(ctrl) testProvider2.EXPECT().Metadata().Return(of.Metadata{ @@ -131,7 +131,7 @@ func TestMultiProvider_Init(t *testing.T) { testProvider1 := mocks.NewMockFeatureProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) - testProvider2 := oft.NewTestProvider() + testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) testProvider3 := mocks.NewMockFeatureProvider(ctrl) testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) @@ -169,7 +169,7 @@ func TestMultiProvider_InitErrorWithProvider(t *testing.T) { testProvider1 := mocks.NewMockFeatureProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) - testProvider2 := oft.NewTestProvider() + testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) providers := make(ProviderMap) providers["provider1"] = testProvider1 @@ -194,7 +194,7 @@ func TestMultiProvider_Shutdown(t *testing.T) { testProvider1 := mocks.NewMockFeatureProvider(ctrl) testProvider1.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) - testProvider2 := oft.NewTestProvider() + testProvider2 := imp.NewInMemoryProvider(map[string]imp.InMemoryFlag{}) testProvider3 := mocks.NewMockFeatureProvider(ctrl) testProvider3.EXPECT().Metadata().Return(of.Metadata{Name: "MockProvider"}) diff --git a/providers/multi-provider/pkg/strategies/strategies.go b/providers/multi-provider/pkg/strategies/strategies.go index 3efda055e..4670acfc6 100644 --- a/providers/multi-provider/pkg/strategies/strategies.go +++ b/providers/multi-provider/pkg/strategies/strategies.go @@ -117,7 +117,7 @@ func setFlagMetadata(strategyUsed EvaluationStrategy, successProviderName string func cleanErrorMessage(msg string) string { codeRegex := strings.Join([]string{ string(of.ProviderNotReadyCode), - string(of.ProviderFatalCode), + //string(of.ProviderFatalCode), // TODO: not available until go-sdk 14 string(of.FlagNotFoundCode), string(of.ParseErrorCode), string(of.TypeMismatchCode), From 3e092fcd767780db0cec2599e50d1d64d20765f2 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Fri, 25 Apr 2025 19:11:47 -0400 Subject: [PATCH 66/68] doc: Update documentation Signed-off-by: Jordan Blacker --- providers/multi-provider/README.md | 3 ++- providers/multi-provider/pkg/providers.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/providers/multi-provider/README.md b/providers/multi-provider/README.md index df58bcc1f..b2bab36e1 100644 --- a/providers/multi-provider/README.md +++ b/providers/multi-provider/README.md @@ -80,4 +80,5 @@ the evaluation immediately stops and that error result is returned. This strateg # Not Yet Implemented - Hooks support -- Event support \ No newline at end of file +- Event support +- Full slog support \ No newline at end of file diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index 1d27ac4ec..fa6f1a7ee 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -57,7 +57,7 @@ const ( StrategyFirstSuccess EvaluationStrategy = strategies.StrategyFirstSuccess // StrategyComparison All providers are called in parallel. If all responses agree the value will be returned. // Otherwise, the value from the designated fallback provider's response will be returned. The fallback provider - // will be assigned to the first provider registered. (NOT YET IMPLEMENTED, SUBJECT TO CHANGE) + // will be assigned to the first provider registered. StrategyComparison EvaluationStrategy = "comparison" // StrategyCustom allows for using a custom Strategy implementation. If this is set you MUST use the WithCustomStrategy // option to set it From 71a9f29466df6644047419caaaa1611d89530d48 Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Fri, 25 Apr 2025 19:12:04 -0400 Subject: [PATCH 67/68] refactor: Simplify ProviderMap.Size() impl Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/providers/multi-provider/pkg/providers.go b/providers/multi-provider/pkg/providers.go index fa6f1a7ee..c40703981 100644 --- a/providers/multi-provider/pkg/providers.go +++ b/providers/multi-provider/pkg/providers.go @@ -78,11 +78,7 @@ func (m ProviderMap) AsNamedProviderSlice() []*strategies.NamedProvider { // Size The size of the map. This operates in O(n) time. func (m ProviderMap) Size() int { - count := 0 - for range m { - count += 1 - } - return count + return len(m.AsNamedProviderSlice()) } func (m ProviderMap) buildMetadata() of.Metadata { @@ -279,7 +275,7 @@ func (mp *MultiProvider) Shutdown() { wg.Wait() } -// EventChannel the channel emits are emitted on +// EventChannel the channel events are emitted on (Not Yet Implemented) func (mp *MultiProvider) EventChannel() <-chan of.Event { return mp.events } From 16a541cb997add762f4788e7f85f54b7eca36cca Mon Sep 17 00:00:00 2001 From: Jordan Blacker Date: Fri, 25 Apr 2025 19:24:31 -0400 Subject: [PATCH 68/68] fix: Correct one last broken test post downgrade Signed-off-by: Jordan Blacker --- providers/multi-provider/pkg/providers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/multi-provider/pkg/providers_test.go b/providers/multi-provider/pkg/providers_test.go index cc3f77c38..d4efdf02a 100644 --- a/providers/multi-provider/pkg/providers_test.go +++ b/providers/multi-provider/pkg/providers_test.go @@ -123,7 +123,7 @@ func TestMultiProvider_MetaData(t *testing.T) { metadata := mp.Metadata() require.NotZero(t, metadata) - assert.Equal(t, "MultiProvider {provider1: NoopProvider, provider2: MockProvider}", metadata.Name) + assert.Equal(t, "MultiProvider {provider1: InMemoryProvider, provider2: MockProvider}", metadata.Name) } func TestMultiProvider_Init(t *testing.T) {