diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java index 66d40736f..6d0d8feb4 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.java @@ -5,9 +5,12 @@ import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; @@ -21,14 +24,14 @@ public class OpenFeatureAPI implements EventBus { // package-private multi-read/single-write lock static AutoCloseableReentrantReadWriteLock lock = new AutoCloseableReentrantReadWriteLock(); - private final List apiHooks; + private final ConcurrentLinkedQueue apiHooks; private ProviderRepository providerRepository; private EventSupport eventSupport; - private EvaluationContext evaluationContext; + private final AtomicReference evaluationContext = new AtomicReference<>(); private TransactionContextPropagator transactionContextPropagator; protected OpenFeatureAPI() { - apiHooks = new ArrayList<>(); + apiHooks = new ConcurrentLinkedQueue<>(); providerRepository = new ProviderRepository(this); eventSupport = new EventSupport(); transactionContextPropagator = new NoOpTransactionContextPropagator(); @@ -115,9 +118,7 @@ public Client getClient(String domain, String version) { * @return api instance */ public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { - this.evaluationContext = evaluationContext; - } + this.evaluationContext.set(evaluationContext); return this; } @@ -127,16 +128,14 @@ public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) * @return evaluation context */ public EvaluationContext getEvaluationContext() { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { - return this.evaluationContext; - } + return evaluationContext.get(); } /** * Return the transaction context propagator. */ public TransactionContextPropagator getTransactionContextPropagator() { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.readLockAutoCloseable()) { return this.transactionContextPropagator; } } @@ -150,7 +149,7 @@ public void setTransactionContextPropagator(TransactionContextPropagator transac if (transactionContextPropagator == null) { throw new IllegalArgumentException("Transaction context propagator cannot be null"); } - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { this.transactionContextPropagator = transactionContextPropagator; } } @@ -176,7 +175,7 @@ public void setTransactionContext(EvaluationContext evaluationContext) { * Set the default provider. */ public void setProvider(FeatureProvider provider) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( provider, this::attachEventProvider, @@ -194,7 +193,7 @@ public void setProvider(FeatureProvider provider) { * @param provider The provider to set. */ public void setProvider(String domain, FeatureProvider provider) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( domain, provider, @@ -216,7 +215,7 @@ public void setProvider(String domain, FeatureProvider provider) { * @throws OpenFeatureError if the provider fails during initialization. */ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( provider, this::attachEventProvider, @@ -238,7 +237,7 @@ public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError * @throws OpenFeatureError if the provider fails during initialization. */ public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.setProvider( domain, provider, @@ -252,9 +251,7 @@ public void setProviderAndWait(String domain, FeatureProvider provider) throws O private void attachEventProvider(FeatureProvider provider) { if (provider instanceof EventProvider) { - ((EventProvider) provider).attach((p, event, details) -> { - runHandlersForProvider(p, event, details); - }); + ((EventProvider) provider).attach(this::runHandlersForProvider); } } @@ -307,9 +304,7 @@ public FeatureProvider getProvider(String domain) { * @param hooks The hook to add. */ public void addHooks(Hook... hooks) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { - this.apiHooks.addAll(Arrays.asList(hooks)); - } + this.apiHooks.addAll(Arrays.asList(hooks)); } /** @@ -318,18 +313,23 @@ public void addHooks(Hook... hooks) { * @return A list of {@link Hook}s. */ public List getHooks() { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { - return this.apiHooks; - } + return new ArrayList<>(this.apiHooks); + } + + /** + * Returns a reference to the collection of {@link Hook}s. + * + * @return The collection of {@link Hook}s. + */ + Collection getMutableHooks() { + return this.apiHooks; } /** * Removes all hooks. */ public void clearHooks() { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { - this.apiHooks.clear(); - } + this.apiHooks.clear(); } /** @@ -339,7 +339,7 @@ public void clearHooks() { * Once shut down is complete, API is reset and ready to use again. */ public void shutdown() { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { providerRepository.shutdown(); eventSupport.shutdown(); @@ -385,7 +385,7 @@ public OpenFeatureAPI onProviderError(Consumer handler) { */ @Override public OpenFeatureAPI on(ProviderEvent event, Consumer handler) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { this.eventSupport.addGlobalHandler(event, handler); return this; } @@ -396,18 +396,20 @@ public OpenFeatureAPI on(ProviderEvent event, Consumer handler) { */ @Override public OpenFeatureAPI removeHandler(ProviderEvent event, Consumer handler) { - this.eventSupport.removeGlobalHandler(event, handler); + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { + this.eventSupport.removeGlobalHandler(event, handler); + } return this; } void removeHandler(String domain, ProviderEvent event, Consumer handler) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { eventSupport.removeClientHandler(domain, event, handler); } } void addHandler(String domain, ProviderEvent event, Consumer handler) { - try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { // if the provider is in the state associated with event, run immediately if (Optional.ofNullable(this.providerRepository.getProviderState(domain)) .orElse(ProviderState.READY) @@ -431,32 +433,28 @@ FeatureProviderStateManager getFeatureProviderStateManager(String domain) { * @param details the event details */ private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) { - try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { + try (AutoCloseableLock ignored = lock.readLockAutoCloseable()) { List domainsForProvider = providerRepository.getDomainsForProvider(provider); final String providerName = Optional.ofNullable(provider.getMetadata()) - .map(metadata -> metadata.getName()) + .map(Metadata::getName) .orElse(null); // run the global handlers eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details, providerName)); // run the handlers associated with domains for this provider - domainsForProvider.forEach(domain -> { - eventSupport.runClientHandlers( - domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain)); - }); + 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 allDomainNames = eventSupport.getAllDomainNames(); Set boundDomains = providerRepository.getAllBoundDomains(); allDomainNames.removeAll(boundDomains); - allDomainNames.forEach(domain -> { - eventSupport.runClientHandlers( - domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain)); - }); + allDomainNames.forEach(domain -> eventSupport.runClientHandlers( + domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain))); } } } diff --git a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java index e68d28f79..b5522b66a 100644 --- a/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java +++ b/src/main/java/dev/openfeature/sdk/OpenFeatureClient.java @@ -5,9 +5,8 @@ import dev.openfeature.sdk.exceptions.GeneralError; import dev.openfeature.sdk.exceptions.OpenFeatureError; import dev.openfeature.sdk.exceptions.ProviderNotReadyError; -import dev.openfeature.sdk.internal.AutoCloseableLock; -import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; import dev.openfeature.sdk.internal.ObjectUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -15,6 +14,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -46,11 +47,9 @@ public class OpenFeatureClient implements Client { @Getter private final String version; - private final List clientHooks; + private final ConcurrentLinkedQueue clientHooks; private final HookSupport hookSupport; - AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock(); - AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock(); - private EvaluationContext evaluationContext; + private final AtomicReference evaluationContext = new AtomicReference<>(); /** * Deprecated public constructor. Use OpenFeature.API.getClient() instead. @@ -68,7 +67,7 @@ public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String ve this.openfeatureApi = openFeatureAPI; this.domain = domain; this.version = version; - this.clientHooks = new ArrayList<>(); + this.clientHooks = new ConcurrentLinkedQueue<>(); this.hookSupport = new HookSupport(); } @@ -125,9 +124,7 @@ public void track(String trackingEventName, EvaluationContext context, TrackingE */ @Override public OpenFeatureClient addHooks(Hook... hooks) { - try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) { - this.clientHooks.addAll(Arrays.asList(hooks)); - } + this.clientHooks.addAll(Arrays.asList(hooks)); return this; } @@ -136,9 +133,7 @@ public OpenFeatureClient addHooks(Hook... hooks) { */ @Override public List getHooks() { - try (AutoCloseableLock __ = this.hooksLock.readLockAutoCloseable()) { - return this.clientHooks; - } + return new ArrayList<>(this.clientHooks); } /** @@ -146,9 +141,7 @@ public List getHooks() { */ @Override public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContext) { - try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) { - this.evaluationContext = evaluationContext; - } + this.evaluationContext.set(evaluationContext); return this; } @@ -157,32 +150,33 @@ public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContex */ @Override public EvaluationContext getEvaluationContext() { - try (AutoCloseableLock __ = contextLock.readLockAutoCloseable()) { - return this.evaluationContext; - } + return this.evaluationContext.get(); } + @SuppressFBWarnings( + value = {"REC_CATCH_EXCEPTION"}, + justification = "We don't want to allow any exception to reach the user. " + + "Instead, we return an evaluation result with the appropriate error code.") private FlagEvaluationDetails evaluateFlag( FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) { - FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull( + var flagOptions = ObjectUtils.defaultIfNull( options, () -> FlagEvaluationOptions.builder().build()); - Map hints = Collections.unmodifiableMap(flagOptions.getHookHints()); + var hints = Collections.unmodifiableMap(flagOptions.getHookHints()); FlagEvaluationDetails details = null; List mergedHooks = null; HookContext afterHookContext = null; - FeatureProvider provider; try { - FeatureProviderStateManager stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain); + var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain); // provider must be accessed once to maintain a consistent reference - provider = stateManager.getProvider(); - ProviderState state = stateManager.getState(); + var provider = stateManager.getProvider(); + var state = stateManager.getState(); mergedHooks = ObjectUtils.merge( - provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getHooks()); + provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getMutableHooks()); - EvaluationContext mergedCtx = hookSupport.beforeHooks( + var mergedCtx = hookSupport.beforeHooks( type, HookContext.from( key, @@ -205,12 +199,12 @@ private FlagEvaluationDetails evaluateFlag( throw new FatalError("Provider is in an irrecoverable error state"); } - ProviderEvaluation providerEval = + var providerEval = (ProviderEvaluation) createProviderEvaluation(type, key, defaultValue, provider, mergedCtx); details = FlagEvaluationDetails.from(providerEval, key); if (details.getErrorCode() != null) { - OpenFeatureError error = + var error = ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage()); enrichDetailsWithErrorDefaults(defaultValue, details); hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints); @@ -264,7 +258,7 @@ private void invokeTrack(String trackingEventName, EvaluationContext context, Tr */ private EvaluationContext mergeEvaluationContext(EvaluationContext invocationContext) { final EvaluationContext apiContext = openfeatureApi.getEvaluationContext(); - final EvaluationContext clientContext = this.getEvaluationContext(); + final EvaluationContext clientContext = evaluationContext.get(); final EvaluationContext transactionContext = openfeatureApi.getTransactionContext(); return mergeContextMaps(apiContext, transactionContext, clientContext, invocationContext); } diff --git a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java index b367820c2..86a9ddd70 100644 --- a/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java +++ b/src/main/java/dev/openfeature/sdk/internal/ObjectUtils.java @@ -1,6 +1,7 @@ package dev.openfeature.sdk.internal; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -64,9 +65,9 @@ public static T defaultIfNull(T source, Supplier defaultValue) { * @return resulting object */ @SafeVarargs - public static List merge(List... sources) { + public static List merge(Collection... sources) { List merged = new ArrayList<>(); - for (List source : sources) { + for (Collection source : sources) { merged.addAll(source); } return merged; diff --git a/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java index ad86f4bc3..ae3246cae 100644 --- a/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java +++ b/src/test/java/dev/openfeature/sdk/LockingSingeltonTest.java @@ -20,7 +20,6 @@ class LockingSingeltonTest { private static OpenFeatureAPI api; private OpenFeatureClient client; private AutoCloseableReentrantReadWriteLock apiLock; - private AutoCloseableReentrantReadWriteLock clientContextLock; private AutoCloseableReentrantReadWriteLock clientHooksLock; @BeforeAll @@ -36,10 +35,7 @@ void beforeEach() { apiLock = setupLock(apiLock, mockInnerReadLock(), mockInnerWriteLock()); OpenFeatureAPI.lock = apiLock; - clientContextLock = setupLock(clientContextLock, mockInnerReadLock(), mockInnerWriteLock()); clientHooksLock = setupLock(clientHooksLock, mockInnerReadLock(), mockInnerWriteLock()); - client.contextLock = clientContextLock; - client.hooksLock = clientHooksLock; } @Nested @@ -137,50 +133,6 @@ void onProviderErrorProviderReadyShouldApiWriteLockAndUnlock() { } } - @Test - void addHooksShouldWriteLockAndUnlock() { - client.addHooks(new Hook() {}); - verify(clientHooksLock.writeLock()).lock(); - verify(clientHooksLock.writeLock()).unlock(); - - api.addHooks(new Hook() {}); - verify(apiLock.writeLock()).lock(); - verify(apiLock.writeLock()).unlock(); - } - - @Test - void getHooksShouldReadLockAndUnlock() { - client.getHooks(); - verify(clientHooksLock.readLock()).lock(); - verify(clientHooksLock.readLock()).unlock(); - - api.getHooks(); - verify(apiLock.readLock()).lock(); - verify(apiLock.readLock()).unlock(); - } - - @Test - void setContextShouldWriteLockAndUnlock() { - client.setEvaluationContext(new ImmutableContext()); - verify(clientContextLock.writeLock()).lock(); - verify(clientContextLock.writeLock()).unlock(); - - api.setEvaluationContext(new ImmutableContext()); - verify(apiLock.writeLock()).lock(); - verify(apiLock.writeLock()).unlock(); - } - - @Test - void getContextShouldReadLockAndUnlock() { - client.getEvaluationContext(); - verify(clientContextLock.readLock()).lock(); - verify(clientContextLock.readLock()).unlock(); - - api.getEvaluationContext(); - verify(apiLock.readLock()).lock(); - verify(apiLock.readLock()).unlock(); - } - @Test void setTransactionalContextPropagatorShouldWriteLockAndUnlock() { api.setTransactionContextPropagator(new NoOpTransactionContextPropagator()); @@ -195,13 +147,6 @@ void getTransactionalContextPropagatorShouldReadLockAndUnlock() { verify(apiLock.readLock()).unlock(); } - @Test - void clearHooksShouldWriteLockAndUnlock() { - api.clearHooks(); - verify(apiLock.writeLock()).lock(); - verify(apiLock.writeLock()).unlock(); - } - private static ReentrantReadWriteLock.ReadLock mockInnerReadLock() { ReentrantReadWriteLock.ReadLock readLockMock = mock(ReentrantReadWriteLock.ReadLock.class); doNothing().when(readLockMock).lock();