Skip to content

Commit

Permalink
isolate interfaces from SDK
Browse files Browse the repository at this point in the history
Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>
  • Loading branch information
Kavindu-Dodan committed Apr 29, 2024
1 parent 2cf5717 commit 0e316a3
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 462 deletions.
252 changes: 15 additions & 237 deletions openfeature/api.go
Original file line number Diff line number Diff line change
@@ -1,246 +1,24 @@
package openfeature

import (
"errors"
"fmt"
"sync"

"github.com/go-logr/logr"
"github.com/open-feature/go-sdk/openfeature/internal"
"golang.org/x/exp/maps"
)

// evaluationAPI wraps OpenFeature evaluation API functionalities
type evaluationAPI struct {
defaultProvider FeatureProvider
namedProviders map[string]FeatureProvider
hks []Hook
apiCtx EvaluationContext
logger logr.Logger
mu sync.RWMutex
eventExecutor *eventExecutor
}

// newEvaluationAPI is a helper to generate an API. Used internally
func newEvaluationAPI() evaluationAPI {
logger := logr.New(internal.Logger{})

return evaluationAPI{
defaultProvider: NoopProvider{},
namedProviders: map[string]FeatureProvider{},
hks: []Hook{},
apiCtx: EvaluationContext{},
logger: logger,
mu: sync.RWMutex{},
eventExecutor: newEventExecutor(logger),
}
}

// setProvider sets the default FeatureProvider of the evaluationAPI.
// Returns an error if provider registration cause an error
func (api *evaluationAPI) setProvider(provider FeatureProvider, async bool) error {
api.mu.Lock()
defer api.mu.Unlock()

if provider == nil {
return errors.New("default provider cannot be set to nil")
}

oldProvider := api.defaultProvider
api.defaultProvider = provider

err := api.initNewAndShutdownOld(provider, oldProvider, async)
if err != nil {
return err
}

err = api.eventExecutor.registerDefaultProvider(provider)
if err != nil {
return err
}

return nil
}

// getProvider returns the default FeatureProvider
func (api *evaluationAPI) getProvider() FeatureProvider {
api.mu.RLock()
defer api.mu.RUnlock()

return api.defaultProvider
}

// setProvider sets a provider with client name. Returns an error if FeatureProvider is nil
func (api *evaluationAPI) setNamedProvider(clientName string, provider FeatureProvider, async bool) error {
api.mu.Lock()
defer api.mu.Unlock()

if provider == nil {
return errors.New("provider cannot be set to nil")
}

// Initialize new named provider and shutdown the old one
// Provider update must be non-blocking, hence initialization & shutdown happens concurrently
oldProvider := api.namedProviders[clientName]
api.namedProviders[clientName] = provider

err := api.initNewAndShutdownOld(provider, oldProvider, async)
if err != nil {
return err
}

err = api.eventExecutor.registerNamedEventingProvider(clientName, provider)
if err != nil {
return err
}

return nil
}

// getNamedProviders returns named providers map.
func (api *evaluationAPI) getNamedProviders() map[string]FeatureProvider {
api.mu.RLock()
defer api.mu.RUnlock()

return api.namedProviders
}

func (api *evaluationAPI) setEvaluationContext(apiCtx EvaluationContext) {
api.mu.Lock()
defer api.mu.Unlock()

api.apiCtx = apiCtx
}

func (api *evaluationAPI) setLogger(l logr.Logger) {
api.mu.Lock()
defer api.mu.Unlock()

api.logger = l
api.eventExecutor.updateLogger(l)
}

