-
Notifications
You must be signed in to change notification settings - Fork 58
fix: Reduce locking and concurrency issues #1478
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
Changes from 1 commit
a9d8daa
fe7ed8b
37e89f7
b7816ae
ce92510
0eb7a1e
c7bd342
0e40497
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,9 +5,11 @@ | |
| import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import java.util.function.Consumer; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
||
|
|
@@ -24,7 +26,7 @@ public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> { | |
| private final List<Hook> apiHooks; | ||
| private ProviderRepository providerRepository; | ||
| private EventSupport eventSupport; | ||
| private EvaluationContext evaluationContext; | ||
| private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>(); | ||
| private TransactionContextPropagator transactionContextPropagator; | ||
|
|
||
| protected OpenFeatureAPI() { | ||
|
|
@@ -115,9 +117,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 +127,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 +148,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 +174,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 +192,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 +214,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()) { | ||
|
toddbaert marked this conversation as resolved.
|
||
| try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { | ||
| providerRepository.setProvider( | ||
| provider, | ||
| this::attachEventProvider, | ||
|
|
@@ -238,7 +236,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 +250,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,7 +303,7 @@ public FeatureProvider getProvider(String domain) { | |
| * @param hooks The hook to add. | ||
| */ | ||
| public void addHooks(Hook... hooks) { | ||
| try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { | ||
| try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { | ||
| this.apiHooks.addAll(Arrays.asList(hooks)); | ||
| } | ||
| } | ||
|
|
@@ -318,16 +314,20 @@ public void addHooks(Hook... hooks) { | |
| * @return A list of {@link Hook}s. | ||
| */ | ||
| public List<Hook> getHooks() { | ||
| try (AutoCloseableLock __ = lock.readLockAutoCloseable()) { | ||
| return this.apiHooks; | ||
| try (AutoCloseableLock ignored = lock.readLockAutoCloseable()) { | ||
| if (this.apiHooks.isEmpty()) { | ||
| return Collections.emptyList(); | ||
| } else { | ||
| return new ArrayList<>(this.apiHooks); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Removes all hooks. | ||
| */ | ||
| public void clearHooks() { | ||
| try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { | ||
| try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { | ||
| 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<EventDetails> handler) { | |
| */ | ||
| @Override | ||
| public OpenFeatureAPI on(ProviderEvent event, Consumer<EventDetails> 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<EventDetails> handler) { | |
| */ | ||
| @Override | ||
| public OpenFeatureAPI removeHandler(ProviderEvent event, Consumer<EventDetails> handler) { | ||
| this.eventSupport.removeGlobalHandler(event, handler); | ||
| try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be possible to remove locking on the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Upon further review, I'm not so sure. We might get into trouble in the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You mean it's breaking in that the behavior changes, right (different references)? There's no associated compilation breakages here (or I'm missing something?). If so, I don't think I would really qualify that as breaking... it's a bit of a grey area, but IMO if we don't make specific guarantees about references, we don't necessarily need to adhere to them. I think if we considered all behavior as completely unchangeable we couldn't even improve the locking 😅 .
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's what I meant |
||
| this.eventSupport.removeGlobalHandler(event, handler); | ||
| } | ||
| return this; | ||
| } | ||
|
|
||
| void removeHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) { | ||
| try (AutoCloseableLock __ = lock.writeLockAutoCloseable()) { | ||
| try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) { | ||
| eventSupport.removeClientHandler(domain, event, handler); | ||
| } | ||
| } | ||
|
|
||
| void addHandler(String domain, ProviderEvent event, Consumer<EventDetails> 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<String> 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<String> allDomainNames = eventSupport.getAllDomainNames(); | ||
| Set<String> 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))); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import java.util.function.Consumer; | ||
| import lombok.Getter; | ||
| import lombok.extern.slf4j.Slf4j; | ||
|
|
@@ -49,8 +50,7 @@ public class OpenFeatureClient implements Client { | |
| private final List<Hook> clientHooks; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ya it's final unlike the Do you see any reason not to?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
| private final HookSupport hookSupport; | ||
| AutoCloseableReentrantReadWriteLock hooksLock = new AutoCloseableReentrantReadWriteLock(); | ||
| AutoCloseableReentrantReadWriteLock contextLock = new AutoCloseableReentrantReadWriteLock(); | ||
| private EvaluationContext evaluationContext; | ||
| private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>(); | ||
|
|
||
| /** | ||
| * Deprecated public constructor. Use OpenFeature.API.getClient() instead. | ||
|
|
@@ -125,7 +125,7 @@ public void track(String trackingEventName, EvaluationContext context, TrackingE | |
| */ | ||
| @Override | ||
| public OpenFeatureClient addHooks(Hook... hooks) { | ||
| try (AutoCloseableLock __ = this.hooksLock.writeLockAutoCloseable()) { | ||
| try (AutoCloseableLock ignored = this.hooksLock.writeLockAutoCloseable()) { | ||
| this.clientHooks.addAll(Arrays.asList(hooks)); | ||
| } | ||
| return this; | ||
|
|
@@ -136,8 +136,12 @@ public OpenFeatureClient addHooks(Hook... hooks) { | |
| */ | ||
| @Override | ||
| public List<Hook> getHooks() { | ||
| try (AutoCloseableLock __ = this.hooksLock.readLockAutoCloseable()) { | ||
| return this.clientHooks; | ||
| try (AutoCloseableLock ignored = this.hooksLock.readLockAutoCloseable()) { | ||
| if (this.clientHooks.isEmpty()) { | ||
| return Collections.emptyList(); | ||
| } else { | ||
| return new ArrayList<>(this.clientHooks); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will create some additional object churn per evaluation, I guess you're worried about exposing this mutable state?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't want to expose a reference to the list, as this would effectively remove the lock entirely. At least the |
||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -146,9 +150,7 @@ public List<Hook> getHooks() { | |
| */ | ||
| @Override | ||
| public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContext) { | ||
| try (AutoCloseableLock __ = contextLock.writeLockAutoCloseable()) { | ||
| this.evaluationContext = evaluationContext; | ||
| } | ||
| this.evaluationContext.set(evaluationContext); | ||
| return this; | ||
| } | ||
|
|
||
|
|
@@ -157,9 +159,7 @@ public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContex | |
| */ | ||
| @Override | ||
| public EvaluationContext getEvaluationContext() { | ||
| try (AutoCloseableLock __ = contextLock.readLockAutoCloseable()) { | ||
| return this.evaluationContext; | ||
| } | ||
| return this.evaluationContext.get(); | ||
| } | ||
|
|
||
| private <T> FlagEvaluationDetails<T> evaluateFlag( | ||
|
|
@@ -179,8 +179,10 @@ private <T> FlagEvaluationDetails<T> evaluateFlag( | |
| provider = stateManager.getProvider(); | ||
| ProviderState state = stateManager.getState(); | ||
|
|
||
| mergedHooks = ObjectUtils.merge( | ||
| provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getHooks()); | ||
| try (AutoCloseableLock ignored = this.hooksLock.readLockAutoCloseable()) { | ||
| mergedHooks = ObjectUtils.merge( | ||
| provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getHooks()); | ||
| } | ||
|
|
||
| EvaluationContext mergedCtx = hookSupport.beforeHooks( | ||
| type, | ||
|
|
@@ -264,7 +266,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); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This
ArrayListcould also be replaced with some concurrent data structure, further reducing the usage of locks and object churn, but it might be slower in some cases