Repository: open-feature/go-sdk
Pull request #185 (2023-06-13 20:19:26)

feat: Initialize and shutdown behavior 
## This PR

Fixes https://github.com/open-feature/go-sdk/issues/184 by introducing Initialization & Shutdown support.

### Initialization and Shutdown

This is a non-breaking change 🚀 

Initialization and Shutdown along with Status accessor is introduced through the new interface `StateHandler`.

OpenFeature API implementation undertakes the init and shutdown handling based on interface implementation (type assertion)

### Implementation considerations 

Provider initialization is done asynchronously to avoid blocking the caller. Provider implementations should update their status to `Ready` as soon as initialization is done. 

This implementation does not consider Go `context` to facilitate shutdown. This is to reduce complexity and take over full control of the provider's initialization & shutdown methods.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) Report
> Merging [#185](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (e7e8c3a) into [main](https://app.codecov.io/gh/open-feature/go-sdk/commit/d93f58bfb9ea92fbcbb0faa7cd7880bb629a6ebe?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) (d93f58b) will **decrease** coverage by `0.43%`.
> The diff coverage is `85.91%`.

```diff
@@            Coverage Diff             @@
##             main     #185      +/-   ##
==========================================
- Coverage   72.47%   72.05%   -0.43%     
==========================================
  Files           8        8              
  Lines         723      780      +57     
==========================================
+ Hits          524      562      +38     
- Misses        180      197      +17     
- Partials       19       21       +2     
```


| [Impacted Files](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature) | Coverage Δ | |
|---|---|---|
| [pkg/openfeature/noop\_provider.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL25vb3BfcHJvdmlkZXIuZ28=) | `100.00% <ø> (ø)` | |
| [pkg/openfeature/provider.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL3Byb3ZpZGVyLmdv) | `65.51% <16.66%> (-34.49%)` | :arrow_down: |
| [pkg/openfeature/client.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL2NsaWVudC5nbw==) | `70.20% <20.00%> (-3.04%)` | :arrow_down: |
| [pkg/openfeature/api.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL2FwaS5nbw==) | `94.00% <94.00%> (ø)` | |
| [pkg/openfeature/openfeature.go](https://app.codecov.io/gh/open-feature/go-sdk/pull/185?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=open-feature#diff-cGtnL29wZW5mZWF0dXJlL29wZW5mZWF0dXJlLmdv) | `100.00% <100.00%> (+7.40%)` | :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)

Kavindu-Dodan: > I like moving this into a new interface so we don't break the existing contract. Providers can then add these 3 new functions to their struct receivers in a separate version keeping compatibility with any version of OF SDK.

A big thanks go to @toddbaert for proposing to go with a non-breaking contract. And yes, the implementation is flexible in this regards :) 

Patch:
diff --git a/pkg/openfeature/api.go b/pkg/openfeature/api.go
index e56bf35..7daf7db 100644
--- a/pkg/openfeature/api.go
+++ b/pkg/openfeature/api.go
@@ -39,7 +39,11 @@ func (api *evaluationAPI) setProvider(provider FeatureProvider) error {
 		return errors.New("default provider cannot be set to nil")
 	}
 
+	// Initialize new default provider and shutdown the old one
+	// Provider update must be non-blocking, hence initialization & shutdown happens concurrently
+	api.initAndShutdown(provider, api.defaultProvider)
 	api.defaultProvider = provider
+
 	return nil
 }
 
@@ -60,7 +64,11 @@ func (api *evaluationAPI) setNamedProvider(clientName string, provider FeaturePr
 		return errors.New("provider cannot be set to nil")
 	}
 
+	// Initialize new default provider and shutdown the old one
+	// Provider update must be non-blocking, hence initialization & shutdown happens concurrently
+	api.initAndShutdown(provider, api.namedProviders[clientName])
 	api.namedProviders[clientName] = provider
+
 	return nil
 }
 
@@ -100,6 +108,20 @@ func (api *evaluationAPI) addHooks(hooks ...Hook) {
 	api.hks = append(api.hks, hooks...)
 }
 