func (api *evaluationAPI) getLogger() logr.Logger {
api.mu.RLock()
defer api.mu.RUnlock()

return api.logger
type API interface {
SetProvider(provider FeatureProvider, async bool) error
SetNamedProvider(clientName string, provider FeatureProvider, async bool) error
SetEvaluationContext(apiCtx EvaluationContext)
AddHooks(hooks ...Hook)
Shutdown()
}

func (api *evaluationAPI) addHooks(hooks ...Hook) {
api.mu.Lock()
defer api.mu.Unlock()

api.hks = append(api.hks, hooks...)
type Eventing interface {
APIEvent
ClientEvent
}

func (api *evaluationAPI) shutdown() {
api.mu.Lock()
defer api.mu.Unlock()

v, ok := api.defaultProvider.(StateHandler)
if ok {
v.Shutdown()
}

for _, provider := range api.namedProviders {
v, ok = provider.(StateHandler)
if ok {
v.Shutdown()
}
}
type APIEvent interface {
AddHandler(eventType EventType, callback EventCallback)
RemoveHandler(eventType EventType, callback EventCallback)
}

func (api *evaluationAPI) getHooks() []Hook {
api.mu.RLock()
defer api.mu.RUnlock()

return api.hks
}

// forTransaction is a helper to retrieve transaction(flag evaluation) scoped operators.
// Returns the default FeatureProvider if no provider mapping exist for the given client name.
func (api *evaluationAPI) forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) {
api.mu.RLock()
defer api.mu.RUnlock()

var provider FeatureProvider

provider = api.namedProviders[clientName]
if provider == nil {
provider = api.defaultProvider
}

return provider, api.hks, api.apiCtx
}

// initNewAndShutdownOld is a helper to initialise new FeatureProvider and shutdown the old FeatureProvider.
func (api *evaluationAPI) initNewAndShutdownOld(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.triggerEvent(event, newProvider)
}(api.eventExecutor, api.apiCtx)
} else {
event, err := initializer(newProvider, api.apiCtx)
api.eventExecutor.triggerEvent(event, newProvider)
if err != nil {
return err
}
}

v, ok := oldProvider.(StateHandler)

// oldProvider can be nil or without state handling capability
if oldProvider == nil || !ok {
return nil
}

// check for multiple bindings
if oldProvider == api.defaultProvider || contains(oldProvider, maps.Values(api.namedProviders)) {
return nil
}

go func(forShutdown StateHandler) {
forShutdown.Shutdown()
}(v)

return nil
}

// initializer is a helper to execute provider initialization and generate appropriate event for the initialization
// It also returns an error if the initialization resulted in an error
func initializer(provider FeatureProvider, apiCtx EvaluationContext) (Event, error) {
var event = Event{
ProviderName: provider.Metadata().Name,
EventType: ProviderReady,
ProviderEventDetails: ProviderEventDetails{
Message: "Provider initialization successful",
},
}

handler, ok := provider.(StateHandler)
if !ok {
// Note - a provider without state handling capability can be assumed to be ready immediately.
return event, nil
}

err := handler.Init(apiCtx)
if err != nil {
event.EventType = ProviderError
event.Message = fmt.Sprintf("Provider initialization error, %v", err)
}

return event, err
}

