diff --git a/e2e/evaluation_fuzz_test.go b/e2e/evaluation_fuzz_test.go index a696ccd3..73fe4994 100644 --- a/e2e/evaluation_fuzz_test.go +++ b/e2e/evaluation_fuzz_test.go @@ -2,23 +2,22 @@ package e2e_test import ( "context" - "strings" - "testing" - "github.com/open-feature/go-sdk/openfeature" "github.com/open-feature/go-sdk/openfeature/memprovider" + "strings" + "testing" ) func setupFuzzClient(f *testing.F) *openfeature.Client { f.Helper() memoryProvider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{}) - err := openfeature.SetProvider(memoryProvider) + err := openfeature.SetNamedProviderAndWait(f.Name(), memoryProvider) if err != nil { f.Errorf("error setting up provider %v", err) } - return openfeature.NewClient("fuzzing") + return openfeature.GetApiInstance().GetNamedClient(f.Name()).(*openfeature.Client) } func FuzzBooleanEvaluation(f *testing.F) { diff --git a/e2e/evaluation_test.go b/e2e/evaluation_test.go index 3563cbb6..076c56a4 100644 --- a/e2e/evaluation_test.go +++ b/e2e/evaluation_test.go @@ -12,8 +12,6 @@ import ( "github.com/open-feature/go-sdk/openfeature/memprovider" ) -var client = openfeature.NewClient("evaluation tests") - // ctxStorageKey is the key used to pass test data across context.Context type ctxStorageKey struct{} @@ -95,7 +93,7 @@ func initializeEvaluationScenario(ctx *godog.ScenarioContext) { func aProviderIsRegisteredWithCacheDisabled(ctx context.Context) error { memoryProvider := memprovider.NewInMemoryProvider(memoryFlags) - err := openfeature.SetProvider(memoryProvider) + err := openfeature.SetNamedProvider("evaluation-test", memoryProvider) if err != nil { return err } @@ -111,7 +109,7 @@ func aBooleanFlagWithKeyIsEvaluatedWithDefaultValue( return ctx, errors.New("default value must be of type bool") } - got, err := client.BooleanValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").BooleanValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -140,7 +138,7 @@ func theResolvedBooleanValueShouldBe(ctx context.Context, expectedValueStr strin func aStringFlagWithKeyIsEvaluatedWithDefaultValue( ctx context.Context, flagKey, defaultValue string, ) (context.Context, error) { - got, err := client.StringValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").StringValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -164,7 +162,7 @@ func theResolvedStringValueShouldBe(ctx context.Context, expectedValue string) e func anIntegerFlagWithKeyIsEvaluatedWithDefaultValue( ctx context.Context, flagKey string, defaultValue int64, ) (context.Context, error) { - got, err := client.IntValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").IntValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -188,7 +186,7 @@ func theResolvedIntegerValueShouldBe(ctx context.Context, expectedValue int64) e func aFloatFlagWithKeyIsEvaluatedWithDefaultValue( ctx context.Context, flagKey string, defaultValue float64, ) (context.Context, error) { - got, err := client.FloatValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").FloatValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -210,7 +208,7 @@ func theResolvedFloatValueShouldBe(ctx context.Context, expectedValue float64) e } func anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue(ctx context.Context, flagKey string) (context.Context, error) { - got, err := client.ObjectValue(ctx, flagKey, nil, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").ObjectValue(ctx, flagKey, nil, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -272,7 +270,7 @@ func aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( return ctx, errors.New("default value must be of type bool") } - got, err := client.BooleanValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").BooleanValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -316,7 +314,7 @@ func theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldB func aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( ctx context.Context, flagKey, defaultValue string, ) (context.Context, error) { - got, err := client.StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -355,7 +353,7 @@ func theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe func anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( ctx context.Context, flagKey string, defaultValue int64, ) (context.Context, error) { - got, err := client.IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -394,7 +392,7 @@ func theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldB func aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( ctx context.Context, flagKey string, defaultValue float64, ) (context.Context, error) { - got, err := client.FloatValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").FloatValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -433,7 +431,7 @@ func theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe( func anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue( ctx context.Context, flagKey string, ) (context.Context, error) { - got, err := client.ObjectValueDetails(ctx, flagKey, nil, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").ObjectValueDetails(ctx, flagKey, nil, openfeature.EvaluationContext{}) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -540,7 +538,7 @@ func aFlagWithKeyIsEvaluatedWithDefaultValue( return ctx, errors.New("no contextAwareEvaluationData found") } - got, err := client.StringValue(ctx, flagKey, defaultValue, ctxAwareEvalData.evaluationContext) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").StringValue(ctx, flagKey, defaultValue, ctxAwareEvalData.evaluationContext) if err != nil { return ctx, fmt.Errorf("openfeature client: %w", err) } @@ -570,7 +568,7 @@ func theResolvedFlagValueIsWhenTheContextIsEmpty(ctx context.Context, expectedRe return errors.New("no contextAwareEvaluationData found") } - got, err := client.StringValue( + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").StringValue( ctx, ctxAwareEvalData.flagKey, ctxAwareEvalData.defaultValue, openfeature.EvaluationContext{}, ) if err != nil { @@ -587,7 +585,7 @@ func theResolvedFlagValueIsWhenTheContextIsEmpty(ctx context.Context, expectedRe func aNonexistentStringFlagWithKeyIsEvaluatedWithDetailsAndADefaultValue( ctx context.Context, flagKey, defaultValue string, ) (context.Context, error) { - got, err := client.StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, stringFlagNotFoundData{ evalDetails: got, @@ -644,7 +642,7 @@ func theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMissingFlagWith func aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue( ctx context.Context, flagKey string, defaultValue int64, ) (context.Context, error) { - got, err := client.IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + got, err := openfeature.GetApiInstance().GetNamedClient("evaluation-test").IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) return context.WithValue(ctx, ctxStorageKey{}, typeErrorData{ evalDetails: got, diff --git a/go.sum b/go.sum index 00fd24b9..b3275c49 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= -github.com/cucumber/godog v0.14.0 h1:h/K4t7XBxsFBF+UJEahNqJ1/2VHVepRXCSq3WWWnehs= -github.com/cucumber/godog v0.14.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= github.com/cucumber/godog v0.14.1 h1:HGZhcOyyfaKclHjJ+r/q93iaTJZLKYW6Tv3HkmUE6+M= github.com/cucumber/godog v0.14.1/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces= github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= @@ -11,12 +9,9 @@ github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -31,7 +26,6 @@ github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= @@ -57,12 +51,6 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo= -golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= -golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= 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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -79,14 +67,6 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/openfeature/client.go b/openfeature/client.go index d44bc436..101c6fc8 100644 --- a/openfeature/client.go +++ b/openfeature/client.go @@ -41,6 +41,7 @@ type Client struct { metadata ClientMetadata hooks []Hook evaluationContext EvaluationContext + domain string mx sync.RWMutex } @@ -56,6 +57,7 @@ func NewClient(name string) *Client { func newClient(name string, apiRef evaluationImpl, eventRef clientEvent) *Client { return &Client{ + domain: name, api: apiRef, clientEventing: eventRef, metadata: ClientMetadata{name: name}, @@ -64,6 +66,11 @@ func newClient(name string, apiRef evaluationImpl, eventRef clientEvent) *Client } } +// State returns the state of the associated provider +func (c *Client) State() State { + return c.clientEventing.State(c.domain) +} + // Deprecated // WithLogger sets the logger of the client func (c *Client) WithLogger(l logr.Logger) *Client { @@ -677,6 +684,18 @@ func (c *Client) evaluate( c.finallyHooks(ctx, hookCtx, providerInvocationClientApiHooks, options) }() + // short circuit if provider is in NOT READY state + if c.State() == NotReadyState { + c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, ProviderNotReadyError, options) + return evalDetails, ProviderNotReadyError + } + + // short circuit if provider is in FATAL state + if c.State() == FatalState { + c.errorHooks(ctx, hookCtx, providerInvocationClientApiHooks, ProviderFatalError, options) + return evalDetails, ProviderFatalError + } + evalCtx, err = c.beforeHooks(ctx, hookCtx, apiClientInvocationProviderHooks, evalCtx, options) hookCtx.evaluationContext = evalCtx if err != nil { diff --git a/openfeature/client_example_test.go b/openfeature/client_example_test.go index f72290e0..a874c97f 100644 --- a/openfeature/client_example_test.go +++ b/openfeature/client_example_test.go @@ -16,6 +16,9 @@ func ExampleNewClient() { } func ExampleClient_BooleanValue() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } client := openfeature.NewClient("example-client") value, err := client.BooleanValue( context.Background(), "test-flag", true, openfeature.EvaluationContext{}, @@ -29,6 +32,9 @@ func ExampleClient_BooleanValue() { } func ExampleClient_StringValue() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } client := openfeature.NewClient("example-client") value, err := client.StringValue( context.Background(), "test-flag", "openfeature", openfeature.EvaluationContext{}, @@ -42,6 +48,9 @@ func ExampleClient_StringValue() { } func ExampleClient_FloatValue() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } client := openfeature.NewClient("example-client") value, err := client.FloatValue( context.Background(), "test-flag", 0.55, openfeature.EvaluationContext{}, @@ -55,6 +64,9 @@ func ExampleClient_FloatValue() { } func ExampleClient_IntValue() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } client := openfeature.NewClient("example-client") value, err := client.IntValue( context.Background(), "test-flag", 3, openfeature.EvaluationContext{}, @@ -68,6 +80,9 @@ func ExampleClient_IntValue() { } func ExampleClient_ObjectValue() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } client := openfeature.NewClient("example-client") value, err := client.ObjectValue( context.Background(), "test-flag", map[string]string{"foo": "bar"}, openfeature.EvaluationContext{}, @@ -82,6 +97,9 @@ func ExampleClient_ObjectValue() { } func ExampleClient_Boolean() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } ctx := context.Background() client := openfeature.NewClient("example-client") @@ -95,6 +113,9 @@ func ExampleClient_Boolean() { } func ExampleClient_String() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } ctx := context.Background() client := openfeature.NewClient("example-client") @@ -104,6 +125,9 @@ func ExampleClient_String() { } func ExampleClient_Float() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } ctx := context.Background() client := openfeature.NewClient("example-client") @@ -113,6 +137,9 @@ func ExampleClient_Float() { } func ExampleClient_Int() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } ctx := context.Background() client := openfeature.NewClient("example-client") @@ -122,6 +149,9 @@ func ExampleClient_Int() { } func ExampleClient_Object() { + if err := openfeature.SetNamedProviderAndWait("example-client", openfeature.NoopProvider{}); err != nil { + log.Fatalf("error setting up provider %v", err) + } ctx := context.Background() client := openfeature.NewClient("example-client") diff --git a/openfeature/client_test.go b/openfeature/client_test.go index 1d7e4a91..329bfc92 100644 --- a/openfeature/client_test.go +++ b/openfeature/client_test.go @@ -2,6 +2,7 @@ package openfeature import ( "context" + "errors" "reflect" "testing" "time" @@ -108,7 +109,6 @@ func TestRequirement_1_4_1(t *testing.T) { // by the configured `provider`, if the field is set. func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") const ( booleanValue = true stringValue = "str" @@ -134,7 +134,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait("test-client", mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -151,7 +151,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { }, }) - evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, false, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").BooleanValueDetails(context.Background(), flagKey, false, EvaluationContext{}) if err != nil { t.Error(err) } @@ -170,7 +170,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { }, }) - evDetails, err := client.StringValueDetails(context.Background(), flagKey, "", EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").StringValueDetails(context.Background(), flagKey, "", EvaluationContext{}) if err != nil { t.Error(err) } @@ -195,7 +195,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { }, }) - evDetails, err := client.FloatValueDetails(context.Background(), flagKey, 0, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").FloatValueDetails(context.Background(), flagKey, 0, EvaluationContext{}) if err != nil { t.Error(err) } @@ -220,7 +220,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { }, }) - evDetails, err := client.IntValueDetails(context.Background(), flagKey, 0, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").IntValueDetails(context.Background(), flagKey, 0, EvaluationContext{}) if err != nil { t.Error(err) } @@ -245,7 +245,7 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { }, }) - evDetails, err := client.ObjectValueDetails(context.Background(), flagKey, nil, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").ObjectValueDetails(context.Background(), flagKey, nil, EvaluationContext{}) if err != nil { t.Error(err) } @@ -267,12 +267,14 @@ func TestRequirement_1_4_2__1_4_5__1_4_6(t *testing.T) { // argument passed to the detailed flag evaluation method. func TestRequirement_1_4_4(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") + if err := SetNamedProviderAndWait("test-client", NoopProvider{}); err != nil { + t.Errorf("error setting up provider %v", err) + } flagKey := "foo" t.Run("BooleanValueDetails", func(t *testing.T) { - evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, true, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").BooleanValueDetails(context.Background(), flagKey, true, EvaluationContext{}) if err != nil { t.Error(err) } @@ -285,7 +287,7 @@ func TestRequirement_1_4_4(t *testing.T) { }) t.Run("StringValueDetails", func(t *testing.T) { - evDetails, err := client.StringValueDetails(context.Background(), flagKey, "", EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").StringValueDetails(context.Background(), flagKey, "", EvaluationContext{}) if err != nil { t.Error(err) } @@ -298,7 +300,7 @@ func TestRequirement_1_4_4(t *testing.T) { }) t.Run("FloatValueDetails", func(t *testing.T) { - evDetails, err := client.FloatValueDetails(context.Background(), flagKey, 1, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").FloatValueDetails(context.Background(), flagKey, 1, EvaluationContext{}) if err != nil { t.Error(err) } @@ -311,7 +313,7 @@ func TestRequirement_1_4_4(t *testing.T) { }) t.Run("IntValueDetails", func(t *testing.T) { - evDetails, err := client.IntValueDetails(context.Background(), flagKey, 1, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").IntValueDetails(context.Background(), flagKey, 1, EvaluationContext{}) if err != nil { t.Error(err) } @@ -324,7 +326,7 @@ func TestRequirement_1_4_4(t *testing.T) { }) t.Run("ObjectValueDetails", func(t *testing.T) { - evDetails, err := client.ObjectValueDetails(context.Background(), flagKey, 1, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient("test-client").ObjectValueDetails(context.Background(), flagKey, 1, EvaluationContext{}) if err != nil { t.Error(err) } @@ -341,7 +343,6 @@ func TestRequirement_1_4_4(t *testing.T) { // `error code` field MUST contain an `error code`. func TestRequirement_1_4_7(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) @@ -355,12 +356,12 @@ func TestRequirement_1_4_7(t *testing.T) { }, }) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - res, err := client.evaluate( + res, err := GetApiInstance().GetNamedClient(t.Name()).(*Client).evaluate( context.Background(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { @@ -377,7 +378,6 @@ func TestRequirement_1_4_7(t *testing.T) { // in the `evaluation details` SHOULD indicate an error. func TestRequirement_1_4_8(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") ctrl := gomock.NewController(t) mockProvider := NewMockFeatureProvider(ctrl) @@ -390,12 +390,12 @@ func TestRequirement_1_4_8(t *testing.T) { ResolutionError: NewGeneralResolutionError("test"), }, }) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - res, err := client.evaluate( + res, err := GetApiInstance().GetNamedClient(t.Name()).(*Client).evaluate( context.Background(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { @@ -425,7 +425,6 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("Boolean", func(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") mockProvider := NewMockFeatureProvider(ctrl) defaultValue := true mockProvider.EXPECT().Metadata().AnyTimes() @@ -438,12 +437,12 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - value, err := client.BooleanValue(context.Background(), flagKey, defaultValue, evalCtx) + value, err := GetApiInstance().GetNamedClient(t.Name()).BooleanValue(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected BooleanValue to return an error, got nil") } @@ -452,7 +451,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Errorf("expected default value from BooleanValue, got %v", value) } - valueDetails, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, evalCtx) + valueDetails, err := GetApiInstance().GetNamedClient(t.Name()).BooleanValueDetails(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected BooleanValueDetails to return an error, got nil") } @@ -465,7 +464,6 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("String", func(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") mockProvider := NewMockFeatureProvider(ctrl) defaultValue := "default" mockProvider.EXPECT().Metadata().AnyTimes() @@ -478,12 +476,12 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - value, err := client.StringValue(context.Background(), flagKey, defaultValue, evalCtx) + value, err := GetApiInstance().GetNamedClient(t.Name()).StringValue(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected StringValue to return an error, got nil") } @@ -492,7 +490,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Errorf("expected default value from StringValue, got %v", value) } - valueDetails, err := client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx) + valueDetails, err := GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected StringValueDetails to return an error, got nil") } @@ -505,7 +503,6 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("Float", func(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") mockProvider := NewMockFeatureProvider(ctrl) defaultValue := 3.14159 mockProvider.EXPECT().Metadata().AnyTimes() @@ -518,12 +515,12 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - value, err := client.FloatValue(context.Background(), flagKey, defaultValue, evalCtx) + value, err := GetApiInstance().GetNamedClient(t.Name()).FloatValue(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected FloatValue to return an error, got nil") } @@ -532,7 +529,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Errorf("expected default value from FloatValue, got %v", value) } - valueDetails, err := client.FloatValueDetails(context.Background(), flagKey, defaultValue, evalCtx) + valueDetails, err := GetApiInstance().GetNamedClient(t.Name()).FloatValueDetails(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected FloatValueDetails to return an error, got nil") } @@ -545,7 +542,6 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("Int", func(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") mockProvider := NewMockFeatureProvider(ctrl) var defaultValue int64 = 3 mockProvider.EXPECT().Metadata().AnyTimes() @@ -558,12 +554,12 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - value, err := client.IntValue(context.Background(), flagKey, defaultValue, evalCtx) + value, err := GetApiInstance().GetNamedClient(t.Name()).IntValue(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected IntValue to return an error, got nil") } @@ -572,7 +568,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Errorf("expected default value from IntValue, got %v", value) } - valueDetails, err := client.IntValueDetails(context.Background(), flagKey, defaultValue, evalCtx) + valueDetails, err := GetApiInstance().GetNamedClient(t.Name()).IntValueDetails(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected FloatValueDetails to return an error, got nil") } @@ -585,7 +581,6 @@ func TestRequirement_1_4_9(t *testing.T) { t.Run("Object", func(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") mockProvider := NewMockFeatureProvider(ctrl) type obj struct { foo string @@ -600,12 +595,12 @@ func TestRequirement_1_4_9(t *testing.T) { }, }).Times(2) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - value, err := client.ObjectValue(context.Background(), flagKey, defaultValue, evalCtx) + value, err := GetApiInstance().GetNamedClient(t.Name()).ObjectValue(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected ObjectValue to return an error, got nil") } @@ -614,7 +609,7 @@ func TestRequirement_1_4_9(t *testing.T) { t.Errorf("expected default value from ObjectValue, got %v", value) } - valueDetails, err := client.ObjectValueDetails(context.Background(), flagKey, defaultValue, evalCtx) + valueDetails, err := GetApiInstance().GetNamedClient(t.Name()).ObjectValueDetails(context.Background(), flagKey, defaultValue, evalCtx) if err == nil { t.Error("expected ObjectValueDetails to return an error, got nil") } @@ -645,7 +640,7 @@ func TestRequirement_1_4_12(t *testing.T) { mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -658,8 +653,7 @@ func TestRequirement_1_4_12(t *testing.T) { }, }) - client := NewClient("test") - evalDetails, err := client.evaluate( + evalDetails, err := GetApiInstance().GetNamedClient(t.Name()).(*Client).evaluate( context.Background(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { @@ -687,7 +681,6 @@ func TestRequirement_1_4_13(t *testing.T) { t.Run("No Metadata", func(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") mockProvider := NewMockFeatureProvider(ctrl) defaultValue := true mockProvider.EXPECT().Metadata().AnyTimes() @@ -700,12 +693,12 @@ func TestRequirement_1_4_13(t *testing.T) { }, }).Times(1) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient(t.Name()).BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{}) if err != nil { t.Error(err) } @@ -720,7 +713,6 @@ func TestRequirement_1_4_13(t *testing.T) { t.Run("Metadata present", func(t *testing.T) { defer t.Cleanup(initSingleton) - client := NewClient("test-client") mockProvider := NewMockFeatureProvider(ctrl) defaultValue := true metadata := FlagMetadata{ @@ -736,12 +728,12 @@ func TestRequirement_1_4_13(t *testing.T) { }, }).Times(1) - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - evDetails, err := client.BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{}) + evDetails, err := GetApiInstance().GetNamedClient(t.Name()).BooleanValueDetails(context.Background(), flagKey, defaultValue, EvaluationContext{}) if err != nil { t.Error(err) } @@ -851,14 +843,14 @@ func TestBeforeHookNilContext(t *testing.T) { mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } hookNilContext := UnimplementedHook{} - client := NewClient("test") + client := GetApiInstance().GetNamedClient(t.Name()) attributes := map[string]interface{}{"should": "persist"} evalCtx := EvaluationContext{attributes: attributes} mockProvider.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), attributes) @@ -881,7 +873,7 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) { mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -894,8 +886,8 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) { }, }) - client := NewClient("test") - evalDetails, err := client.evaluate( + client := GetApiInstance().GetNamedClient(t.Name()) + evalDetails, err := client.(*Client).evaluate( context.Background(), "foo", Boolean, true, EvaluationContext{}, EvaluationOptions{}, ) if err == nil { @@ -934,7 +926,7 @@ func TestSwitchingProvidersMidEvaluationCausesNoImpactToEvaluation(t *testing.T) return nil, nil }) - err := SetProvider(mockProvider1) + err := SetNamedProviderAndWait(t.Name(), mockProvider1) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -944,7 +936,7 @@ func TestSwitchingProvidersMidEvaluationCausesNoImpactToEvaluation(t *testing.T) mockProvider1Hook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockProvider1Hook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) - client := NewClient("test") + client := GetApiInstance().GetNamedClient(t.Name()) _, err = client.BooleanValue(context.Background(), "foo", true, EvaluationContext{}) if err != nil { t.Error(err) @@ -963,7 +955,7 @@ func TestObjectEvaluationShouldSupportNilValue(t *testing.T) { mockProvider.EXPECT().Metadata().AnyTimes() mockProvider.EXPECT().Hooks().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -977,7 +969,7 @@ func TestObjectEvaluationShouldSupportNilValue(t *testing.T) { }, }) - client := NewClient("test") + client := GetApiInstance().GetNamedClient(t.Name()) evDetails, err := client.ObjectValueDetails(context.Background(), "foo", nil, EvaluationContext{}) if err != nil { t.Errorf("should not return an error: %s", err.Error()) @@ -1112,3 +1104,259 @@ func TestFlagMetadataAccessors(t *testing.T) { } }) } + +// The client MUST define a provider status accessor which indicates the readiness of the associated provider. +// with possible values NOT_READY, READY, STALE, ERROR, or FATAL. +func TestRequirement_1_7_1(t *testing.T) { + + client := NewClient("test-client") + + type requirements interface { + State() State + } + + var clientI interface{} = client + if _, ok := clientI.(requirements); !ok { + t.Fatal("client des not define a status accessor which indicates the readiness of the associated provider") + } + + TestRequirement_5_3_5(t) +} + +// The client's provider status accessor MUST indicate READY if the initialize function of the associated provider +// terminates normally. +func TestRequirement_1_7_2(t *testing.T) { + defer t.Cleanup(initSingleton) + + if GetApiInstance().GetNamedClient(t.Name()).State() != NotReadyState { + t.Fatalf("expected client to report NOT READY state") + } + + provider := struct { + FeatureProvider + EventHandler + }{ + NoopProvider{}, + &ProviderEventing{ + c: make(chan Event, 1), + }, + } + + if err := SetNamedProviderAndWait(t.Name(), provider); err != nil { + t.Fatalf("failed to set up provider: %v", err) + } + + if GetApiInstance().GetNamedClient(t.Name()).State() != ReadyState { + t.Fatalf("expected client to report READY state") + } +} + +// The client's provider status accessor MUST indicate ERROR if the initialize function of the associated provider +// terminates abnormally +func TestRequirement_1_7_3(t *testing.T) { + defer t.Cleanup(initSingleton) + provider := struct { + FeatureProvider + StateHandler + EventHandler + }{ + NoopProvider{}, + &stateHandlerForTests{ + initF: func(e EvaluationContext) error { + return errors.New("whoops... error from initialization") + }, + }, + &ProviderEventing{}, + } + + _ = SetNamedProviderAndWait(t.Name(), provider) + if GetApiInstance().GetNamedClient(t.Name()).State() != ErrorState { + t.Fatalf("expected client to report ERROR state") + } + +} + +// The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider +// terminates abnormally and indicates error code PROVIDER_FATAL. +func TestRequirement_1_7_4(t *testing.T) { + defer t.Cleanup(initSingleton) + provider := struct { + FeatureProvider + StateHandler + EventHandler + }{ + NoopProvider{}, + &stateHandlerForTests{ + initF: func(e EvaluationContext) error { + return errors.New("whoops... error from initialization") + }, + }, + &ProviderEventing{}, + } + + _ = SetNamedProviderAndWait(t.Name(), provider) + + if GetApiInstance().GetNamedClient(t.Name()).State() != ErrorState { + t.Fatalf("expected client to report ERROR state") + } + +} + +// The client's provider status accessor MUST indicate FATAL if the initialize function of the associated provider +// terminates abnormally and indicates error code PROVIDER_FATAL. +func TestRequirement_1_7_5(t *testing.T) { + defer t.Cleanup(initSingleton) + provider := struct { + FeatureProvider + StateHandler + EventHandler + }{ + NoopProvider{}, + &stateHandlerForTests{ + initF: func(e EvaluationContext) error { + return &ProviderInitError{ErrorCode: ProviderFatalCode} + }, + }, + &ProviderEventing{}, + } + + _ = SetNamedProviderAndWait(t.Name(), provider) + + if GetApiInstance().GetNamedClient(t.Name()).State() != FatalState { + t.Fatalf("expected client to report ERROR state") + } + +} + +// The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider +// is in NOT_READY. +func TestRequirement_1_7_6(t *testing.T) { + defer t.Cleanup(initSingleton) + + ctrl := gomock.NewController(t) + mockHook := NewMockHook(ctrl) + mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), ProviderNotReadyError, gomock.Any()) + mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) + + client := GetApiInstance().GetNamedClient(t.Name()) + client.AddHooks(mockHook) + + if client.State() != NotReadyState { + t.Fatalf("expected client to report NOT READY state") + } + + defaultVal := true + res, err := client.BooleanValue(context.Background(), "a-flag", defaultVal, EvaluationContext{}) + if err == nil { + t.Fatalf("expected client to report an error") + } + + if res != defaultVal { + t.Fatalf("expected resolved boolean value to default to %t, got %t", defaultVal, res) + } + +} + +// The client MUST default, run error hooks, and indicate an error if flag resolution is attempted while the provider +// is in FATAL. +func TestRequirement_1_7_7(t *testing.T) { + defer t.Cleanup(initSingleton) + provider := struct { + FeatureProvider + StateHandler + EventHandler + }{ + NoopProvider{}, + &stateHandlerForTests{ + initF: func(e EvaluationContext) error { + return &ProviderInitError{ErrorCode: ProviderFatalCode} + }, + }, + &ProviderEventing{}, + } + + _ = SetNamedProviderAndWait(t.Name(), provider) + + ctrl := gomock.NewController(t) + mockHook := NewMockHook(ctrl) + mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), ProviderFatalError, gomock.Any()) + mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) + + client := GetApiInstance().GetNamedClient(t.Name()) + client.AddHooks(mockHook) + + if client.State() != FatalState { + t.Fatalf("expected client to report FATAL state") + } + + defaultVal := true + res, err := client.BooleanValue(context.Background(), "a-flag", defaultVal, EvaluationContext{}) + if err == nil { + t.Fatalf("expected client to report an error") + } + + if res != defaultVal { + t.Fatalf("expected resolved boolean value to default to %t, got %t", defaultVal, res) + } +} + +// Implementations SHOULD propagate the error code returned from any provider lifecycle methods. +func TestRequirement_1_7_8(t *testing.T) { + + t.Skip("Test not yet implemented") +} + +// PROVIDER_ERROR events SHOULD populate the provider event details's error code field. +func TestRequirement_5_1_5(t *testing.T) { + t.Skip("Test not yet implemented") +} + +// If the provider emits an event, the value of the client's provider status MUST be updated accordingly. +func TestRequirement_5_3_5(t *testing.T) { + defer t.Cleanup(initSingleton) + + eventually(t, func() bool { + return GetApiInstance().GetClient().State() == NotReadyState + }, time.Second, 100*time.Millisecond, "expected client to report NOT READY state") + + eventing := &ProviderEventing{ + c: make(chan Event, 1), + } + + provider := struct { + FeatureProvider + EventHandler + }{ + NoopProvider{}, + eventing, + } + + if err := SetNamedProviderAndWait(t.Name(), provider); err != nil { + t.Fatalf("failed to set up provider: %v", err) + } + + eventually(t, func() bool { + return GetApiInstance().GetNamedClient(t.Name()).State() == ReadyState + }, time.Second, 100*time.Millisecond, "expected client to report READY state") + + eventing.Invoke(Event{EventType: ProviderStale}) + eventually(t, func() bool { + return GetApiInstance().GetNamedClient(t.Name()).State() == StaleState + }, time.Second, 100*time.Millisecond, "expected client to report STALE state") + + eventing.Invoke(Event{EventType: ProviderError}) + eventually(t, func() bool { + return GetApiInstance().GetNamedClient(t.Name()).State() == ErrorState + }, time.Second, 100*time.Millisecond, "expected client to report ERROR state") + + eventing.Invoke(Event{EventType: ProviderReady}) + eventually(t, func() bool { + return GetApiInstance().GetNamedClient(t.Name()).State() == ReadyState + }, time.Second, 100*time.Millisecond, "expected client to report READY state") + + eventing.Invoke(Event{EventType: ProviderError, ProviderEventDetails: ProviderEventDetails{ErrorCode: ProviderFatalCode}}) + eventually(t, func() bool { + return GetApiInstance().GetNamedClient(t.Name()).State() == FatalState + }, time.Second, 100*time.Millisecond, "expected client to report FATAL state") + +} diff --git a/openfeature/error.go b/openfeature/error.go new file mode 100644 index 00000000..e1cbba20 --- /dev/null +++ b/openfeature/error.go @@ -0,0 +1 @@ +package openfeature diff --git a/openfeature/evaluation_context_test.go b/openfeature/evaluation_context_test.go index 1e1c87b2..d7ada99e 100644 --- a/openfeature/evaluation_context_test.go +++ b/openfeature/evaluation_context_test.go @@ -113,12 +113,12 @@ func TestRequirement_3_2_2(t *testing.T) { mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } - client := NewClient("test") + client := GetApiInstance().GetNamedClient(t.Name()) clientEvalCtx := EvaluationContext{ targetingKey: "Client", attributes: map[string]interface{}{ diff --git a/openfeature/event_executor.go b/openfeature/event_executor.go index 388cfb42..7b33ee39 100644 --- a/openfeature/event_executor.go +++ b/openfeature/event_executor.go @@ -24,8 +24,11 @@ type eventingImpl interface { type clientEvent interface { AddClientHandler(clientName string, t EventType, c EventCallback) RemoveClientHandler(name string, t EventType, c EventCallback) + State(domain string) State } +const defaultClient = "" + // event executor is a registry to connect API and Client event handlers to Providers // eventExecutor handles events emitted from FeatureProvider. It follows a pub-sub model based on channels. @@ -33,6 +36,7 @@ type clientEvent interface { // feature provider as well as from API(ex:- for initialization events). // Usage of channels help with concurrency and adhere to the principal of sharing memory by communication. type eventExecutor struct { + states sync.Map defaultProviderReference providerReference namedProviderReference map[string]providerReference activeSubscriptions []providerReference @@ -45,6 +49,7 @@ type eventExecutor struct { func newEventExecutor() *eventExecutor { executor := eventExecutor{ + states: sync.Map{}, namedProviderReference: map[string]providerReference{}, activeSubscriptions: []providerReference{}, apiRegistry: map[EventType][]EventCallback{}, @@ -97,7 +102,7 @@ func (e *eventExecutor) AddHandler(t EventType, c EventCallback) { e.apiRegistry[t] = append(e.apiRegistry[t], c) } - e.emitOnRegistration(e.defaultProviderReference, t, c) + e.emitOnRegistration(defaultClient, e.defaultProviderReference, t, c) } // RemoveHandler removes an API(global) level handler @@ -144,7 +149,7 @@ func (e *eventExecutor) AddClientHandler(clientDomain string, t EventType, c Eve reference = e.defaultProviderReference } - e.emitOnRegistration(reference, t, c) + e.emitOnRegistration(clientDomain, reference, t, c) } // RemoveClientHandler removes a client level handler @@ -183,19 +188,12 @@ func (e *eventExecutor) GetClientRegistry(client string) scopedCallback { // emitOnRegistration fulfils the spec requirement to fire events if the // event type and the state of the associated provider are compatible. -func (e *eventExecutor) emitOnRegistration( - providerReference providerReference, - eventType EventType, - callback EventCallback, -) { - s, ok := (providerReference.featureProvider).(StateHandler) +func (e *eventExecutor) emitOnRegistration(clientDomain string, providerReference providerReference, eventType EventType, callback EventCallback) { + state, ok := e.states.Load(clientDomain) if !ok { - // not a state handler, hence ignore state emitting return } - state := s.Status() - var message string if state == ReadyState && eventType == ProviderReady { message = "provider is in ready state" @@ -215,6 +213,14 @@ func (e *eventExecutor) emitOnRegistration( } } +func (e *eventExecutor) State(domain string) State { + s, ok := e.states.Load(domain) + if !ok { + return NotReadyState + } + return s.(State) +} + // registerDefaultProvider registers the default FeatureProvider and remove the old default provider if available func (e *eventExecutor) registerDefaultProvider(provider FeatureProvider) error { e.mu.Lock() @@ -340,6 +346,7 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { continue } + e.states.Store(name, stateFromEvent(event)) for _, c := range e.scopedRegistry[name].callbacks[event.EventType] { e.executeHandler(*c, event) } @@ -349,7 +356,9 @@ func (e *eventExecutor) triggerEvent(event Event, handler FeatureProvider) { return } - // handling the default provider - invoke default provider bound (no provider associated) handlers by filtering + // handling the default provider + e.states.Store(defaultClient, stateFromEvent(event)) + // invoke default provider bound (no provider associated) handlers by filtering for clientName, registry := range e.scopedRegistry { if _, ok := e.namedProviderReference[clientName]; ok { // association exist, skip and check next diff --git a/openfeature/event_executor_test.go b/openfeature/event_executor_test.go index 83f85b8d..1e55074e 100644 --- a/openfeature/event_executor_test.go +++ b/openfeature/event_executor_test.go @@ -147,7 +147,7 @@ func TestEventHandler_Eventing(t *testing.T) { } client := NewClient(associatedName) - client.AddHandler(ProviderReady, &callBack) + client.AddHandler(ProviderError, &callBack) fCh := []string{"flagA"} meta := map[string]interface{}{ @@ -157,7 +157,7 @@ func TestEventHandler_Eventing(t *testing.T) { // trigger event from provider implementation eventingImpl.Invoke(Event{ ProviderName: eventingProvider.Metadata().Name, - EventType: ProviderReady, + EventType: ProviderError, ProviderEventDetails: ProviderEventDetails{ Message: "ReadyMessage", FlagChanges: fCh, @@ -235,7 +235,7 @@ func TestEventHandler_clientAssociation(t *testing.T) { rsp <- details } - event := ProviderReady + event := ProviderError client := NewClient("someClient") client.AddHandler(event, &callBack) @@ -333,15 +333,16 @@ func TestEventHandler_InitOfProvider(t *testing.T) { t.Run("for default provider in global handler scope", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, + eventingImpl, } // callback @@ -367,15 +368,16 @@ func TestEventHandler_InitOfProvider(t *testing.T) { t.Run("for default provider with unassociated client handler", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, + eventingImpl, } // callback @@ -402,15 +404,16 @@ func TestEventHandler_InitOfProvider(t *testing.T) { t.Run("for named provider in client scope", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, + eventingImpl, } // callback @@ -438,15 +441,17 @@ func TestEventHandler_InitOfProvider(t *testing.T) { t.Run("no callback for named provider with no associations", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderConfigChange}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, + eventingImpl, } // callback @@ -478,18 +483,17 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { t.Run("for default provider in global scope", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderError}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - initF: func(e EvaluationContext) error { - return errors.New("initialization failed") - }, - State: NotReadyState, - }, + eventingImpl, } // callback @@ -515,18 +519,17 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { t.Run("for default provider with unassociated client handler", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderError}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - initF: func(e EvaluationContext) error { - return errors.New("initialization failed") - }, - State: NotReadyState, - }, + eventingImpl, } // callback @@ -554,18 +557,17 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { t.Run("for named provider in client scope", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderError}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - initF: func(e EvaluationContext) error { - return errors.New("initialization failed") - }, - State: NotReadyState, - }, + eventingImpl, } // callback @@ -593,18 +595,17 @@ func TestEventHandler_InitOfProviderError(t *testing.T) { t.Run("no callback for named provider with no associations", func(t *testing.T) { defer t.Cleanup(initSingleton) - // provider + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderError}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - initF: func(e EvaluationContext) error { - return errors.New("initialization failed") - }, - State: NotReadyState, - }, + eventingImpl, } // callback @@ -636,16 +637,16 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { t.Run("for api level under default provider", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + readyEventingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: ReadyState, - }, - &ProviderEventing{}, + eventingImpl, } err := SetProvider(readyEventingProvider) @@ -671,16 +672,16 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { t.Run("for domain associated handler", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + readyEventingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: ReadyState, - }, - &ProviderEventing{}, + eventingImpl, } clientAssociation := "clientA" @@ -708,16 +709,17 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { t.Run("for unassociated handler from default", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderConfigChange}) + readyEventingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: ReadyState, - }, - &ProviderEventing{}, + eventingImpl, } err := SetProvider(readyEventingProvider) @@ -751,7 +753,6 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - State: NotReadyState, initF: func(e EvaluationContext) error { return errors.New("some error from initialization") }, @@ -790,7 +791,6 @@ func TestEventHandler_ProviderReadiness(t *testing.T) { }{ NoopProvider{}, &stateHandlerForTests{ - State: NotReadyState, initF: func(e EvaluationContext) error { return nil }, @@ -826,14 +826,16 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { t.Run("ready handler runs when provider ready", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: ReadyState, - }, + eventingImpl, } if err := SetProvider(provider); err != nil { @@ -858,14 +860,17 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { t.Run("error handler runs when provider error", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderError}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: ErrorState, - }, + eventingImpl, } if err := SetProvider(provider); err != nil { @@ -890,14 +895,17 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { t.Run("stale handler runs when provider stale", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderStale}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: StaleState, - }, + eventingImpl, } if err := SetProvider(provider); err != nil { @@ -922,14 +930,16 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { t.Run("non-ready handler does not run when provider ready", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: ReadyState, - }, + eventingImpl, } if err := SetProvider(provider); err != nil { @@ -956,14 +966,17 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { t.Run("non-error handler does not run when provider error", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderError}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: ErrorState, - }, + eventingImpl, } if err := SetProviderAndWait(provider); err != nil { @@ -976,9 +989,15 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { } AddHandler(ProviderReady, &callback) + <-rsp // ignore first READY event which gets emitted after registration AddHandler(ProviderStale, &callback) AddHandler(ProviderConfigChange, &callback) + // assert client transitioned to ERROR + eventually(t, func() bool { + return GetApiInstance().GetClient().State() == ErrorState + }, time.Second, time.Millisecond*100, "") + select { case <-rsp: t.Errorf("event must not emit for this handler") @@ -990,14 +1009,17 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { t.Run("non-stale handler does not run when provider stale", func(t *testing.T) { defer t.Cleanup(initSingleton) + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingImpl.Invoke(Event{EventType: ProviderStale}) + provider := struct { FeatureProvider - StateHandler + EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: StaleState, - }, + eventingImpl, } if err := SetProviderAndWait(provider); err != nil { @@ -1010,9 +1032,15 @@ func TestEventHandler_HandlersRunImmediately(t *testing.T) { } AddHandler(ProviderReady, &callback) + <-rsp // ignore first READY event which gets emitted after registration AddHandler(ProviderError, &callback) AddHandler(ProviderConfigChange, &callback) + // assert client transitioned to STALE + eventually(t, func() bool { + return GetApiInstance().GetClient().State() == StaleState + }, time.Second, time.Millisecond*100, "") + select { case <-rsp: t.Errorf("event must not emit for this handler") @@ -1159,16 +1187,17 @@ func TestEventHandler_multiSubs(t *testing.T) { func TestEventHandler_1ToNMapping(t *testing.T) { t.Run("provider eventing must be subscribed only once", func(t *testing.T) { + + eventingImpl := &ProviderEventing{ + c: make(chan Event, 1), + } + eventingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, - &ProviderEventing{}, + eventingImpl, } executor := newEventExecutor() @@ -1199,17 +1228,14 @@ func TestEventHandler_1ToNMapping(t *testing.T) { }) t.Run("avoid unsubscribe from active providers - default and named", func(t *testing.T) { + eventingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, &ProviderEventing{ - c: make(chan Event), + c: make(chan Event, 1), }, } @@ -1227,15 +1253,11 @@ func TestEventHandler_1ToNMapping(t *testing.T) { overridingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, &ProviderEventing{ - c: make(chan Event), + c: make(chan Event, 1), }, } @@ -1250,17 +1272,14 @@ func TestEventHandler_1ToNMapping(t *testing.T) { }) t.Run("avoid unsubscribe from active providers - named only", func(t *testing.T) { + eventingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, &ProviderEventing{ - c: make(chan Event), + c: make(chan Event, 1), }, } @@ -1278,15 +1297,11 @@ func TestEventHandler_1ToNMapping(t *testing.T) { overridingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, &ProviderEventing{ - c: make(chan Event), + c: make(chan Event, 1), }, } @@ -1301,17 +1316,14 @@ func TestEventHandler_1ToNMapping(t *testing.T) { }) t.Run("unbound providers must be removed from active subscriptions", func(t *testing.T) { + eventingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, &ProviderEventing{ - c: make(chan Event), + c: make(chan Event, 1), }, } @@ -1324,15 +1336,11 @@ func TestEventHandler_1ToNMapping(t *testing.T) { overridingProvider := struct { FeatureProvider - StateHandler EventHandler }{ NoopProvider{}, - &stateHandlerForTests{ - State: NotReadyState, - }, &ProviderEventing{ - c: make(chan Event), + c: make(chan Event, 1), }, } diff --git a/openfeature/hooks/logging_hook_test.go b/openfeature/hooks/logging_hook_test.go index a8b20a87..832d770d 100644 --- a/openfeature/hooks/logging_hook_test.go +++ b/openfeature/hooks/logging_hook_test.go @@ -100,12 +100,12 @@ func testLoggingHookLogsMessagesAsExpected(hook LoggingHook, logger *slog.Logger }, }) - err := openfeature.SetProviderAndWait(memoryProvider) + err := openfeature.SetNamedProviderAndWait("test-app", memoryProvider) if err != nil { t.Error("error setting provider", err) } openfeature.AddHooks(&hook) - client := openfeature.NewClient("test-app") + client := openfeature.GetApiInstance().GetNamedClient("test-app") t.Run("test boolean success", func(t *testing.T) { res, err := client.BooleanValue( diff --git a/openfeature/hooks_test.go b/openfeature/hooks_test.go index a5b58a0a..2ffe9338 100644 --- a/openfeature/hooks_test.go +++ b/openfeature/hooks_test.go @@ -155,11 +155,10 @@ func TestRequirement_4_3_2(t *testing.T) { t.Run("before stage MUST run before flag resolution occurs", func(t *testing.T) { mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -177,7 +176,7 @@ func TestRequirement_4_3_2(t *testing.T) { mockHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) - _, err = client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err != nil { t.Errorf("unexpected err: %v", err) } @@ -205,14 +204,14 @@ func TestRequirement_4_3_3(t *testing.T) { mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } mockHook1 := NewMockHook(ctrl) mockHook2 := NewMockHook(ctrl) - client := NewClient("test") + client := GetApiInstance().GetNamedClient(t.Name()) flagKey := "foo" defaultValue := "bar" @@ -265,13 +264,13 @@ func TestRequirement_4_3_4(t *testing.T) { mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } mockHook := NewMockHook(ctrl) - client := NewClient("test") + client := GetApiInstance().GetNamedClient(t.Name()) apiEvalCtx := EvaluationContext{ attributes: map[string]interface{}{ @@ -339,11 +338,10 @@ func TestRequirement_4_3_5(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -361,7 +359,7 @@ func TestRequirement_4_3_5(t *testing.T) { After(mockProvider.EXPECT().StringEvaluation(gomock.Any(), flagKey, defaultValue, flatCtx)) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) - _, err = client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err != nil { t.Errorf("unexpected err: %v", err) } @@ -396,11 +394,10 @@ func TestRequirement_4_3_6(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -412,7 +409,7 @@ func TestRequirement_4_3_6(t *testing.T) { After(mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced"))) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) - _, err = client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -422,11 +419,10 @@ func TestRequirement_4_3_6(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -446,7 +442,7 @@ func TestRequirement_4_3_6(t *testing.T) { ) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) - _, err = client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -456,11 +452,10 @@ func TestRequirement_4_3_6(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -474,7 +469,7 @@ func TestRequirement_4_3_6(t *testing.T) { After(mockHook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("forced"))) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) - _, err = client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -508,11 +503,10 @@ func TestRequirement_4_3_7(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -525,7 +519,7 @@ func TestRequirement_4_3_7(t *testing.T) { After(mockHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())) mockProvider.EXPECT().StringEvaluation(context.Background(), flagKey, defaultValue, flatCtx) - _, err = client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err != nil { t.Errorf("unexpected err: %v", err) } @@ -535,11 +529,10 @@ func TestRequirement_4_3_7(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -551,7 +544,7 @@ func TestRequirement_4_3_7(t *testing.T) { mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()). After(mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())) - _, err = client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + _, err = GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -650,21 +643,23 @@ func TestRequirement_4_4_2(t *testing.T) { mockAPIHook := NewMockHook(ctrl) AddHooks(mockAPIHook) mockClientHook := NewMockHook(ctrl) - client := NewClient("test") - client.AddHooks(mockClientHook) + mockInvocationHook := NewMockHook(ctrl) mockProviderHook := NewMockHook(ctrl) mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(2) + client := GetApiInstance().GetNamedClient(t.Name()) + client.AddHooks(mockClientHook) + // before: API, Client, Invocation, Provider mockProviderHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()). After(mockInvocationHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any())). @@ -697,19 +692,21 @@ func TestRequirement_4_4_2(t *testing.T) { mockAPIHook := NewMockHook(ctrl) AddHooks(mockAPIHook) mockClientHook := NewMockHook(ctrl) - client := NewClient("test") - client.AddHooks(mockClientHook) + mockInvocationHook := NewMockHook(ctrl) mockProviderHook := NewMockHook(ctrl) mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } + client := GetApiInstance().GetNamedClient(t.Name()) + client.AddHooks(mockClientHook) + mockProvider.EXPECT().Hooks().Return([]Hook{mockProviderHook}).Times(2) mockAPIHook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) @@ -766,15 +763,17 @@ func TestRequirement_4_4_6(t *testing.T) { mockHook1 := NewMockHook(ctrl) mockHook2 := NewMockHook(ctrl) - client := NewClient("test") + mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } + client := GetApiInstance().GetNamedClient(t.Name()) + mockProvider.EXPECT().Hooks().AnyTimes() mockHook1.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("forced")) @@ -798,15 +797,16 @@ func TestRequirement_4_4_6(t *testing.T) { mockHook1 := NewMockHook(ctrl) mockHook2 := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } + client := GetApiInstance().GetNamedClient(t.Name()) + mockProvider.EXPECT().Hooks().AnyTimes() mockHook1.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()) @@ -838,11 +838,10 @@ func TestRequirement_4_4_7(t *testing.T) { evalCtx := EvaluationContext{} mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } @@ -853,7 +852,7 @@ func TestRequirement_4_4_7(t *testing.T) { mockHook.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()) mockHook.EXPECT().Finally(gomock.Any(), gomock.Any(), gomock.Any()) - res, err := client.StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) + res, err := GetApiInstance().GetNamedClient(t.Name()).StringValueDetails(context.Background(), flagKey, defaultValue, evalCtx, WithHooks(mockHook)) if err == nil { t.Error("expected error, got nil") } @@ -885,15 +884,16 @@ func TestRequirement_4_5_2(t *testing.T) { t.Run("hook hints must be passed to before, after & finally hooks", func(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } + client := GetApiInstance().GetNamedClient(t.Name()) + mockProvider.EXPECT().Hooks().AnyTimes() hookHints := NewHookHints(map[string]interface{}{"foo": "bar"}) @@ -913,15 +913,17 @@ func TestRequirement_4_5_2(t *testing.T) { t.Run("hook hints must be passed to error hooks", func(t *testing.T) { defer t.Cleanup(initSingleton) mockHook := NewMockHook(ctrl) - client := NewClient("test") + mockProvider := NewMockFeatureProvider(ctrl) mockProvider.EXPECT().Metadata().AnyTimes() - err := SetProvider(mockProvider) + err := SetNamedProviderAndWait(t.Name(), mockProvider) if err != nil { t.Errorf("error setting up provider %v", err) } + client := GetApiInstance().GetNamedClient(t.Name()) + // wait for initialization time.Sleep(200 * time.Millisecond) diff --git a/openfeature/interfaces.go b/openfeature/interfaces.go index ac9b4628..76347f50 100644 --- a/openfeature/interfaces.go +++ b/openfeature/interfaces.go @@ -40,6 +40,8 @@ type IClient interface { Int(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) int64 Object(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) interface{} + State() State + IEventing } diff --git a/openfeature/openfeature_api.go b/openfeature/openfeature_api.go index 0d854dfa..317f776f 100644 --- a/openfeature/openfeature_api.go +++ b/openfeature/openfeature_api.go @@ -74,7 +74,7 @@ func (api *evaluationAPI) SetNamedProvider(clientName string, provider FeaturePr oldProvider := api.namedProviders[clientName] api.namedProviders[clientName] = provider - err := api.initNewAndShutdownOld(provider, oldProvider, async) + err := api.initNewAndShutdownOld(clientName, provider, oldProvider, async) if err != nil { return err } @@ -208,7 +208,7 @@ func (api *evaluationAPI) setProvider(provider FeatureProvider, async bool) erro oldProvider := api.defaultProvider api.defaultProvider = provider - err := api.initNewAndShutdownOld(provider, oldProvider, async) + err := api.initNewAndShutdownOld("", provider, oldProvider, async) if err != nil { return err } @@ -222,15 +222,17 @@ func (api *evaluationAPI) setProvider(provider FeatureProvider, async bool) erro } // initNewAndShutdownOld is a helper to initialise new FeatureProvider and Shutdown the old FeatureProvider. -func (api *evaluationAPI) initNewAndShutdownOld(newProvider FeatureProvider, oldProvider FeatureProvider, async bool) error { +func (api *evaluationAPI) initNewAndShutdownOld(clientName string, newProvider FeatureProvider, oldProvider FeatureProvider, async bool) error { if async { go func(executor *eventExecutor, ctx EvaluationContext) { // for async initialization, error is conveyed as an event event, _ := initializer(newProvider, ctx) + executor.states.Store(clientName, stateFromEventOrError(event, nil)) executor.triggerEvent(event, newProvider) }(api.eventExecutor, api.apiCtx) } else { event, err := initializer(newProvider, api.apiCtx) + api.eventExecutor.states.Store(clientName, stateFromEventOrError(event, err)) api.eventExecutor.triggerEvent(event, newProvider) if err != nil { return err @@ -277,6 +279,13 @@ func initializer(provider FeatureProvider, apiCtx EvaluationContext) (Event, err if err != nil { event.EventType = ProviderError event.Message = fmt.Sprintf("Provider initialization error, %v", err) + var initErr *ProviderInitError + if errors.As(err, &initErr) { + event.EventType = ProviderError + event.ErrorCode = initErr.ErrorCode + event.Message = initErr.Message + } + } return event, err @@ -291,3 +300,40 @@ func contains(provider FeatureProvider, in []FeatureProvider) bool { return false } + +var statesMap = map[EventType]func(ProviderEventDetails) State{ + ProviderReady: func(_ ProviderEventDetails) State { return ReadyState }, + ProviderConfigChange: func(_ ProviderEventDetails) State { return ReadyState }, + ProviderStale: func(_ ProviderEventDetails) State { return StaleState }, + ProviderError: func(e ProviderEventDetails) State { + if e.ErrorCode == ProviderFatalCode { + return FatalState + } + return ErrorState + }, +} + +func stateFromEventOrError(event Event, err error) State { + if err != nil { + return stateFromError(err) + } + return stateFromEvent(event) +} + +func stateFromEvent(event Event) State { + if stateFn, ok := statesMap[event.EventType]; ok { + return stateFn(event.ProviderEventDetails) + } + return NotReadyState // default +} + +func stateFromError(err error) State { + var e *ProviderInitError + switch { + case errors.As(err, &e): + if e.ErrorCode == ProviderFatalCode { + return FatalState + } + } + return ErrorState // default +} diff --git a/openfeature/openfeature_test.go b/openfeature/openfeature_test.go index f01eacd8..6427b682 100644 --- a/openfeature/openfeature_test.go +++ b/openfeature/openfeature_test.go @@ -757,7 +757,6 @@ func setupProviderWithSemaphores() (struct { shutdownF: func() { shutdownSem <- "" }, - State: NotReadyState, } eventing := &ProviderEventing{ diff --git a/openfeature/provider.go b/openfeature/provider.go index 634b23b3..624deeda 100644 --- a/openfeature/provider.go +++ b/openfeature/provider.go @@ -27,6 +27,7 @@ const ( ReadyState State = "READY" ErrorState State = "ERROR" StaleState State = "STALE" + FatalState State = "FATAL" ProviderReady EventType = "PROVIDER_READY" ProviderConfigChange EventType = "PROVIDER_CONFIGURATION_CHANGED" @@ -63,7 +64,6 @@ type State string type StateHandler interface { Init(evaluationContext EvaluationContext) error Shutdown() - Status() State } // NoopStateHandler is a noop StateHandler implementation @@ -99,6 +99,7 @@ type ProviderEventDetails struct { Message string FlagChanges []string EventMetadata map[string]interface{} + ErrorCode ErrorCode } // Event is an event emitted by a FeatureProvider. diff --git a/openfeature/resolution_error.go b/openfeature/resolution_error.go index b4d4ae00..78b0e118 100644 --- a/openfeature/resolution_error.go +++ b/openfeature/resolution_error.go @@ -1,12 +1,17 @@ package openfeature -import "fmt" +import ( + "errors" + "fmt" +) type ErrorCode string const ( // ProviderNotReadyCode - the value was resolved before the provider was ready. ProviderNotReadyCode ErrorCode = "PROVIDER_NOT_READY" + // ProviderFatalCode - a fatal provider error occured + ProviderFatalCode ErrorCode = "PROVIDER_FATAL" // FlagNotFoundCode - the flag could not be found. FlagNotFoundCode ErrorCode = "FLAG_NOT_FOUND" // ParseErrorCode - an error was encountered parsing data, such as a flag configuration. @@ -102,3 +107,21 @@ func NewGeneralResolutionError(msg string) ResolutionError { message: msg, } } + +// ProviderInitError represents an error that occurs during provider initialization. +type ProviderInitError struct { + ErrorCode ErrorCode // Field to store the specific error code + Message string // Custom error message +} + +// Error implements the error interface for ProviderInitError. +func (e *ProviderInitError) Error() string { + return fmt.Sprintf("ProviderInitError: %s (code: %s)", e.Message, e.ErrorCode) +} + +var ( + // ProviderNotReadyError signifies that an operation failed because the provider is in a NOT_READY state. + ProviderNotReadyError = errors.New("provider not yet initialized") + // ProviderFatalError signifies that an operation failed because the provider is in a FATAL state. + ProviderFatalError = errors.New("provider is in an irrecoverable error state") +) diff --git a/openfeature/util_test.go b/openfeature/util_test.go index c4ac6f5a..142f45cc 100644 --- a/openfeature/util_test.go +++ b/openfeature/util_test.go @@ -1,5 +1,10 @@ package openfeature +import ( + "testing" + "time" +) + // Test Utils // event handlers @@ -30,7 +35,6 @@ func init() { type stateHandlerForTests struct { initF func(e EvaluationContext) error shutdownF func() - State } func (s *stateHandlerForTests) Init(e EvaluationContext) error { @@ -46,10 +50,6 @@ func (s *stateHandlerForTests) Shutdown() { } } -func (s *stateHandlerForTests) Status() State { - return s.State -} - // ProviderEventing is an eventing implementation with invoke capability type ProviderEventing struct { c chan Event @@ -62,3 +62,18 @@ func (s ProviderEventing) Invoke(e Event) { func (s ProviderEventing) EventChannel() <-chan Event { return s.c } + +func eventually(t *testing.T, condition func() bool, timeout, interval time.Duration, errMsg string) { + t.Helper() + + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + if condition() { + return + } + time.Sleep(interval) + } + + t.Fatalf("condition not met: %s", errMsg) +}