Repository: open-feature/go-sdk
Pull request #93 (2022-10-06 16:17:41)

fix: locks on singleton and client state to ensure thread safety
<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR
<!-- add the description of the PR here -->

- locks on singleton and client state to ensure thread safety

This PR adds multi-read/single-write locks to both the global singleton and the client. Particularly it addresses problems relating to the providers/hooks being mutated while an evaluation is occurring. This is especially important since some providers have provider hooks, and if we don't lock, it's possible hooks from provider1 run on provider2 if an interleaving thread switched the provider.

I've opted for the approach of locking a provider and its hooks to an evaluation transaction. Another thread is free to set a new provider but any ongoing transactions will carry on using the existing provider and its hooks.


### Related Issues
<!-- add here the GitHub issue that this PR resolves if applicable -->

Fixes https://github.com/open-feature/go-sdk/issues/90

### Notes
<!-- any additional notes for this PR -->

### Follow-up Tasks
<!-- anything that is related to this PR but not done here should be noted under this section -->
<!-- if there is a need for a new issue, please link it here -->

### How to test
<!-- if applicable, add testing instructions under this section -->



codecov-commenter: # [Codecov](https://codecov.io/gh/open-feature/go-sdk/pull/93?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
> Merging [#93](https://codecov.io/gh/open-feature/go-sdk/pull/93?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (efd2342) into [main](https://codecov.io/gh/open-feature/go-sdk/commit/9fbed881195a5e1801a0157309db855bbad2b356?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (9fbed88) will **increase** coverage by `2.02%`.
> The diff coverage is `95.34%`.

```diff
@@            Coverage Diff             @@
##             main      #93      +/-   ##
==========================================
+ Coverage   69.24%   71.26%   +2.02%     
==========================================
  Files           7        7              
  Lines         569      623      +54     
==========================================
+ Hits          394      444      +50     
- Misses        158      162       +4     
  Partials       17       17              
```


| [Impacted Files](https://codecov.io/gh/open-feature/go-sdk/pull/93?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [pkg/openfeature/client.go](https://codecov.io/gh/open-feature/go-sdk/pull/93/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL2NsaWVudC5nbw==) | `71.23% <93.75%> (+1.85%)` | :arrow_up: |
| [pkg/openfeature/openfeature.go](https://codecov.io/gh/open-feature/go-sdk/pull/93/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL29wZW5mZWF0dXJlLmdv) | `92.98% <100.00%> (+2.28%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature)


Patch:
diff --git a/Makefile b/Makefile
index 89d57b9..8065a47 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,7 @@
 mockgen:
 	mockgen -source=pkg/openfeature/provider.go -destination=pkg/openfeature/provider_mock_test.go -package=openfeature
 	mockgen -source=pkg/openfeature/hooks.go -destination=pkg/openfeature/hooks_mock_test.go -package=openfeature
+	mockgen -source=pkg/openfeature/mutex.go -destination=pkg/openfeature/mutex_mock_test.go -package=openfeature
 test:
 	go test -cover ./...
 lint:
diff --git a/pkg/openfeature/client.go b/pkg/openfeature/client.go
index a22e086..9033a8c 100644
--- a/pkg/openfeature/client.go
+++ b/pkg/openfeature/client.go
@@ -39,7 +39,7 @@ func (cm ClientMetadata) Name() string {
 
 // Client implements the behaviour required of an openfeature client
 type Client struct {
-	mx                *sync.Mutex
+	mx                sync.RWMutex
 	metadata          ClientMetadata
 	hooks             []Hook
 	evaluationContext EvaluationContext
@@ -49,7 +49,6 @@ type Client struct {
 // NewClient returns a new Client. Name is a unique identifier for this client
 func NewClient(name string) *Client {
 	return &Client{
-		mx:                &sync.Mutex{},
 		metadata:          ClientMetadata{name: name},
 		hooks:             []Hook{},
 		evaluationContext: EvaluationContext{},
@@ -66,7 +65,9 @@ func (c *Client) WithLogger(l logr.Logger) *Client {
 }
 
 // Metadata returns the client's metadata
-func (c Client) Metadata() ClientMetadata {
+func (c *Client) Metadata() ClientMetadata {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
 	return c.metadata
 }
 
@@ -75,7 +76,7 @@ func (c *Client) AddHooks(hooks ...Hook) {
 	c.mx.Lock()
 	defer c.mx.Unlock()
 	c.hooks = append(c.hooks, hooks...)
-	c.logger().V(info).Info("appended hooks to client", "client", c.Metadata().name, "hooks", hooks)
+	c.logger().V(info).Info("appended hooks to client", "client", c.metadata.name, "hooks", hooks)
 }
 
 // SetEvaluationContext sets the client's evaluation context
@@ -84,12 +85,14 @@ func (c *Client) SetEvaluationContext(evalCtx EvaluationContext) {
 	defer c.mx.Unlock()
 	c.evaluationContext = evalCtx
 	c.logger().V(info).Info(
-		"set client evaluation context", "client", c.Metadata().name, "evaluationContext", evalCtx,
+		"set client evaluation context", "client", c.metadata.name, "evaluationContext", evalCtx,
 	)
 }
 
 // EvaluationContext returns the client's evaluation context
 func (c *Client) EvaluationContext() EvaluationContext {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
 	return c.evaluationContext
 }
 
@@ -179,7 +182,10 @@ func WithHookHints(hookHints HookHints) Option {
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) BooleanValue(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (bool, error) {
+func (c *Client) BooleanValue(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (bool, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -211,7 +217,10 @@ func (c Client) BooleanValue(ctx context.Context, flag string, defaultValue bool
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) StringValue(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (string, error) {
+func (c *Client) StringValue(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (string, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -243,7 +252,10 @@ func (c Client) StringValue(ctx context.Context, flag string, defaultValue strin
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) FloatValue(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (float64, error) {
+func (c *Client) FloatValue(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (float64, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -275,7 +287,10 @@ func (c Client) FloatValue(ctx context.Context, flag string, defaultValue float6
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) IntValue(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (int64, error) {
+func (c *Client) IntValue(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (int64, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -307,7 +322,10 @@ func (c Client) IntValue(ctx context.Context, flag string, defaultValue int64, e
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) ObjectValue(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (interface{}, error) {
+func (c *Client) ObjectValue(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (interface{}, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -325,7 +343,10 @@ func (c Client) ObjectValue(ctx context.Context, flag string, defaultValue inter
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) BooleanValueDetails(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (BooleanEvaluationDetails, error) {
+func (c *Client) BooleanValueDetails(ctx context.Context, flag string, defaultValue bool, evalCtx EvaluationContext, options ...Option) (BooleanEvaluationDetails, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -370,7 +391,10 @@ func (c Client) BooleanValueDetails(ctx context.Context, flag string, defaultVal
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) StringValueDetails(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (StringEvaluationDetails, error) {
+func (c *Client) StringValueDetails(ctx context.Context, flag string, defaultValue string, evalCtx EvaluationContext, options ...Option) (StringEvaluationDetails, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -415,7 +439,10 @@ func (c Client) StringValueDetails(ctx context.Context, flag string, defaultValu
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) FloatValueDetails(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (FloatEvaluationDetails, error) {
+func (c *Client) FloatValueDetails(ctx context.Context, flag string, defaultValue float64, evalCtx EvaluationContext, options ...Option) (FloatEvaluationDetails, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -460,7 +487,10 @@ func (c Client) FloatValueDetails(ctx context.Context, flag string, defaultValue
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) IntValueDetails(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (IntEvaluationDetails, error) {
+func (c *Client) IntValueDetails(ctx context.Context, flag string, defaultValue int64, evalCtx EvaluationContext, options ...Option) (IntEvaluationDetails, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -505,7 +535,10 @@ func (c Client) IntValueDetails(ctx context.Context, flag string, defaultValue i
 // - defaultValue is returned if an error occurs
 // - evalCtx is the evaluation context used in a flag evaluation (not to be confused with ctx)
 // - options are optional additional evaluation options e.g. WithHooks & WithHookHints
-func (c Client) ObjectValueDetails(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) {
+func (c *Client) ObjectValueDetails(ctx context.Context, flag string, defaultValue interface{}, evalCtx EvaluationContext, options ...Option) (InterfaceEvaluationDetails, error) {
+	c.mx.RLock()
+	defer c.mx.RUnlock()
+
 	evalOptions := &EvaluationOptions{}
 	for _, option := range options {
 		option(evalOptions)
@@ -514,14 +547,24 @@ func (c Client) ObjectValueDetails(ctx context.Context, flag string, defaultValu
 	return c.evaluate(ctx, flag, Object, defaultValue, evalCtx, *evalOptions)
 }
 
-func (c Client) evaluate(
+func (c *Client) evaluate(
 	ctx context.Context, flag string, flagType Type, defaultValue interface{}, evalCtx EvaluationContext, options EvaluationOptions,
 ) (InterfaceEvaluationDetails, error) {
 	c.logger().V(debug).Info(
 		"evaluating flag", "flag", flag, "type", flagType.String(), "defaultValue", defaultValue,
 		"evaluationContext", evalCtx, "evaluationOptions", options,
 	)
-	evalCtx = mergeContexts(evalCtx, c.evaluationContext, api.evaluationContext) // API (global) -> client -> invocation
+
+	// ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour
+	api.RLock()
+	provider := api.prvder
+	globalHooks := api.hks
+	globalCtx := api.evalCtx
+	api.RUnlock()
+
+	evalCtx = mergeContexts(evalCtx, c.evaluationContext, globalCtx)                                                           // API (global) -> client -> invocation
+	apiClientInvocationProviderHooks := append(append(append(globalHooks, c.hooks...), options.hooks...), provider.Hooks()...) // API, Client, Invocation, Provider
+	providerInvocationClientApiHooks := append(append(append(provider.Hooks(), options.hooks...), c.hooks...), globalHooks...) // Provider, Invocation, Client, API
 
 	var err error
 	hookCtx := HookContext{
@@ -529,7 +572,7 @@ func (c Client) evaluate(
 		flagType:          flagType,
 		defaultValue:      defaultValue,
 		clientMetadata:    c.metadata,
-		providerMetadata:  api.provider.Metadata(),
+		providerMetadata:  provider.Metadata(),
 		evaluationContext: evalCtx,
 	}
 	evalDetails := InterfaceEvaluationDetails{
@@ -540,8 +583,6 @@ func (c Client) evaluate(
 		},
 	}
 
-	apiClientInvocationProviderHooks := append(append(append(api.hooks, c.hooks...), options.hooks...), api.provider.Hooks()...) // API, Client, Invocation, Provider
-	providerInvocationClientApiHooks := append(append(append(api.provider.Hooks(), options.hooks...), c.hooks...), api.hooks...) // Provider, Invocation, Client, API
 	defer func() {
 		c.finallyHooks(hookCtx, providerInvocationClientApiHooks, options)
 	}()
@@ -562,25 +603,25 @@ func (c Client) evaluate(
 	var resolution InterfaceResolutionDetail
 	switch flagType {
 	case Object:
-		resolution = api.provider.ObjectEvaluation(ctx, flag, defaultValue, flatCtx)
+		resolution = provider.ObjectEvaluation(ctx, flag, defaultValue, flatCtx)
 	case Boolean:
 		defValue := defaultValue.(bool)
-		res := api.provider.BooleanEvaluation(ctx, flag, defValue, flatCtx)
+		res := provider.BooleanEvaluation(ctx, flag, defValue, flatCtx)
 		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	case String:
 		defValue := defaultValue.(string)
-		res := api.provider.StringEvaluation(ctx, flag, defValue, flatCtx)
+		res := provider.StringEvaluation(ctx, flag, defValue, flatCtx)
 		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	case Float:
 		defValue := defaultValue.(float64)
-		res := api.provider.FloatEvaluation(ctx, flag, defValue, flatCtx)
+		res := provider.FloatEvaluation(ctx, flag, defValue, flatCtx)
 		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	case Int:
 		defValue := defaultValue.(int64)
-		res := api.provider.IntEvaluation(ctx, flag, defValue, flatCtx)
+		res := provider.IntEvaluation(ctx, flag, defValue, flatCtx)
 		resolution.ProviderResolutionDetail = res.ProviderResolutionDetail
 		resolution.Value = res.Value
 	}
@@ -628,7 +669,7 @@ func flattenContext(evalCtx EvaluationContext) FlattenedContext {
 	return flatCtx
 }
 
-func (c Client) beforeHooks(
+func (c *Client) beforeHooks(
 	hookCtx HookContext, hooks []Hook, evalCtx EvaluationContext, options EvaluationOptions,
 ) (EvaluationContext, error) {
 	c.logger().V(debug).Info("executing before hooks")
@@ -647,7 +688,7 @@ func (c Client) beforeHooks(
 	return mergeContexts(hookCtx.evaluationContext, evalCtx), nil
 }
 
-func (c Client) afterHooks(
+func (c *Client) afterHooks(
 	hookCtx HookContext, hooks []Hook, evalDetails InterfaceEvaluationDetails, options EvaluationOptions,
 ) error {
 	c.logger().V(debug).Info("executing after hooks")
@@ -662,7 +703,7 @@ func (c Client) afterHooks(
 	return nil
 }
 
-func (c Client) errorHooks(hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) {
+func (c *Client) errorHooks(hookCtx HookContext, hooks []Hook, err error, options EvaluationOptions) {
 	c.logger().V(debug).Info("executing error hooks")
 	defer c.logger().V(debug).Info("executed error hooks")
 
@@ -671,7 +712,7 @@ func (c Client) errorHooks(hookCtx HookContext, hooks []Hook, err error, options
 	}
 }
 
-func (c Client) finallyHooks(hookCtx HookContext, hooks []Hook, options EvaluationOptions) {
+func (c *Client) finallyHooks(hookCtx HookContext, hooks []Hook, options EvaluationOptions) {
 	c.logger().V(debug).Info("executing finally hooks")
 	defer c.logger().V(debug).Info("executed finally hooks")
 
diff --git a/pkg/openfeature/mutex.go b/pkg/openfeature/mutex.go
new file mode 100644
index 0000000..5234bee
--- /dev/null
+++ b/pkg/openfeature/mutex.go
@@ -0,0 +1,9 @@
+package openfeature
+
+// for the sake of testing
+type mutex interface {
+	Lock()
+	Unlock()
+	RLock()
+	RUnlock()
+}
diff --git a/pkg/openfeature/openfeature.go b/pkg/openfeature/openfeature.go
index 265f644..b4721f2 100644
--- a/pkg/openfeature/openfeature.go
+++ b/pkg/openfeature/openfeature.go
@@ -7,11 +7,11 @@ import (
 )
 
 type evaluationAPI struct {
-	provider          FeatureProvider
-	hooks             []Hook
-	evaluationContext EvaluationContext
-	logger            logr.Logger
-	sync.RWMutex
+	prvder  FeatureProvider
+	hks     []Hook
+	evalCtx EvaluationContext
+	logger  logr.Logger
+	mutex
 }
 
 // api is the global evaluationAPI.  This is a singleton and there can only be one instance.
@@ -24,10 +24,11 @@ func init() {
 
 func initSingleton() {
 	api = evaluationAPI{
-		provider:          NoopProvider{},
-		hooks:             []Hook{},
-		evaluationContext: EvaluationContext{},
-		logger:            logr.New(logger{}),
+		prvder:  NoopProvider{},
+		hks:     []Hook{},
+		evalCtx: EvaluationContext{},
+		logger:  logr.New(logger{}),
+		mutex:   &sync.RWMutex{},
 	}
 }
 
@@ -47,17 +48,23 @@ func (e EvaluationOptions) Hooks() []Hook {
 	return e.hooks
 }
 
+func (api *evaluationAPI) provider() FeatureProvider {
+	api.RLock()
+	defer api.RUnlock()
+	return api.prvder
+}
+
 func (api *evaluationAPI) setProvider(provider FeatureProvider) {
 	api.Lock()
 	defer api.Unlock()
-	api.provider = provider
+	api.prvder = provider
 	api.logger.V(info).Info("set global provider", "name", provider.Metadata().Name)
 }
 
 func (api *evaluationAPI) setEvaluationContext(evalCtx EvaluationContext) {
 	api.Lock()
 	defer api.Unlock()
-	api.evaluationContext = evalCtx
+	api.evalCtx = evalCtx
 	api.logger.V(info).Info("set global evaluation context", "evaluationContext", evalCtx)
 }
 
@@ -68,6 +75,12 @@ func (api *evaluationAPI) setLogger(l logr.Logger) {
 	api.logger.V(info).Info("set global logger")
 }
 
+func (api *evaluationAPI) hooks() []Hook {
+	api.RLock()
+	defer api.RUnlock()
+	return api.hks
+}
+
 // SetProvider sets the global provider.
 func SetProvider(provider FeatureProvider) {
 	api.setProvider(provider)
@@ -85,17 +98,19 @@ func SetLogger(l logr.Logger) {
 
 // ProviderMetadata returns the global provider's metadata
 func ProviderMetadata() Metadata {
-	return api.provider.Metadata()
+	return api.provider().Metadata()
 }
 
 // AddHooks appends to the collection of any previously added hooks
 func AddHooks(hooks ...Hook) {
 	api.Lock()
 	defer api.Unlock()
-	api.hooks = append(api.hooks, hooks...)
+	api.hks = append(api.hks, hooks...)
 	api.logger.V(info).Info("appended hooks to the global singleton", "hooks", hooks)
 }
 
 func globalLogger() logr.Logger {
+	api.RLock()
+	defer api.RUnlock()
 	return api.logger
 }


Test patch:
diff --git a/pkg/openfeature/client_test.go b/pkg/openfeature/client_test.go
index cabc329..daaf559 100644
--- a/pkg/openfeature/client_test.go
+++ b/pkg/openfeature/client_test.go
@@ -806,3 +806,63 @@ func TestErrorCodeFromProviderReturnedInEvaluationDetails(t *testing.T) {
 		)
 	}
 }
+
+func TestSwitchingProvidersMidEvaluationCausesNoImpactToEvaluation(t *testing.T) {
+	defer t.Cleanup(initSingleton)
+	ctrl := gomock.NewController(t)
+
+	mockProvider1 := NewMockFeatureProvider(ctrl)
+	mockProvider2 := NewMockFeatureProvider(ctrl)
+	mockProvider1Hook := NewMockHook(ctrl)
+	mockProvider1.EXPECT().Metadata().AnyTimes()
+	mockProvider2.EXPECT().Metadata().AnyTimes()
+	mockProvider1.EXPECT().Hooks().Return([]Hook{mockProvider1Hook}).AnyTimes()
+
+	// set new provider during initial provider's Before hook
+	mockProvider1Hook.EXPECT().Before(gomock.Any(), gomock.Any()).
+		DoAndReturn(func(_ HookContext, _ HookHints) (*EvaluationContext, error) {
+			SetProvider(mockProvider2)
+			return nil, nil
+		})
+	SetProvider(mockProvider1)
+
+	mockProvider1.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
+	// ensure that the first provider's hooks are still fired
+	mockProvider1Hook.EXPECT().After(gomock.Any(), gomock.Any(), gomock.Any())
+	mockProvider1Hook.EXPECT().Finally(gomock.Any(), gomock.Any())
+
+	client := NewClient("test")
+	_, err := client.BooleanValue(context.Background(), "foo", true, EvaluationContext{})
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestClientProviderLock(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer t.Cleanup(initSingleton)
+
+	mockMutex := NewMockmutex(ctrl)
+	mockProvider1 := NewMockFeatureProvider(ctrl)
+	mockProvider2 := NewMockFeatureProvider(ctrl)
+
+	mockProvider1.EXPECT().BooleanEvaluation(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
+	mockProvider1.EXPECT().Hooks().AnyTimes()
+	mockProvider1.EXPECT().Metadata().AnyTimes()
+
+	mockMutex.EXPECT().RLock().DoAndReturn(func() {
+		api.prvder = mockProvider1
+	})
+
+	// test that any provider change after RUnlock has no impact on this transaction
+	mockMutex.EXPECT().RUnlock().DoAndReturn(func() {
+		api.prvder = mockProvider2
+	})
+	api.mutex = mockMutex
+
+	client := NewClient("test").WithLogger(logr.New(logger{}))
+	_, err := client.BooleanValue(context.Background(), "foo", false, EvaluationContext{})
+	if err != nil {
+		t.Error(err)
+	}
+}
diff --git a/pkg/openfeature/mutex_mock_test.go b/pkg/openfeature/mutex_mock_test.go
new file mode 100644
index 0000000..e27bb5a
--- /dev/null
+++ b/pkg/openfeature/mutex_mock_test.go
@@ -0,0 +1,82 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: pkg/openfeature/mutex.go
+
+// Package openfeature is a generated GoMock package.
+package openfeature
+
+import (
+	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
+)
+
+// Mockmutex is a mock of mutex interface.
+type Mockmutex struct {
+	ctrl     *gomock.Controller
+	recorder *MockmutexMockRecorder
+}
+
+// MockmutexMockRecorder is the mock recorder for Mockmutex.
+type MockmutexMockRecorder struct {
+	mock *Mockmutex
+}
+
+// NewMockmutex creates a new mock instance.
+func NewMockmutex(ctrl *gomock.Controller) *Mockmutex {
+	mock := &Mockmutex{ctrl: ctrl}
+	mock.recorder = &MockmutexMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *Mockmutex) EXPECT() *MockmutexMockRecorder {
+	return m.recorder
+}
+
+// Lock mocks base method.
+func (m *Mockmutex) Lock() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Lock")
+}
+
+// Lock indicates an expected call of Lock.
+func (mr *MockmutexMockRecorder) Lock() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*Mockmutex)(nil).Lock))
+}
+
+// RLock mocks base method.
+func (m *Mockmutex) RLock() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "RLock")
+}
+
+// RLock indicates an expected call of RLock.
+func (mr *MockmutexMockRecorder) RLock() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RLock", reflect.TypeOf((*Mockmutex)(nil).RLock))
+}
+
+// RUnlock mocks base method.
+func (m *Mockmutex) RUnlock() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "RUnlock")
+}
+
+// RUnlock indicates an expected call of RUnlock.
+func (mr *MockmutexMockRecorder) RUnlock() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RUnlock", reflect.TypeOf((*Mockmutex)(nil).RUnlock))
+}
+
+// Unlock mocks base method.
+func (m *Mockmutex) Unlock() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Unlock")
+}
+
+// Unlock indicates an expected call of Unlock.
+func (mr *MockmutexMockRecorder) Unlock() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*Mockmutex)(nil).Unlock))
+}
diff --git a/pkg/openfeature/openfeature_test.go b/pkg/openfeature/openfeature_test.go
index 2c7bb6c..f3a7676 100644
--- a/pkg/openfeature/openfeature_test.go
+++ b/pkg/openfeature/openfeature_test.go
@@ -17,7 +17,7 @@ func TestRequirement_1_1_1(t *testing.T) {
 	mockProvider.EXPECT().Metadata().AnyTimes()
 	SetProvider(mockProvider)
 
-	if api.provider != mockProvider {
+	if api.provider() != mockProvider {
 		t.Error("func SetProvider hasn't set the provider to the singleton")
 	}
 }
@@ -50,7 +50,7 @@ func TestRequirement_1_1_3(t *testing.T) {
 	AddHooks(mockHook)
 	AddHooks(mockHook, mockHook)
 
-	if len(api.hooks) != 3 {
+	if len(api.hooks()) != 3 {
 		t.Error("func AddHooks didn't append the list of hooks to the existing collection of hooks")
 	}
 }