+func (api *evaluationAPI) shutdown() {
+	v, ok := api.defaultProvider.(StateHandler)
+	if ok {
+		v.Shutdown()
+	}
+
+	for _, provider := range api.namedProviders {
+		v, ok = provider.(StateHandler)
+		if ok {
+			v.Shutdown()
+		}
+	}
+}
+
 func (api *evaluationAPI) getHooks() []Hook {
 	api.mu.RLock()
 	defer api.mu.RUnlock()
@@ -122,3 +144,22 @@ func (api *evaluationAPI) forTransaction(clientName string) (FeatureProvider, []
 
 	return provider, api.hks, api.evalCtx
 }
+
+// initAndShutdown is a helper to initialise new FeatureProvider and shutdown old FeatureProvider.
+// Operation happens concurrently.
+func (api *evaluationAPI) initAndShutdown(newProvider FeatureProvider, oldProvider FeatureProvider) {
+	go func() {
+		v, ok := newProvider.(StateHandler)
+		if ok {
+			v.Init(api.evalCtx)
+		}
+
+		// oldProvider can be nil
+		if oldProvider != nil {
+			v, ok = oldProvider.(StateHandler)
+			if ok {
+				v.Shutdown()
+			}
+		}
+	}()
+}
diff --git a/pkg/openfeature/noop_provider.go b/pkg/openfeature/noop_provider.go
index 85df437..12a7255 100644
--- a/pkg/openfeature/noop_provider.go
+++ b/pkg/openfeature/noop_provider.go
@@ -3,7 +3,8 @@ package openfeature
 import "context"
 
 // NoopProvider implements the FeatureProvider interface and provides functions for evaluating flags
-type NoopProvider struct{}
+type NoopProvider struct {
+}
 
 // Metadata returns the metadata of the provider
 func (e NoopProvider) Metadata() Metadata {
diff --git a/pkg/openfeature/openfeature.go b/pkg/openfeature/openfeature.go
index 7f996de..5d71f28 100644
--- a/pkg/openfeature/openfeature.go
+++ b/pkg/openfeature/openfeature.go
@@ -17,12 +17,14 @@ func initSingleton() {
 	api = newEvaluationAPI()
 }
 
-// SetProvider sets the default provider.
+// SetProvider sets the default provider. Provider initialization is asynchronous and status can be checked from
+// provider status
 func SetProvider(provider FeatureProvider) error {
 	return api.setProvider(provider)
 }
 
-// SetNamedProvider sets a provider mapped to the given Client name.
+// SetNamedProvider sets a provider mapped to the given Client name. Provider initialization is asynchronous and
+// status can be checked from provider status
 func SetNamedProvider(clientName string, provider FeatureProvider) error {
 	return api.setNamedProvider(clientName, provider)
 }
@@ -47,6 +49,11 @@ func AddHooks(hooks ...Hook) {
 	api.addHooks(hooks...)
 }
 
+// Shutdown active providers
+func Shutdown() {
+	api.shutdown()
+}
+
 // getProvider returns the default provider of the API. Intended to be used by tests
 func getProvider() FeatureProvider {
 	return api.getProvider()
diff --git a/pkg/openfeature/provider.go b/pkg/openfeature/provider.go
index 7bc9009..bcf3af7 100644
--- a/pkg/openfeature/provider.go
+++ b/pkg/openfeature/provider.go
@@ -23,6 +23,10 @@ const (
 	// ErrorReason - the resolved value was the result of an error.
 	ErrorReason Reason = "ERROR"
 
+	NotReadyState State = "NOT_READY"
+	ReadyState    State = "READY"
+	ErrorState    State = "ERROR"
+
 	TargetingKey string = "targetingKey" // evaluation context map key. The targeting key uniquely identifies the subject (end-user, or client service) of a flag evaluation.
 )
 
@@ -34,7 +38,7 @@ type FlattenedContext map[string]interface{}
 type Reason string
 
 // FeatureProvider interface defines a set of functions that can be called in order to evaluate a flag.
-// vendors should implement
+// This should be implemented by flag management systems.
 type FeatureProvider interface {
 	Metadata() Metadata
 	BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail
@@ -45,6 +49,17 @@ type FeatureProvider interface {
 	Hooks() []Hook
 }
 
+// State represents the status of the provider
+type State string
+
+// StateHandler is the contract for initialization & shutdown.
+// FeatureProvider can opt in for this behavior by implementing the interface
+type StateHandler interface {
+	Init(evaluationContext EvaluationContext)
+	Shutdown()
+	Status() State
+}
+
 // ProviderResolutionDetail is a structure which contains a subset of the fields defined in the EvaluationDetail,
 // representing the result of the provider's flag resolution process
 // see https://github.com/open-feature/spec/blob/main/specification/types.md#resolution-details


Test patch:
diff --git a/pkg/openfeature/client_test.go b/pkg/openfeature/client_test.go
index 254f954..d02d8bd 100644
--- a/pkg/openfeature/client_test.go
+++ b/pkg/openfeature/client_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"reflect"
 	"testing"
+	"time"
 
 	"github.com/golang/mock/gomock"
 )
@@ -911,10 +912,13 @@ func TestSwitchingProvidersMidEvaluationCausesNoImpactToEvaluation(t *testing.T)
 	// set new provider during initial provider's Before hook
 	mockProvider1Hook.EXPECT().Before(gomock.Any(), gomock.Any(), gomock.Any()).
 		DoAndReturn(func(_ context.Context, _ HookContext, _ HookHints) (*EvaluationContext, error) {
-			err := SetProvider(mockProvider1)
+			err := SetProvider(mockProvider2)
 			if err != nil {
 				return nil, err
 			}
+
+			// wait for initialization
+			time.Sleep(200 * time.Millisecond)
 			return nil, nil
 		})
 
diff --git a/pkg/openfeature/evaluation_context_test.go b/pkg/openfeature/evaluation_context_test.go
index 9ad4059..6a9e626 100644
--- a/pkg/openfeature/evaluation_context_test.go
+++ b/pkg/openfeature/evaluation_context_test.go
@@ -2,10 +2,9 @@ package openfeature
 
 import (
 	"context"
+	"github.com/golang/mock/gomock"
 	"reflect"
 	"testing"
-
-	"github.com/golang/mock/gomock"
 )
 
 // The `evaluation context` structure MUST define an optional `targeting key` field of type string,
diff --git a/pkg/openfeature/hooks_mock_test.go b/pkg/openfeature/hooks_mock_test.go
index a8d33ae..b99a21e 100644
--- a/pkg/openfeature/hooks_mock_test.go
+++ b/pkg/openfeature/hooks_mock_test.go
@@ -6,34 +6,49 @@ package openfeature
 
 import (
 	context "context"
-	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
 )
 
-// MockHook is a mock of Hook interface
+// MockHook is a mock of Hook interface.
 type MockHook struct {
 	ctrl     *gomock.Controller
 	recorder *MockHookMockRecorder
 }
 
-// MockHookMockRecorder is the mock recorder for MockHook
+// MockHookMockRecorder is the mock recorder for MockHook.
 type MockHookMockRecorder struct {
 	mock *MockHook
 }
 
-// NewMockHook creates a new mock instance
+// NewMockHook creates a new mock instance.
 func NewMockHook(ctrl *gomock.Controller) *MockHook {
 	mock := &MockHook{ctrl: ctrl}
 	mock.recorder = &MockHookMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *MockHook) EXPECT() *MockHookMockRecorder {
 	return m.recorder
 }
 
-// Before mocks base method
+// After mocks base method.
+func (m *MockHook) After(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "After", ctx, hookContext, flagEvaluationDetails, hookHints)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// After indicates an expected call of After.
+func (mr *MockHookMockRecorder) After(ctx, hookContext, flagEvaluationDetails, hookHints interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "After", reflect.TypeOf((*MockHook)(nil).After), ctx, hookContext, flagEvaluationDetails, hookHints)
+}
+
+// Before mocks base method.
 func (m *MockHook) Before(ctx context.Context, hookContext HookContext, hookHints HookHints) (*EvaluationContext, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Before", ctx, hookContext, hookHints)
@@ -42,45 +57,31 @@ func (m *MockHook) Before(ctx context.Context, hookContext HookContext, hookHint
 	return ret0, ret1
 }
 
-// Before indicates an expected call of Before
+// Before indicates an expected call of Before.
 func (mr *MockHookMockRecorder) Before(ctx, hookContext, hookHints interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Before", reflect.TypeOf((*MockHook)(nil).Before), ctx, hookContext, hookHints)
 }
 
-// After mocks base method
-func (m *MockHook) After(ctx context.Context, hookContext HookContext, flagEvaluationDetails InterfaceEvaluationDetails, hookHints HookHints) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "After", ctx, hookContext, flagEvaluationDetails, hookHints)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// After indicates an expected call of After
-func (mr *MockHookMockRecorder) After(ctx, hookContext, flagEvaluationDetails, hookHints interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "After", reflect.TypeOf((*MockHook)(nil).After), ctx, hookContext, flagEvaluationDetails, hookHints)
-}
-
-// Error mocks base method
+// Error mocks base method.
 func (m *MockHook) Error(ctx context.Context, hookContext HookContext, err error, hookHints HookHints) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "Error", ctx, hookContext, err, hookHints)
 }
 
