Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement domain scoping #261

Merged
merged 28 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
25889a6
added support for domains
gunishmatta Mar 29, 2024
ebd1e4a
added support for domains
gunishmatta Mar 30, 2024
b3a6b61
chore(deps): update actions/setup-go action to v5 (#237)
renovate[bot] Apr 2, 2024
fa84bd9
chore(deps): update cyclonedx/gh-gomod-generate-sbom action to v2 (#179)
renovate[bot] Apr 2, 2024
f0440a3
Update openfeature/client.go
gunishmatta Apr 25, 2024
44ac3ad
Update openfeature/client.go
gunishmatta Apr 25, 2024
84d7dd4
code review changes
gunishmatta Apr 25, 2024
2bcb40f
code review changes
gunishmatta Apr 25, 2024
0f0411a
code review changes
gunishmatta Apr 25, 2024
5c61d0a
code review changes
gunishmatta Apr 25, 2024
7fffad3
feat: Added domain scoping #261
hairyhenderson Apr 9, 2024
69985f6
chore(main): release 1.11.0 (#254)
github-actions[bot] Apr 9, 2024
2e3fe32
chore: bump Go to version 1.20 (#255)
odubajDT Apr 9, 2024
9785fac
code review changes
gunishmatta Apr 25, 2024
11235b0
code review changes
gunishmatta Apr 25, 2024
22e5dba
code review changes
gunishmatta Apr 25, 2024
7427db9
code review changes
gunishmatta May 3, 2024
543f053
fix(deps): update module github.com/cucumber/godog to v0.14.1 (#267)
renovate[bot] Apr 29, 2024
617110a
chore(deps): update goreleaser/goreleaser-action action to v5 (#219)
renovate[bot] Apr 29, 2024
10661be
chore(deps): update codecov/codecov-action action to v4 (#250)
renovate[bot] Apr 29, 2024
7cd6656
fix(deps): update module golang.org/x/exp to v0.0.0-20240416160154-fe…
renovate[bot] May 1, 2024
98d651c
chore(deps): update codecov/codecov-action action to v4.3.1 (#270)
renovate[bot] May 1, 2024
b2e2d17
Revert "code review changes"
gunishmatta May 3, 2024
4e9a2d2
code review changes
gunishmatta May 3, 2024
db37405
Merge branch 'open-feature:main' into domains
gunishmatta May 4, 2024
f151713
Merge branch 'main' into domains
gunishmatta May 5, 2024
37fa69a
Merge branch 'main' into domains
gunishmatta May 7, 2024
012a941
Merge branch 'main' into domains
Kavindu-Dodan May 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,16 +89,16 @@ See [here](https://pkg.go.dev/github.com/open-feature/go-sdk/pkg/openfeature) fo

## 🌟 Features

| Status | Features | Description |
| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| Status | Features | Description |
| ------ |---------------------------------| --------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
| ✅ | [Named clients](#named-clients) | Utilize multiple providers in a single application. |
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
| ✅ | [Domains](#domains) | Logically bind clients with providers.|
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |

<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>

Expand All @@ -115,7 +115,7 @@ openfeature.SetProvider(MyProvider{})
```

In some situations, it may be beneficial to register multiple providers in the same application.
This is possible using [named clients](#named-clients), which is covered in more details below.
This is possible using [domains](#domains), which is covered in more details below.

### Targeting

Expand Down Expand Up @@ -190,11 +190,8 @@ c := openfeature.NewClient("log").WithLogger(l) // set the logger at client leve
[logr](https://github.com/go-logr/logr) uses incremental verbosity levels (akin to named levels but in integer form).
The SDK logs `info` at level `0` and `debug` at level `1`. Errors are always logged.

### Named clients

Clients can be given a name.
A name is a logical identifier which can be used to associate clients with a particular provider.
If a name has no associated provider, the global provider is used.
### Domains
Clients can be assigned to a domain. A domain is a logical identifier which can be used to associate clients with a particular provider. If a domain has no associated provider, the default provider is used.

```go
import "github.com/open-feature/go-sdk/openfeature"
Expand All @@ -210,6 +207,7 @@ clientWithDefault := openfeature.NewClient("")
clientForCache := openfeature.NewClient("clientForCache")
```


### Eventing

Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
Expand Down
16 changes: 8 additions & 8 deletions openfeature/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ func (api *evaluationAPI) getProvider() FeatureProvider {
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 {
// setProvider sets a provider with client domain. Returns an error if FeatureProvider is nil
func (api *evaluationAPI) setNamedProvider(clientDomain string, provider FeatureProvider, async bool) error {
api.mu.Lock()
defer api.mu.Unlock()

Expand All @@ -81,15 +81,15 @@ func (api *evaluationAPI) setNamedProvider(clientName string, provider FeaturePr

// 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
oldProvider := api.namedProviders[clientDomain]
api.namedProviders[clientDomain] = provider

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

err = api.eventExecutor.registerNamedEventingProvider(clientName, provider)
err = api.eventExecutor.registerNamedEventingProvider(clientDomain, provider)
if err != nil {
return err
}
Expand Down Expand Up @@ -159,14 +159,14 @@ func (api *evaluationAPI) getHooks() []Hook {
}

// 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) {
// Returns the default FeatureProvider if no provider mapping exist for the given client domain.
func (api *evaluationAPI) forTransaction(clientDomain string) (FeatureProvider, []Hook, EvaluationContext) {
api.mu.RLock()
defer api.mu.RUnlock()

var provider FeatureProvider

provider = api.namedProviders[clientName]
provider = api.namedProviders[clientDomain]
if provider == nil {
provider = api.defaultProvider
}
Expand Down
14 changes: 10 additions & 4 deletions openfeature/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,16 @@ func NewClientMetadata(name string) ClientMetadata {
}

// Name returns the client's name
// Deprecated: Name() exists for historical compatibility, use Domain() instead.
func (cm ClientMetadata) Name() string {
return cm.name
}

// Domain returns the client's domain
func (cm ClientMetadata) Domain() string {
return cm.name
}

// Client implements the behaviour required of an openfeature client
type Client struct {
mx sync.RWMutex
Expand All @@ -67,9 +73,9 @@ type Client struct {
var _ IClient = (*Client)(nil)

// NewClient returns a new Client. Name is a unique identifier for this client
func NewClient(name string) *Client {
func NewClient(domain string) *Client {
return &Client{
metadata: ClientMetadata{name: name},
metadata: ClientMetadata{name: domain},
hooks: []Hook{},
evaluationContext: EvaluationContext{},
logger: globalLogger,
Expand Down Expand Up @@ -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)
addClientHandler(c.metadata.Domain(), eventType, callback)
}

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

// SetEvaluationContext sets the client's evaluation context
Expand Down
4 changes: 2 additions & 2 deletions openfeature/client_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (

func ExampleNewClient() {
client := openfeature.NewClient("example-client")
fmt.Printf("Client Name: %s", client.Metadata().Name())
// Output: Client Name: example-client
fmt.Printf("Client Domain: %s", client.Metadata().Domain())
// Output: Client Domain: example-client
}

func ExampleClient_BooleanValue() {
Expand Down
8 changes: 4 additions & 4 deletions openfeature/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ func TestRequirement_1_2_1(t *testing.T) {
}

// The client interface MUST define a `metadata` member or accessor,
// containing an immutable `name` field or accessor of type string,
// which corresponds to the `name` value supplied during client creation.
// containing an immutable `domain` field or accessor of type string,
// which corresponds to the `domain` value supplied during client creation.
func TestRequirement_1_2_2(t *testing.T) {
defer t.Cleanup(initSingleton)
clientName := "test-client"
client := NewClient(clientName)

if client.Metadata().Name() != clientName {
t.Errorf("client name not initiated as expected, got %s, want %s", client.Metadata().Name(), clientName)
if client.Metadata().Domain() != clientName {
t.Errorf("client domain not initiated as expected, got %s, want %s", client.Metadata().Domain(), clientName)
}
}

Expand Down
14 changes: 7 additions & 7 deletions openfeature/event_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ func newEventExecutor(logger logr.Logger) *eventExecutor {
return &executor
}

// scopedCallback is a helper struct to hold client name associated callbacks.
// Here, the scope correlates to the client and provider name
// scopedCallback is a helper struct to hold client domain associated callbacks.
// Here, the scope correlates to the client and provider domain
type scopedCallback struct {
scope string
callbacks map[EventType][]EventCallback
Expand Down Expand Up @@ -111,24 +111,24 @@ func (e *eventExecutor) removeApiHandler(t EventType, c EventCallback) {
}

// registerClientHandler registers a client level handler
func (e *eventExecutor) registerClientHandler(clientName string, t EventType, c EventCallback) {
func (e *eventExecutor) registerClientHandler(clientDomain string, t EventType, c EventCallback) {
e.mu.Lock()
defer e.mu.Unlock()

_, ok := e.scopedRegistry[clientName]
_, ok := e.scopedRegistry[clientDomain]
if !ok {
e.scopedRegistry[clientName] = newScopedCallback(clientName)
e.scopedRegistry[clientDomain] = newScopedCallback(clientDomain)
}

registry := e.scopedRegistry[clientName]
registry := e.scopedRegistry[clientDomain]

if registry.callbacks[t] == nil {
registry.callbacks[t] = []EventCallback{c}
} else {
registry.callbacks[t] = append(registry.callbacks[t], c)
}

reference, ok := e.namedProviderReference[clientName]
reference, ok := e.namedProviderReference[clientDomain]
if !ok {
// fallback to default
reference = e.defaultProviderReference
Expand Down
10 changes: 5 additions & 5 deletions openfeature/event_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ func TestEventHandler_RegisterUnregisterEventProvider(t *testing.T) {
t.Error("implementation should register default eventing provider")
}

err = executor.registerNamedEventingProvider("name", eventingProvider)
err = executor.registerNamedEventingProvider("domain", eventingProvider)
if err != nil {
t.Fatal(err)
}

if _, ok := executor.namedProviderReference["name"]; !ok {
if _, ok := executor.namedProviderReference["domain"]; !ok {
t.Errorf("implementation should register named eventing provider")
}
})
Expand Down Expand Up @@ -137,7 +137,7 @@ func TestEventHandler_Eventing(t *testing.T) {
eventingImpl,
}

// associated to client name
// associated to client domain
associatedName := "providerForClient"

err := SetNamedProviderAndWait(associatedName, eventingProvider)
Expand Down Expand Up @@ -220,7 +220,7 @@ func TestEventHandler_clientAssociation(t *testing.T) {
t.Fatal(err)
}

// named provider(associated to name someClient)
// named provider(associated to domain someClient)
err = SetNamedProviderAndWait("someClient", struct {
FeatureProvider
EventHandler
Expand Down Expand Up @@ -672,7 +672,7 @@ func TestEventHandler_ProviderReadiness(t *testing.T) {
}
})

t.Run("for name associated handler", func(t *testing.T) {
t.Run("for domain associated handler", func(t *testing.T) {
defer t.Cleanup(initSingleton)

readyEventingProvider := struct {
Expand Down
24 changes: 12 additions & 12 deletions openfeature/openfeature.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ func SetProviderAndWait(provider FeatureProvider) error {
return api.setProvider(provider, false)
}

// SetNamedProvider sets a provider mapped to the given Client name. Provider initialization is asynchronous and
// SetNamedProvider sets a provider mapped to the given Client domain. Provider initialization is asynchronous and
// status can be checked from provider status
func SetNamedProvider(clientName string, provider FeatureProvider) error {
return api.setNamedProvider(clientName, provider, true)
func SetNamedProvider(clientDomain string, provider FeatureProvider) error {
return api.setNamedProvider(clientDomain, provider, true)
}

// SetNamedProviderAndWait sets a provider mapped to the given Client name and waits for its initialization.
// SetNamedProviderAndWait sets a provider mapped to the given Client domain and waits for its initialization.
// Returns an error if initialization cause error
func SetNamedProviderAndWait(clientName string, provider FeatureProvider) error {
return api.setNamedProvider(clientName, provider, false)
func SetNamedProviderAndWait(clientDomain string, provider FeatureProvider) error {
return api.setNamedProvider(clientDomain, provider, false)
}

// SetEvaluationContext sets the global evaluation context.
Expand Down Expand Up @@ -67,8 +67,8 @@ func AddHandler(eventType EventType, callback EventCallback) {
}

// addClientHandler is a helper for Client to add an event handler
func addClientHandler(name string, t EventType, c EventCallback) {
api.eventExecutor.registerClientHandler(name, t, c)
func addClientHandler(domain string, t EventType, c EventCallback) {
api.eventExecutor.registerClientHandler(domain, t, c)
}

// RemoveHandler allows to remove API level event handler
Expand All @@ -77,8 +77,8 @@ func RemoveHandler(eventType EventType, callback EventCallback) {
}

// removeClientHandler is a helper for Client to add an event handler
func removeClientHandler(name string, t EventType, c EventCallback) {
api.eventExecutor.removeClientHandler(name, t, c)
func removeClientHandler(domain string, t EventType, c EventCallback) {
api.eventExecutor.removeClientHandler(domain, t, c)
}

// getAPIEventRegistry is a helper for testing
Expand Down Expand Up @@ -122,6 +122,6 @@ func globalLogger() logr.Logger {

// forTransaction is a helper to retrieve transaction scoped operators by Client.
// Here, transaction means a flag evaluation.
func forTransaction(clientName string) (FeatureProvider, []Hook, EvaluationContext) {
return api.forTransaction(clientName)
func forTransaction(clientDomain string) (FeatureProvider, []Hook, EvaluationContext) {
return api.forTransaction(clientDomain)
}
14 changes: 7 additions & 7 deletions openfeature/openfeature_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func TestRequirement_1_1_2_3(t *testing.T) {
}
})

t.Run("ignore shutdown for multiple references - name client bound", func(t *testing.T) {
t.Run("ignore shutdown for multiple references - domain client bound", func(t *testing.T) {
defer t.Cleanup(initSingleton)

// setup
Expand Down Expand Up @@ -393,8 +393,8 @@ func TestRequirement_1_1_2_4(t *testing.T) {
})
}

// 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.
// The `API` MUST provide a function to bind a given `provider` to one or more client `domain`s.
// If the client-domain already has a bound provider, it is overwritten with the new mapping.
func TestRequirement_1_1_3(t *testing.T) {
defer t.Cleanup(initSingleton)

Expand Down Expand Up @@ -445,7 +445,7 @@ func TestRequirement_1_1_3(t *testing.T) {
t.Errorf("expected %s, but got %s", "providerB", providerA.Metadata().Name)
}

// Validate overriding: If the client-name already has a bound provider, it is overwritten with the new mapping.
// Validate overriding: If the client-domain already has a bound provider, it is overwritten with the new mapping.

providerB2 := NewMockFeatureProvider(ctrl)
providerB2.EXPECT().Metadata().Return(Metadata{Name: "providerB2"}).AnyTimes()
Expand Down Expand Up @@ -495,7 +495,7 @@ func TestRequirement_1_1_5(t *testing.T) {
}

// The `API` MUST provide a function for creating a `client` which accepts the following options:
// - name (optional): A logical string identifier for the client.
// - domain (optional): A logical string identifier for the client.
func TestRequirement_1_1_6(t *testing.T) {
defer t.Cleanup(initSingleton)
NewClient("test-client")
Expand Down Expand Up @@ -563,7 +563,7 @@ func TestRequirement_EventCompliance(t *testing.T) {
registry := getClientRegistry(clientName)

if registry == nil {
t.Fatalf("no event handler registry present for client name %s", clientName)
t.Fatalf("no event handler registry present for client domain %s", clientName)
}

if len(registry.callbacks[ProviderReady]) < 1 {
Expand Down Expand Up @@ -661,7 +661,7 @@ func TestRequirement_EventCompliance(t *testing.T) {

// Non-spec bound validations

// If there is no client name bound provider, then return the default provider
// If there is no client domain bound provider, then return the default provider
func TestDefaultClientUsage(t *testing.T) {
defer t.Cleanup(initSingleton)

Expand Down
Loading