func contains(provider FeatureProvider, in []FeatureProvider) bool {
for _, p := range in {
if provider == p {
return true
}
}

return false
type ClientEvent interface {
RegisterClientHandler(clientName string, t EventType, c EventCallback)
RemoveClientHandler(name string, t EventType, c EventCallback)
}
34 changes: 20 additions & 14 deletions openfeature/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ func (cm ClientMetadata) Name() string {

// Client implements the behaviour required of an openfeature client
type Client struct {
mx sync.RWMutex
api evalAPI
clientEventing ClientEvent
metadata ClientMetadata
hooks []Hook
evaluationContext EvaluationContext
logger func() logr.Logger
logger logr.Logger

mx sync.RWMutex
}

// interface guard to ensure that Client implements IClient
Expand All @@ -69,18 +72,21 @@ var _ IClient = (*Client)(nil)
// NewClient returns a new Client. Name is a unique identifier for this client
func NewClient(name string) *Client {
return &Client{
api: api,
clientEventing: eventing,
logger: logger,

metadata: ClientMetadata{name: name},
hooks: []Hook{},
evaluationContext: EvaluationContext{},
logger: globalLogger,
}
}

// WithLogger sets the logger of the client
func (c *Client) WithLogger(l logr.Logger) *Client {
c.mx.Lock()
defer c.mx.Unlock()
c.logger = func() logr.Logger { return l }
c.logger = l

Check warning on line 89 in openfeature/client.go

View check run for this annotation

Codecov / codecov/patch

openfeature/client.go#L89

Added line #L89 was not covered by tests
return c
}

Expand All @@ -100,12 +106,12 @@ func (c *Client) AddHooks(hooks ...Hook) {

// AddHandler allows to add Client level event handler
func (c *Client) AddHandler(eventType EventType, callback EventCallback) {
addClientHandler(c.metadata.Name(), eventType, callback)
c.clientEventing.RegisterClientHandler(c.metadata.Name(), eventType, callback)
}

// RemoveHandler allows to remove Client level event handler
func (c *Client) RemoveHandler(eventType EventType, callback EventCallback) {
removeClientHandler(c.metadata.Name(), eventType, callback)
c.clientEventing.RemoveClientHandler(c.metadata.Name(), eventType, callback)
}

// SetEvaluationContext sets the client's evaluation context
Expand Down Expand Up @@ -405,7 +411,7 @@ func (c *Client) BooleanValueDetails(ctx context.Context, flag string, defaultVa
value, ok := evalDetails.Value.(bool)
if !ok {
err := errors.New("evaluated value is not a boolean")
c.logger().Error(
c.logger.Error(

Check warning on line 414 in openfeature/client.go

View check run for this annotation

Codecov / codecov/patch

openfeature/client.go#L414

Added line #L414 was not covered by tests
err, "invalid flag resolution type", "expectedType", "boolean",
"gotType", fmt.Sprintf("%T", evalDetails.Value),
)
Expand Down Expand Up @@ -453,7 +459,7 @@ func (c *Client) StringValueDetails(ctx context.Context, flag string, defaultVal
value, ok := evalDetails.Value.(string)
if !ok {
err := errors.New("evaluated value is not a string")
c.logger().Error(
c.logger.Error(

Check warning on line 462 in openfeature/client.go

View check run for this annotation

Codecov / codecov/patch

openfeature/client.go#L462

Added line #L462 was not covered by tests
err, "invalid flag resolution type", "expectedType", "string",
"gotType", fmt.Sprintf("%T", evalDetails.Value),
)
Expand Down Expand Up @@ -501,7 +507,7 @@ func (c *Client) FloatValueDetails(ctx context.Context, flag string, defaultValu
value, ok := evalDetails.Value.(float64)
if !ok {
err := errors.New("evaluated value is not a float64")
c.logger().Error(
c.logger.Error(

Check warning on line 510 in openfeature/client.go

View check run for this annotation

Codecov / codecov/patch

openfeature/client.go#L510

Added line #L510 was not covered by tests
err, "invalid flag resolution type", "expectedType", "float64",
"gotType", fmt.Sprintf("%T", evalDetails.Value),
)
Expand Down Expand Up @@ -549,7 +555,7 @@ func (c *Client) IntValueDetails(ctx context.Context, flag string, defaultValue
value, ok := evalDetails.Value.(int64)
if !ok {
err := errors.New("evaluated value is not an int64")
c.logger().Error(
c.logger.Error(

Check warning on line 558 in openfeature/client.go

View check run for this annotation

Codecov / codecov/patch

openfeature/client.go#L558

Added line #L558 was not covered by tests
err, "invalid flag resolution type", "expectedType", "int64",
"gotType", fmt.Sprintf("%T", evalDetails.Value),
)
Expand Down Expand Up @@ -685,7 +691,7 @@ func (c *Client) evaluate(
}

// ensure that the same provider & hooks are used across this transaction to avoid unexpected behaviour
provider, globalHooks, globalCtx := forTransaction(c.metadata.name)
provider, globalHooks, globalCtx := c.api.ForEvaluation(c.metadata.name)

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
Expand All @@ -708,7 +714,7 @@ func (c *Client) evaluate(
evalCtx, err = c.beforeHooks(ctx, hookCtx, apiClientInvocationProviderHooks, evalCtx, options)
hookCtx.evaluationContext = evalCtx
if err != nil {
c.logger().Error(
c.logger.Error(
err, "before hook", "flag", flag, "defaultValue", defaultValue,
"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(),
)
Expand Down Expand Up @@ -746,7 +752,7 @@ func (c *Client) evaluate(

err = resolution.Error()
if err != nil {
c.logger().Error(
c.logger.Error(
err, "flag resolution", "flag", flag, "defaultValue", defaultValue,
"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(), "errorCode", err,
"errMessage", resolution.ResolutionError.message,
Expand All @@ -761,7 +767,7 @@ func (c *Client) evaluate(
evalDetails.ResolutionDetail = resolution.ResolutionDetail()

if err := c.afterHooks(ctx, hookCtx, providerInvocationClientApiHooks, evalDetails, options); err != nil {
c.logger().Error(
c.logger.Error(
err, "after hook", "flag", flag, "defaultValue", defaultValue,
"evaluationContext", evalCtx, "evaluationOptions", options, "type", flagType.String(),
)
Expand Down

0 comments on commit 0e316a3

Please sign in to comment.