-// Error indicates an expected call of Error
+// Error indicates an expected call of Error.
 func (mr *MockHookMockRecorder) Error(ctx, hookContext, err, hookHints interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockHook)(nil).Error), ctx, hookContext, err, hookHints)
 }
 
-// Finally mocks base method
+// Finally mocks base method.
 func (m *MockHook) Finally(ctx context.Context, hookContext HookContext, hookHints HookHints) {
 	m.ctrl.T.Helper()
 	m.ctrl.Call(m, "Finally", ctx, hookContext, hookHints)
 }
 
-// Finally indicates an expected call of Finally
+// Finally indicates an expected call of Finally.
 func (mr *MockHookMockRecorder) Finally(ctx, hookContext, hookHints interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Finally", reflect.TypeOf((*MockHook)(nil).Finally), ctx, hookContext, hookHints)
diff --git a/pkg/openfeature/hooks_test.go b/pkg/openfeature/hooks_test.go
index e7044d5..a4c45a1 100644
--- a/pkg/openfeature/hooks_test.go
+++ b/pkg/openfeature/hooks_test.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"reflect"
 	"testing"
+	"time"
 
 	"github.com/golang/mock/gomock"
 
@@ -332,10 +333,11 @@ func TestRequirement_4_3_4(t *testing.T) {
 // The `after` stage MUST run after flag resolution occurs. It accepts a `hook context` (required),
 // `flag evaluation details` (required) and `hook hints` (optional). It has no return value.
 func TestRequirement_4_3_5(t *testing.T) {
-	defer t.Cleanup(initSingleton)
 	ctrl := gomock.NewController(t)
 
 	t.Run("after hook MUST run after flag resolution occurs", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
 		mockHook := NewMockHook(ctrl)
 		client := NewClient("test")
 		mockProvider := NewMockFeatureProvider(ctrl)
@@ -383,7 +385,6 @@ func TestRequirement_4_3_5(t *testing.T) {
 // resolution. It accepts `hook context` (required), `exception` representing what went wrong (required),
 // and `hook hints` (optional). It has no return value.
 func TestRequirement_4_3_6(t *testing.T) {
-	defer t.Cleanup(initSingleton)
 	ctrl := gomock.NewController(t)
 
 	flagKey := "foo"
@@ -392,6 +393,8 @@ func TestRequirement_4_3_6(t *testing.T) {
 	flatCtx := flattenContext(evalCtx)
 
 	t.Run("error hook MUST run when errors are encountered in the before stage", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
 		mockHook := NewMockHook(ctrl)
 		client := NewClient("test")
 		mockProvider := NewMockFeatureProvider(ctrl)
@@ -416,6 +419,8 @@ func TestRequirement_4_3_6(t *testing.T) {
 	})
 
 	t.Run("error hook MUST run when errors are encountered during flag evaluation", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
 		mockHook := NewMockHook(ctrl)
 		client := NewClient("test")
 		mockProvider := NewMockFeatureProvider(ctrl)
@@ -448,6 +453,8 @@ func TestRequirement_4_3_6(t *testing.T) {
 	})
 
 	t.Run("error hook MUST run when errors are encountered during flag evaluation", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
 		mockHook := NewMockHook(ctrl)
 		client := NewClient("test")
 		mockProvider := NewMockFeatureProvider(ctrl)
@@ -490,7 +497,6 @@ func TestRequirement_4_3_6(t *testing.T) {
 // The `finally` hook MUST run after the `before`, `after`, and `error` stages. It accepts a `hook context` (required)
 // and `hook hints` (optional). There is no return value.
 func TestRequirement_4_3_7(t *testing.T) {
-	defer t.Cleanup(initSingleton)
 	ctrl := gomock.NewController(t)
 
 	flagKey := "foo"
@@ -499,6 +505,8 @@ func TestRequirement_4_3_7(t *testing.T) {
 	flatCtx := flattenContext(evalCtx)
 
 	t.Run("finally hook MUST run after the before & after stages", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
 		mockHook := NewMockHook(ctrl)
 		client := NewClient("test")
 		mockProvider := NewMockFeatureProvider(ctrl)
@@ -524,6 +532,8 @@ func TestRequirement_4_3_7(t *testing.T) {
 	})
 
 	t.Run("finally hook MUST run after the error stage", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
 		mockHook := NewMockHook(ctrl)
 		client := NewClient("test")
 		mockProvider := NewMockFeatureProvider(ctrl)
@@ -742,7 +752,6 @@ func TestRequirement_4_4_2(t *testing.T) {
 // If an error occurs during the evaluation of `before` or `after` hooks, any remaining hooks in the `before` or `after`
 // stages MUST NOT be invoked.
 func TestRequirement_4_4_6(t *testing.T) {
-	defer t.Cleanup(initSingleton)
 	ctrl := gomock.NewController(t)
 
 	flagKey := "foo"
@@ -753,6 +762,8 @@ func TestRequirement_4_4_6(t *testing.T) {
 	t.Run(
 		"if an error occurs during the evaluation of before hooks, any remaining before hooks MUST NOT be invoked",
 		func(t *testing.T) {
+			defer t.Cleanup(initSingleton)
+
 			mockHook1 := NewMockHook(ctrl)
 			mockHook2 := NewMockHook(ctrl)
 			client := NewClient("test")
@@ -783,6 +794,8 @@ func TestRequirement_4_4_6(t *testing.T) {
 	t.Run(
 		"if an error occurs during the evaluation of after hooks, any remaining after hooks MUST NOT be invoked",
 		func(t *testing.T) {
+			defer t.Cleanup(initSingleton)
+
 			mockHook1 := NewMockHook(ctrl)
 			mockHook2 := NewMockHook(ctrl)
 			client := NewClient("test")
@@ -880,7 +893,7 @@ func TestRequirement_4_5_2(t *testing.T) {
 		if err != nil {
 			t.Errorf("error setting up provider %v", err)
 		}
-
+		
 		mockProvider.EXPECT().Hooks().AnyTimes()
 
 		hookHints := NewHookHints(map[string]interface{}{"foo": "bar"})
@@ -909,6 +922,9 @@ func TestRequirement_4_5_2(t *testing.T) {
 			t.Errorf("error setting up provider %v", err)
 		}
 
+		// wait for initialization
+		time.Sleep(200 * time.Millisecond)
+
 		mockProvider.EXPECT().Hooks().AnyTimes()
 
 		hookHints := NewHookHints(map[string]interface{}{"foo": "bar"})
diff --git a/pkg/openfeature/openfeature_test.go b/pkg/openfeature/openfeature_test.go
index 3199d1b..98da6bb 100644
--- a/pkg/openfeature/openfeature_test.go
+++ b/pkg/openfeature/openfeature_test.go
@@ -1,12 +1,12 @@
 package openfeature
 
 import (
-	"reflect"
-	"testing"
-
 	"github.com/go-logr/logr"
 	"github.com/golang/mock/gomock"
 	"github.com/open-feature/go-sdk/pkg/openfeature/internal"
+	"reflect"
+	"testing"
+	"time"
 )
 
 // The `API`, and any state it maintains SHOULD exist as a global singleton,
@@ -31,7 +31,7 @@ func TestRequirement_1_1_1(t *testing.T) {
 
 // The `API` MUST provide a function to set the default `provider`,
 // which accepts an API-conformant `provider` implementation.
-func TestRequirement_1_1_2(t *testing.T) {
+func TestRequirement_1_1_2_1(t *testing.T) {
 	defer t.Cleanup(initSingleton)
 	ctrl := gomock.NewController(t)
 
@@ -49,6 +49,136 @@ func TestRequirement_1_1_2(t *testing.T) {
 	}
 }
 
+// The provider mutator function MUST invoke the initialize function on the newly registered provider before using it
+// to resolve flag values.
+func TestRequirement_1_1_2_2(t *testing.T) {
+	t.Run("default provider", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
+		provider, initSem, _ := setupProviderWithSemaphores()
+
+		err := SetProvider(provider)
+		if err != nil {
+			t.Errorf("error setting up provider %v", err)
+		}
+
+		select {
+		// short enough wait time, but not too long
+		case <-time.After(100 * time.Millisecond):
+			t.Errorf("initialization not invoked with provider registration")
+		case <-initSem:
+			break
+		}
+
+		if !reflect.DeepEqual(provider, getProvider()) {
+			t.Errorf("provider not updated to the one set")
+		}
+	})
+
+	t.Run("named provider", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
+		provider, initSem, _ := setupProviderWithSemaphores()
+
+		var client = "client"
+
+		err := SetNamedProvider(client, provider)
+		if err != nil {
+			t.Errorf("error setting up provider %v", err)
+		}
+
+		select {
+		// short enough wait time, but not too long
+		case <-time.After(100 * time.Millisecond):
+			t.Errorf("initialization not invoked with provider registration")
+		case <-initSem:
+			break
+		}
+
+		if !reflect.DeepEqual(provider, getNamedProviders()[client]) {
+			t.Errorf("provider not updated to the one set")
+		}
+	})
+}
+
+// The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no
+// longer being used to resolve flag values.
+func TestRequirement_1_1_2_3(t *testing.T) {
+	t.Run("default provider", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
+		provider, initSem, shutdownSem := setupProviderWithSemaphores()
+
+		err := SetProvider(provider)
+		if err != nil {
+			t.Errorf("error setting up provider %v", err)
+		}
+
+		select {
+		// short enough wait time, but not too long
+		case <-time.After(100 * time.Millisecond):
+			t.Errorf("initialization not invoked with provider registration")
+		case <-initSem:
+			break
+		}
+
+		providerOverride, _, _ := setupProviderWithSemaphores()
+
+		err = SetProvider(providerOverride)
+		if err != nil {
+			t.Errorf("error setting up provider %v", err)
+		}
+
+		select {
+		// short enough wait time, but not too long
+		case <-time.After(100 * time.Millisecond):
+			t.Errorf("shutdown not invoked for old default provider when registering new provider")
+		case <-shutdownSem:
+			break
+		}
+
+	})
+
+	t.Run("named provider", func(t *testing.T) {
+		defer t.Cleanup(initSingleton)
+
+		provider, initSem, shutdownSem := setupProviderWithSemaphores()
+
+		var client = "client"
+
+		err := SetNamedProvider(client, provider)
+		if err != nil {
+			t.Errorf("error setting up provider %v", err)
+		}
+
+		select {
+		// short enough wait time, but not too long
+		case <-time.After(100 * time.Millisecond):
+			t.Errorf("initialization not invoked with provider registration")
+		case <-initSem:
+			break
+		}
+		
+		providerOverride, _, _ := setupProviderWithSemaphores()
+
+		err = SetNamedProvider(client, providerOverride)
+		if err != nil {
+			t.Errorf("error setting up provider %v", err)
+		}
+
+		select {
+		// short enough wait time, but not too long
+		case <-time.After(100 * time.Millisecond):
+			t.Errorf("shutdown not invoked for old default provider when registering new provider")
+		case <-shutdownSem:
+			break
+		}
+	})
+}
+
+// The provider mutator function MUST invoke the shutdown function on the previously registered provider once it's no
+// longer being used to resolve flag values.
+
 // The `API` MUST provide a function to bind a given `provider` to one or more client `name`s.
 // If the client-name already has a bound provider, it is overwritten with the new mapping.
 func TestRequirement_1_1_3(t *testing.T) {
@@ -168,6 +298,37 @@ func TestRequirement_1_1_7(t *testing.T) {
 	use(f) // to avoid the declared and not used error
 }
 
+// The API MUST define a mechanism to propagate a shutdown request to active providers.
+func TestRequirement_1_6_1(t *testing.T) {
+	defer t.Cleanup(initSingleton)
+
+	provider, initSem, shutdownSem := setupProviderWithSemaphores()
+
+	// Setup provider and wait for initialization done
+	err := SetProvider(provider)
+	if err != nil {
+		t.Errorf("error setting up provider %v", err)
+	}
+
+	select {
+	// short enough wait time, but not too long
+	case <-time.After(100 * time.Millisecond):
+		t.Errorf("intialization timeout")
+	case <-initSem:
+		break
+	}
+
+	Shutdown()
+
+	select {
+	// short enough wait time, but not too long
+	case <-time.After(100 * time.Millisecond):
+		t.Errorf("shutdown not invoked")
+	case <-shutdownSem:
+		break
+	}
+}
+
 // Non-spec bound validations
 
 // If there is no client name bound provider, then return the default provider
@@ -223,3 +384,34 @@ func use(vals ...interface{}) {
 		_ = val
 	}
 }
+
+func setupProviderWithSemaphores() (struct {
+	FeatureProvider
+	StateHandler
+}, chan interface{}, chan interface{}) {
+	intiSem := make(chan interface{}, 1)
+	shutdownSem := make(chan interface{}, 1)
+
+	sh := &stateHandlerForTests{
+		// Semaphore must be invoked
+		initF: func(e EvaluationContext) {
+			intiSem <- ""
+		},
+		// Semaphore must be invoked
+		shutdownF: func() {
+			shutdownSem <- ""
+		},
+		State: ReadyState,
+	}
+
+	// Derive provider with initialize & shutdown support
+	provider := struct {
+		FeatureProvider
+		StateHandler
+	}{
+		FeatureProvider: NoopProvider{},
+		StateHandler:    sh,
+	}
+
+	return provider, intiSem, shutdownSem
+}
diff --git a/pkg/openfeature/provider_mock_test.go b/pkg/openfeature/provider_mock_test.go
index 0f69d09..d202147 100644
--- a/pkg/openfeature/provider_mock_test.go
+++ b/pkg/openfeature/provider_mock_test.go
@@ -6,48 +6,35 @@ package openfeature
 
 import (
 	context "context"
-	gomock "github.com/golang/mock/gomock"
 	reflect "reflect"
+
+	gomock "github.com/golang/mock/gomock"
 )
 
-// MockFeatureProvider is a mock of FeatureProvider interface
+// MockFeatureProvider is a mock of FeatureProvider interface.
 type MockFeatureProvider struct {
 	ctrl     *gomock.Controller
 	recorder *MockFeatureProviderMockRecorder
 }
 
-// MockFeatureProviderMockRecorder is the mock recorder for MockFeatureProvider
+// MockFeatureProviderMockRecorder is the mock recorder for MockFeatureProvider.
 type MockFeatureProviderMockRecorder struct {
 	mock *MockFeatureProvider
 }
 
-// NewMockFeatureProvider creates a new mock instance
+// NewMockFeatureProvider creates a new mock instance.
 func NewMockFeatureProvider(ctrl *gomock.Controller) *MockFeatureProvider {
 	mock := &MockFeatureProvider{ctrl: ctrl}
 	mock.recorder = &MockFeatureProviderMockRecorder{mock}
 	return mock
 }
 
-// EXPECT returns an object that allows the caller to indicate expected use
+// EXPECT returns an object that allows the caller to indicate expected use.
 func (m *MockFeatureProvider) EXPECT() *MockFeatureProviderMockRecorder {
 	return m.recorder
 }
 
-// Metadata mocks base method
-func (m *MockFeatureProvider) Metadata() Metadata {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Metadata")
-	ret0, _ := ret[0].(Metadata)
-	return ret0
-}
-
-// Metadata indicates an expected call of Metadata
-func (mr *MockFeatureProviderMockRecorder) Metadata() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockFeatureProvider)(nil).Metadata))
-}
-
-// BooleanEvaluation mocks base method
+// BooleanEvaluation mocks base method.
 func (m *MockFeatureProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx FlattenedContext) BoolResolutionDetail {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "BooleanEvaluation", ctx, flag, defaultValue, evalCtx)
@@ -55,41 +42,41 @@ func (m *MockFeatureProvider) BooleanEvaluation(ctx context.Context, flag string
 	return ret0
 }
 
-// BooleanEvaluation indicates an expected call of BooleanEvaluation
+// BooleanEvaluation indicates an expected call of BooleanEvaluation.
 func (mr *MockFeatureProviderMockRecorder) BooleanEvaluation(ctx, flag, defaultValue, evalCtx interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BooleanEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).BooleanEvaluation), ctx, flag, defaultValue, evalCtx)
 }
 
-// StringEvaluation mocks base method
-func (m *MockFeatureProvider) StringEvaluation(ctx context.Context, flag, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail {
+// FloatEvaluation mocks base method.
+func (m *MockFeatureProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "StringEvaluation", ctx, flag, defaultValue, evalCtx)
-	ret0, _ := ret[0].(StringResolutionDetail)
+	ret := m.ctrl.Call(m, "FloatEvaluation", ctx, flag, defaultValue, evalCtx)
+	ret0, _ := ret[0].(FloatResolutionDetail)
 	return ret0
 }
 
-// StringEvaluation indicates an expected call of StringEvaluation
-func (mr *MockFeatureProviderMockRecorder) StringEvaluation(ctx, flag, defaultValue, evalCtx interface{}) *gomock.Call {
+// FloatEvaluation indicates an expected call of FloatEvaluation.
+func (mr *MockFeatureProviderMockRecorder) FloatEvaluation(ctx, flag, defaultValue, evalCtx interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).StringEvaluation), ctx, flag, defaultValue, evalCtx)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).FloatEvaluation), ctx, flag, defaultValue, evalCtx)
 }
 
-// FloatEvaluation mocks base method
-func (m *MockFeatureProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx FlattenedContext) FloatResolutionDetail {
+// Hooks mocks base method.
+func (m *MockFeatureProvider) Hooks() []Hook {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "FloatEvaluation", ctx, flag, defaultValue, evalCtx)
-	ret0, _ := ret[0].(FloatResolutionDetail)
+	ret := m.ctrl.Call(m, "Hooks")
+	ret0, _ := ret[0].([]Hook)
 	return ret0
 }
 
-// FloatEvaluation indicates an expected call of FloatEvaluation
-func (mr *MockFeatureProviderMockRecorder) FloatEvaluation(ctx, flag, defaultValue, evalCtx interface{}) *gomock.Call {
+// Hooks indicates an expected call of Hooks.
+func (mr *MockFeatureProviderMockRecorder) Hooks() *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FloatEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).FloatEvaluation), ctx, flag, defaultValue, evalCtx)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hooks", reflect.TypeOf((*MockFeatureProvider)(nil).Hooks))
 }
 
-// IntEvaluation mocks base method
+// IntEvaluation mocks base method.
 func (m *MockFeatureProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx FlattenedContext) IntResolutionDetail {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "IntEvaluation", ctx, flag, defaultValue, evalCtx)
@@ -97,13 +84,27 @@ func (m *MockFeatureProvider) IntEvaluation(ctx context.Context, flag string, de
 	return ret0
 }
 
-// IntEvaluation indicates an expected call of IntEvaluation
+// IntEvaluation indicates an expected call of IntEvaluation.
 func (mr *MockFeatureProviderMockRecorder) IntEvaluation(ctx, flag, defaultValue, evalCtx interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).IntEvaluation), ctx, flag, defaultValue, evalCtx)
 }
 
-// ObjectEvaluation mocks base method
+// Metadata mocks base method.
+func (m *MockFeatureProvider) Metadata() Metadata {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Metadata")
+	ret0, _ := ret[0].(Metadata)
+	return ret0
+}
+
+// Metadata indicates an expected call of Metadata.
+func (mr *MockFeatureProviderMockRecorder) Metadata() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockFeatureProvider)(nil).Metadata))
+}
+
+// ObjectEvaluation mocks base method.
 func (m *MockFeatureProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx FlattenedContext) InterfaceResolutionDetail {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "ObjectEvaluation", ctx, flag, defaultValue, evalCtx)
