Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Empty file modified mvnw
100644 → 100755
Empty file.
53 changes: 40 additions & 13 deletions src/main/java/dev/openfeature/sdk/EventProvider.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package dev.openfeature.sdk;

import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
import dev.openfeature.sdk.internal.ConfigurableThreadFactory;
import dev.openfeature.sdk.internal.TriConsumer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import lombok.extern.slf4j.Slf4j;

/**
Expand All @@ -29,29 +31,52 @@ void setEventProviderListener(EventProviderListener eventProviderListener) {
this.eventProviderListener = eventProviderListener;
}

private TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit = null;
// Bundles onEmit and lock into a single volatile reference so they are always read atomically:
// a non-null attachment guarantees a non-null lock.
private static final class Attachment {
final TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit;
final AutoCloseableReentrantReadWriteLock lock;

Attachment(
TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit,
AutoCloseableReentrantReadWriteLock lock) {
this.onEmit = onEmit;
this.lock = lock;
}
}

private final AtomicReference<Attachment> attachment = new AtomicReference<>(null);

/**
* "Attach" this EventProvider to an SDK, which allows events to propagate from this provider.
* No-op if the same onEmit is already attached.
*
* @param onEmit the function to run when a provider emits events.
* @param lock the API instance's read/write lock for thread safety.
* @throws IllegalStateException if attempted to bind a new emitter for already bound provider
*/
void attach(TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit) {
if (this.onEmit != null && this.onEmit != onEmit) {
// if we are trying to attach this provider to a different onEmit, something has gone wrong
throw new IllegalStateException("Provider " + this.getMetadata().getName() + " is already attached.");
} else {
this.onEmit = onEmit;
void attach(
TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit,
AutoCloseableReentrantReadWriteLock lock) {
Attachment newAttachment = new Attachment(onEmit, lock);
while (true) {
Attachment existing = this.attachment.get();
if (existing != null && existing.onEmit != onEmit) {
// if we are trying to attach this provider to a different onEmit, something has gone wrong
throw new IllegalStateException(
"Provider " + this.getMetadata().getName() + " is already attached.");
}
if (this.attachment.compareAndSet(existing, newAttachment)) {
return;
}
Comment on lines +61 to +71
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't we simplify all of this with

if (!this.attachment.compareAndSet(null, newAttachment)) {
   throw new IllegalStateException();
}

we expect the existing attachement to be null, every other case is exceptional, and we wouldn't retry anyway

}
}

/**
* "Detach" this EventProvider from an SDK, stopping propagation of all events.
*/
void detach() {
this.onEmit = null;
this.attachment.set(null);
}

/**
Expand Down Expand Up @@ -80,9 +105,9 @@ public void shutdown() {
*/
public Awaitable emit(final ProviderEvent event, final ProviderEventDetails details) {
final var localEventProviderListener = this.eventProviderListener;
final var localOnEmit = this.onEmit;
final var localAttachment = this.attachment.get();

if (localEventProviderListener == null && localOnEmit == null) {
if (localEventProviderListener == null && localAttachment == null) {
return Awaitable.FINISHED;
}

Expand All @@ -91,12 +116,14 @@ public Awaitable emit(final ProviderEvent event, final ProviderEventDetails deta
// These calls need to be executed on a different thread to prevent deadlocks when the provider initialization
// relies on a ready event to be emitted
emitterExecutor.submit(() -> {
try (var ignored = OpenFeatureAPI.lock.readLockAutoCloseable()) {
// Lock is only needed when attached to an API instance. A non-null attachment always
// carries a non-null lock, so no null check on the lock itself is required.
try (var ignored = localAttachment != null ? localAttachment.lock.readLockAutoCloseable() : null) {
if (localEventProviderListener != null) {
localEventProviderListener.onEmit(event, details);
}
if (localOnEmit != null) {
localOnEmit.accept(this, event, details);
if (localAttachment != null) {
localAttachment.onEmit.accept(this, event, details);
}
} finally {
awaitable.wakeup();
Expand Down
82 changes: 75 additions & 7 deletions src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,72 @@
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;

/**
* A global singleton which holds base configuration for the OpenFeature
* library.
* Configuration here will be shared across all {@link Client}s.
* Holds base configuration for the OpenFeature library.
*
* <p>Most applications should use the global singleton via {@link #getInstance()}; configuration
* there is shared across all {@link Client}s. For dependency-injection frameworks, testing, or
* multi-tenant scenarios that need fully independent state (providers, hooks, evaluation context,
* event handlers, transaction context propagators), create isolated instances via
* {@code dev.openfeature.sdk.isolated.OpenFeatureAPIFactory.createAPI()}.
*
* <p><strong>Note:</strong> Isolated API instances (per spec section 1.8) are experimental and
* subject to change.
*
* @see <a href="https://openfeature.dev/specification/sections/flag-evaluation#18-isolated-api-instances">
* Spec &sect;1.8 &mdash; Isolated API Instances</a>
*/
@Slf4j
@SuppressWarnings("PMD.UnusedLocalVariable")
public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
// package-private multi-read/single-write lock
static AutoCloseableReentrantReadWriteLock lock = new AutoCloseableReentrantReadWriteLock();

/**
* Global registry tracking which API instance each provider is currently bound to.
* Used to detect violations of spec requirement 1.8.4 (a provider SHOULD NOT be
* registered with more than one API instance simultaneously).
*/
private static final ConcurrentHashMap<FeatureProvider, OpenFeatureAPI> GLOBAL_PROVIDER_REGISTRY =
new ConcurrentHashMap<>();

// package-private multi-read/single-write lock (instance-level for isolation)
final AutoCloseableReentrantReadWriteLock lock;
private final ConcurrentLinkedQueue<Hook> apiHooks;
private ProviderRepository providerRepository;
private EventSupport eventSupport;
private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>();
private TransactionContextPropagator transactionContextPropagator;

protected OpenFeatureAPI() {
/**
* Creates and returns a new, independent {@link OpenFeatureAPI} instance with fully isolated
* state (providers, hooks, evaluation context, event handlers, transaction context
* propagators).
*
* <p>Prefer {@code OpenFeatureAPIFactory.createAPI()} from
* {@code dev.openfeature.sdk.isolated}, which satisfies spec requirement 1.8.3
* (factory in a distinct package for intentional discoverability).
*
* @apiNote This API is experimental and subject to change.
* @see <a href="https://openfeature.dev/specification/sections/flag-evaluation#18-isolated-api-instances">
* Spec &sect;1.8 &mdash; Isolated API Instances</a>
*/
public static OpenFeatureAPI createIsolated() {
return new OpenFeatureAPI();
}

// Package-private: not part of the public API; use createIsolated() or OpenFeatureAPIFactory.
OpenFeatureAPI() {
this(new AutoCloseableReentrantReadWriteLock());
}

// Package-private constructor for testing with a custom lock.
OpenFeatureAPI(AutoCloseableReentrantReadWriteLock lock) {
this.lock = lock;
apiHooks = new ConcurrentLinkedQueue<>();
providerRepository = new ProviderRepository(this);
eventSupport = new EventSupport();
Expand Down Expand Up @@ -251,7 +295,7 @@ public void setProviderAndWait(String domain, FeatureProvider provider) throws O

private void attachEventProvider(FeatureProvider provider) {
if (provider instanceof EventProvider) {
((EventProvider) provider).attach(this::runHandlersForProvider);
((EventProvider) provider).attach(this::runHandlersForProvider, this.lock);
}
}

Expand Down Expand Up @@ -332,6 +376,30 @@ public void clearHooks() {
this.apiHooks.clear();
}

/**
* Registers a provider with the global registry, warning if it is already
* bound to a different API instance (spec requirement 1.8.4).
*/
void registerGlobalProvider(FeatureProvider provider) {
GLOBAL_PROVIDER_REGISTRY.compute(provider, (p, existing) -> {
if (existing != null && existing != this) {
log.warn("Provider "
+ provider.getClass().getName()
+ " is already registered with another API instance. "
+ "A provider SHOULD NOT be bound to more than one API instance "
+ "simultaneously (spec requirement 1.8.4).");
}
return this;
});
}

/**
* Removes the provider from the global registry if this instance is the current owner.
*/
void deregisterGlobalProvider(FeatureProvider provider) {
GLOBAL_PROVIDER_REGISTRY.remove(provider, this);
}

/**
* Shut down and reset the current status of OpenFeature API.
* This call cleans up all active providers and attempts to shut down internal
Expand Down
10 changes: 9 additions & 1 deletion src/main/java/dev/openfeature/sdk/ProviderRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ private void prepareAndInitializeProvider(
newStateManager = new FeatureProviderStateManager(newProvider);
// only run afterSet if new provider is not already attached
afterSet.accept(newProvider);
// spec 1.8.4: warn if this provider is already bound to another API instance
openFeatureAPI.registerGlobalProvider(newProvider);
} else {
newStateManager = existing;
}
Expand Down Expand Up @@ -236,6 +238,8 @@ private void initializeProvider(
private void shutDownOld(FeatureProviderStateManager oldManager, Consumer<FeatureProvider> afterShutdown) {
synchronized (registerStateManagerLock) {
if (oldManager != null && !isStateManagerRegistered(oldManager)) {
// spec 1.8.4: release the provider from the global registry
openFeatureAPI.deregisterGlobalProvider(oldManager.getProvider());
shutdownProvider(oldManager);
afterShutdown.accept(oldManager.getProvider());
}
Expand Down Expand Up @@ -327,7 +331,11 @@ List<FeatureProviderStateManager> prepareShutdown() {
* @param managersToShutdown the managers to shut down (from prepareShutdown)
*/
void completeShutdown(List<FeatureProviderStateManager> managersToShutdown) {
managersToShutdown.forEach(this::shutdownProvider);
managersToShutdown.forEach(m -> {
// spec 1.8.4: release all providers from the global registry on shutdown
openFeatureAPI.deregisterGlobalProvider(m.getProvider());
shutdownProvider(m);
});
taskExecutor.shutdown();
try {
if (!taskExecutor.awaitTermination(EventSupport.SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package dev.openfeature.sdk.isolated;

import dev.openfeature.sdk.OpenFeatureAPI;

/**
* Factory for creating isolated OpenFeature API instances.
*
* <p>Each instance returned by {@link #createAPI()} maintains its own state,
* including providers, evaluation context, hooks, event handlers, and
* transaction context propagators. Instances do not share state with the
* global singleton ({@link OpenFeatureAPI#getInstance()}) or with each other.
*
* <p>This class lives in a distinct package ({@code dev.openfeature.sdk.isolated})
* to make isolated instances intentionally less discoverable than the global
* singleton, reducing the chance of accidental use when the singleton would be
* appropriate.
*
* <p>This is useful for dependency injection frameworks, testing scenarios,
* and applications composed of multiple submodules requiring distinct providers.
*
* <p><strong>Spec references:</strong>
* <ul>
* <li>Requirement 1.8.1 &mdash; factory function for isolated instances</li>
* <li>Requirement 1.8.3 &mdash; factory in a distinct package/module</li>
* </ul>
*
* @apiNote This API is experimental and subject to change.
* @see <a href="https://openfeature.dev/specification/sections/flag-evaluation#18-isolated-api-instances">
* Spec &sect;1.8 &mdash; Isolated API Instances</a>
*/
public final class OpenFeatureAPIFactory {

private OpenFeatureAPIFactory() {
// utility class
}

/**
* Creates a new, independent {@link OpenFeatureAPI} instance with fully
* isolated state.
*
* <p>Usage:
* <pre>{@code
* OpenFeatureAPI api = OpenFeatureAPIFactory.createAPI();
* api.setProvider(new MyProvider());
* Client client = api.getClient();
* }</pre>
*
* @apiNote This API is experimental and subject to change.
* @return a new API instance
*/
public static OpenFeatureAPI createAPI() {
return OpenFeatureAPI.createIsolated();
}
}
Loading
Loading