Skip to content

Commit

Permalink
feat: implement domain scoping (#934)
Browse files Browse the repository at this point in the history
* initial changes for implementing domain scoping

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>

* completed the rest of the renaming and tidying up

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>

* applied code review suggested changes

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>

* introdcue a dedicated interface for client metadata

Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>

* add deprecated default getName to be backward compatible as much as possible

Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>

---------

Signed-off-by: jarebudev <23311805+jarebudev@users.noreply.github.com>
Signed-off-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>
Co-authored-by: Kavindu Dodanduwa <Kavindu-Dodan@users.noreply.github.com>
Co-authored-by: Kavindu Dodanduwa <kavindudodanduwa@gmail.com>
  • Loading branch information
3 people authored Jun 11, 2024
1 parent 4ea74d8 commit 5c0aaaa
Show file tree
Hide file tree
Showing 21 changed files with 199 additions and 188 deletions.
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.
|| [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. |
|| [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. |
|| [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
Expand Down Expand Up @@ -160,7 +160,7 @@ To register a provider in a non-blocking manner, you can use the `setProvider` m
```

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 detail below.

### Targeting

Expand Down Expand Up @@ -219,27 +219,27 @@ Once you've added a hook as a dependency, it can be registered at the global, cl

The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation.

### Named clients
### Domains

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.
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 global provider is used.

```java
FeatureProvider scopedProvider = new MyProvider();

// registering the default provider
OpenFeatureAPI.getInstance().setProvider(LocalProvider());
// registering a named provider
OpenFeatureAPI.getInstance().setProvider("clientForCache", new CachedProvider());
// registering a provider to a domain
OpenFeatureAPI.getInstance().setProvider("my-domain", new CachedProvider());

// a client backed by default provider
// A client bound to the default provider
Client clientDefault = OpenFeatureAPI.getInstance().getClient();
// a client backed by CachedProvider
Client clientNamed = OpenFeatureAPI.getInstance().getClient("clientForCache");
// A client bound to the CachedProvider provider
Client domainScopedClient = OpenFeatureAPI.getInstance().getClient("my-domain");
```

Named providers can be set in a blocking or non-blocking way.
Providers for domains can be set in a blocking or non-blocking way.
For more details, please refer to the [providers](#providers) section.

### Eventing
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/dev/openfeature/sdk/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Interface used to resolve flags of varying types.
*/
public interface Client extends Features, EventBus<Client> {
Metadata getMetadata();
ClientMetadata getMetadata();

/**
* Return an optional client-level evaluation context.
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/dev/openfeature/sdk/ClientMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfeature.sdk;

/**
* Metadata specific to an OpenFeature {@code Client}.
*/
public interface ClientMetadata {
String getDomain();

@Deprecated
// this is here for compatibility with getName() exposed from {@link Metadata}
default String getName() {
return getDomain();
}
}
6 changes: 3 additions & 3 deletions src/main/java/dev/openfeature/sdk/EventDetails.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
@Data
@SuperBuilder(toBuilder = true)
public class EventDetails extends ProviderEventDetails {
private String clientName;
private String domain;
private String providerName;

static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails, String providerName) {
Expand All @@ -19,9 +19,9 @@ static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventD
static EventDetails fromProviderEventDetails(
ProviderEventDetails providerEventDetails,
String providerName,
String clientName) {
String domain) {
return builder()
.clientName(clientName)
.domain(domain)
.providerName(providerName)
.flagsChanged(providerEventDetails.getFlagsChanged())
.eventMetadata(providerEventDetails.getEventMetadata())
Expand Down
35 changes: 17 additions & 18 deletions src/main/java/dev/openfeature/sdk/EventSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@ class EventSupport {
});

/**
* Run all the event handlers associated with this client name.
* If the client name is null, handlers attached to unnamed clients will run.
* Run all the event handlers associated with this domain.
* If the domain is null, handlers attached to unnamed clients will run.
*
* @param clientName the client name to run event handlers for, or null
* @param domain the domain to run event handlers for, or null
* @param event the event type
* @param eventDetails the event details
*/
public void runClientHandlers(String clientName, ProviderEvent event, EventDetails eventDetails) {
clientName = Optional.ofNullable(clientName)
public void runClientHandlers(String domain, ProviderEvent event, EventDetails eventDetails) {
domain = Optional.ofNullable(domain)
.orElse(defaultClientUuid);

// run handlers if they exist
Optional.ofNullable(handlerStores.get(clientName))
Optional.ofNullable(handlerStores.get(domain))
.filter(store -> Optional.of(store).isPresent())
.map(store -> store.handlerMap.get(event))
.ifPresent(handlers -> handlers
Expand All @@ -66,15 +66,14 @@ public void runGlobalHandlers(ProviderEvent event, EventDetails eventDetails) {
}

/**
* Add a handler for the specified client name, or all unnamed clients.
* Add a handler for the specified domain, or all unnamed clients.
*
* @param clientName the client name to add handlers for, or else the unnamed
* client
* @param domain the domain to add handlers for, or else unnamed
* @param event the event type
* @param handler the handler function to run
*/
public void addClientHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
final String name = Optional.ofNullable(clientName)
public void addClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
final String name = Optional.ofNullable(domain)
.orElse(defaultClientUuid);

// lazily create and cache a HandlerStore if it doesn't exist
Expand All @@ -90,15 +89,15 @@ public void addClientHandler(String clientName, ProviderEvent event, Consumer<Ev
/**
* Remove a client event handler for the specified event type.
*
* @param clientName the name of the client handler to remove, or null to remove
* @param domain the domain of the client handler to remove, or null to remove
* from unnamed clients
* @param event the event type
* @param handler the handler ref to be removed
*/
public void removeClientHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
clientName = Optional.ofNullable(clientName)
public void removeClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
domain = Optional.ofNullable(domain)
.orElse(defaultClientUuid);
this.handlerStores.get(clientName).removeHandler(event, handler);
this.handlerStores.get(domain).removeHandler(event, handler);
}

/**
Expand All @@ -122,11 +121,11 @@ public void removeGlobalHandler(ProviderEvent event, Consumer<EventDetails> hand
}

/**
* Get all client names for which we have event handlers registered.
* Get all domain names for which we have event handlers registered.
*
* @return set of client names
* @return set of domain names
*/
public Set<String> getAllClientNames() {
public Set<String> getAllDomainNames() {
return this.handlerStores.keySet();
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/dev/openfeature/sdk/HookContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class HookContext<T> {
@NonNull FlagValueType type;
@NonNull T defaultValue;
@NonNull EvaluationContext ctx;
Metadata clientMetadata;
ClientMetadata clientMetadata;
Metadata providerMetadata;

/**
Expand All @@ -30,7 +30,7 @@ public class HookContext<T> {
* @param <T> type that the flag is evaluating against
* @return resulting context for hook
*/
public static <T> HookContext<T> from(String key, FlagValueType type, Metadata clientMetadata,
public static <T> HookContext<T> from(String key, FlagValueType type, ClientMetadata clientMetadata,
Metadata providerMetadata, EvaluationContext ctx, T defaultValue) {
return HookContext.<T>builder()
.flagKey(key)
Expand Down
82 changes: 41 additions & 41 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;

import dev.openfeature.sdk.exceptions.OpenFeatureError;
import dev.openfeature.sdk.internal.AutoCloseableLock;
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;

/**
* A global singleton which holds base configuration for the OpenFeature
* library.
Expand Down Expand Up @@ -52,8 +52,8 @@ public Metadata getProviderMetadata() {
return getProvider().getMetadata();
}

public Metadata getProviderMetadata(String clientName) {
return getProvider(clientName).getMetadata();
public Metadata getProviderMetadata(String domain) {
return getProvider(domain).getMetadata();
}

/**
Expand All @@ -66,16 +66,16 @@ public Client getClient() {
/**
* {@inheritDoc}
*/
public Client getClient(String name) {
return getClient(name, null);
public Client getClient(String domain) {
return getClient(domain, null);
}

/**
* {@inheritDoc}
*/
public Client getClient(String name, String version) {
public Client getClient(String domain, String version) {
return new OpenFeatureClient(this,
name,
domain,
version);
}

Expand Down Expand Up @@ -154,14 +154,14 @@ public void setProvider(FeatureProvider provider) {
}

/**
* Add a provider for a named client.
* Add a provider for a domain.
*
* @param clientName The name of the client.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProvider(String clientName, FeatureProvider provider) {
public void setProvider(String domain, FeatureProvider provider) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(clientName,
providerRepository.setProvider(domain,
provider,
this::attachEventProvider,
this::emitReady,
Expand All @@ -187,14 +187,14 @@ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError
}

/**
* Add a provider for a named client and wait for initialization to finish.
* Add a provider for a domain and wait for initialization to finish.
*
* @param clientName The name of the client.
* @param domain The domain to bind the provider to.
* @param provider The provider to set.
*/
public void setProviderAndWait(String clientName, FeatureProvider provider) throws OpenFeatureError {
public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
providerRepository.setProvider(clientName,
providerRepository.setProvider(domain,
provider,
this::attachEventProvider,
this::emitReady,
Expand Down Expand Up @@ -240,13 +240,13 @@ public FeatureProvider getProvider() {
}

/**
* Fetch a provider for a named client. If not found, return the default.
* Fetch a provider for a domain. If not found, return the default.
*
* @param name The client name to look for.
* @param domain The domain to look for.
* @return A named {@link FeatureProvider}
*/
public FeatureProvider getProvider(String name) {
return providerRepository.getProvider(name);
public FeatureProvider getProvider(String domain) {
return providerRepository.getProvider(domain);
}

/**
Expand Down Expand Up @@ -344,20 +344,20 @@ public OpenFeatureAPI removeHandler(ProviderEvent event, Consumer<EventDetails>
return this;
}

void removeHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
void removeHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
eventSupport.removeClientHandler(clientName, event, handler);
eventSupport.removeClientHandler(domain, event, handler);
}
}

void addHandler(String clientName, ProviderEvent event, Consumer<EventDetails> handler) {
void addHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) {
// if the provider is in the state associated with event, run immediately
if (Optional.ofNullable(this.providerRepository.getProvider(clientName).getState())
if (Optional.ofNullable(this.providerRepository.getProvider(domain).getState())
.orElse(ProviderState.READY).matchesEvent(event)) {
eventSupport.runHandler(handler, EventDetails.builder().clientName(clientName).build());
eventSupport.runHandler(handler, EventDetails.builder().domain(domain).build());
}
eventSupport.addClientHandler(clientName, event, handler);
eventSupport.addClientHandler(domain, event, handler);
}
}

Expand All @@ -371,8 +371,8 @@ void addHandler(String clientName, ProviderEvent event, Consumer<EventDetails> h
private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) {
try (AutoCloseableLock __ = lock.readLockAutoCloseable()) {

List<String> clientNamesForProvider = providerRepository
.getClientNamesForProvider(provider);
List<String> domainsForProvider = providerRepository
.getDomainsForProvider(provider);

final String providerName = Optional.ofNullable(provider.getMetadata())
.map(metadata -> metadata.getName())
Expand All @@ -381,20 +381,20 @@ private void runHandlersForProvider(FeatureProvider provider, ProviderEvent even
// run the global handlers
eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details, providerName));

// run the handlers associated with named clients for this provider
clientNamesForProvider.forEach(name -> {
eventSupport.runClientHandlers(name, event,
EventDetails.fromProviderEventDetails(details, providerName, name));
// run the handlers associated with domains for this provider
domainsForProvider.forEach(domain -> {
eventSupport.runClientHandlers(domain, event,
EventDetails.fromProviderEventDetails(details, providerName, domain));
});

if (providerRepository.isDefaultProvider(provider)) {
// run handlers for clients that have no bound providers (since this is the default)
Set<String> allClientNames = eventSupport.getAllClientNames();
Set<String> boundClientNames = providerRepository.getAllBoundClientNames();
allClientNames.removeAll(boundClientNames);
allClientNames.forEach(name -> {
eventSupport.runClientHandlers(name, event,
EventDetails.fromProviderEventDetails(details, providerName, name));
Set<String> allDomainNames = eventSupport.getAllDomainNames();
Set<String> boundDomains = providerRepository.getAllBoundDomains();
allDomainNames.removeAll(boundDomains);
allDomainNames.forEach(domain -> {
eventSupport.runClientHandlers(domain, event,
EventDetails.fromProviderEventDetails(details, providerName, domain));
});
}
}
Expand Down
Loading

0 comments on commit 5c0aaaa

Please sign in to comment.