@@ -111,22 +112,120 @@ func (m *MockFeatureProvider) ObjectEvaluation(ctx context.Context, flag string,
 	return ret0
 }
 
-// ObjectEvaluation indicates an expected call of ObjectEvaluation
+// ObjectEvaluation indicates an expected call of ObjectEvaluation.
 func (mr *MockFeatureProviderMockRecorder) ObjectEvaluation(ctx, flag, defaultValue, evalCtx interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).ObjectEvaluation), ctx, flag, defaultValue, evalCtx)
 }
 
-// Hooks mocks base method
-func (m *MockFeatureProvider) Hooks() []Hook {
+// StringEvaluation mocks base method.
+func (m *MockFeatureProvider) StringEvaluation(ctx context.Context, flag, defaultValue string, evalCtx FlattenedContext) StringResolutionDetail {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Hooks")
-	ret0, _ := ret[0].([]Hook)
+	ret := m.ctrl.Call(m, "StringEvaluation", ctx, flag, defaultValue, evalCtx)
+	ret0, _ := ret[0].(StringResolutionDetail)
 	return ret0
 }
 
-// Hooks indicates an expected call of Hooks
-func (mr *MockFeatureProviderMockRecorder) Hooks() *gomock.Call {
+// StringEvaluation indicates an expected call of StringEvaluation.
+func (mr *MockFeatureProviderMockRecorder) StringEvaluation(ctx, flag, defaultValue, evalCtx interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hooks", reflect.TypeOf((*MockFeatureProvider)(nil).Hooks))
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringEvaluation", reflect.TypeOf((*MockFeatureProvider)(nil).StringEvaluation), ctx, flag, defaultValue, evalCtx)
+}
+
+// MockStateHandler is a mock of StateHandler interface.
+type MockStateHandler struct {
+	ctrl     *gomock.Controller
+	recorder *MockStateHandlerMockRecorder
+}
+
+// MockStateHandlerMockRecorder is the mock recorder for MockStateHandler.
+type MockStateHandlerMockRecorder struct {
+	mock *MockStateHandler
+}
+
+// NewMockStateHandler creates a new mock instance.
+func NewMockStateHandler(ctrl *gomock.Controller) *MockStateHandler {
+	mock := &MockStateHandler{ctrl: ctrl}
+	mock.recorder = &MockStateHandlerMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockStateHandler) EXPECT() *MockStateHandlerMockRecorder {
+	return m.recorder
+}
+
+// Init mocks base method.
+func (m *MockStateHandler) Init(evaluationContext EvaluationContext) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Init", evaluationContext)
+}
+
+// Init indicates an expected call of Init.
+func (mr *MockStateHandlerMockRecorder) Init(evaluationContext interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockStateHandler)(nil).Init), evaluationContext)
+}
+
+// Shutdown mocks base method.
+func (m *MockStateHandler) Shutdown() {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "Shutdown")
+}
+
+// Shutdown indicates an expected call of Shutdown.
+func (mr *MockStateHandlerMockRecorder) Shutdown() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockStateHandler)(nil).Shutdown))
+}
+
+// Status mocks base method.
+func (m *MockStateHandler) Status() State {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "Status")
+	ret0, _ := ret[0].(State)
+	return ret0
+}
+
+// Status indicates an expected call of Status.
+func (mr *MockStateHandlerMockRecorder) Status() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockStateHandler)(nil).Status))
+}
+
+// MockState is a mock of State interface.
+type MockState struct {
+	ctrl     *gomock.Controller
+	recorder *MockStateMockRecorder
+}
+
+// MockStateMockRecorder is the mock recorder for MockState.
+type MockStateMockRecorder struct {
+	mock *MockState
+}
+
+// NewMockState creates a new mock instance.
+func NewMockState(ctrl *gomock.Controller) *MockState {
+	mock := &MockState{ctrl: ctrl}
+	mock.recorder = &MockStateMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockState) EXPECT() *MockStateMockRecorder {
+	return m.recorder
+}
+
+// get mocks base method.
+func (m *MockState) get() string {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "get")
+	ret0, _ := ret[0].(string)
+	return ret0
+}
+
+// get indicates an expected call of get.
+func (mr *MockStateMockRecorder) get() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "get", reflect.TypeOf((*MockState)(nil).get))
 }
diff --git a/pkg/openfeature/util_test.go b/pkg/openfeature/util_test.go
new file mode 100644
index 0000000..0f5522e
--- /dev/null
+++ b/pkg/openfeature/util_test.go
@@ -0,0 +1,21 @@
+package openfeature
+
+// Test Utils
+
+type stateHandlerForTests struct {
+	initF     func(e EvaluationContext)
+	shutdownF func()
+	State
+}
+
+func (s *stateHandlerForTests) Init(e EvaluationContext) {
+	s.initF(e)
+}
+
+func (s *stateHandlerForTests) Shutdown() {
+	s.shutdownF()
+}
+
+func (s *stateHandlerForTests) Status() State {
+	return s.State
+}