ru.fix.dynamic.property.api.AtomicProperty Maven / Gradle / Ivy
Show all versions of dynamic-property-api Show documentation
package ru.fix.dynamic.property.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ru.fix.stdlib.reference.CleanableWeakReference;
import ru.fix.stdlib.reference.ReferenceCleaner;
import javax.annotation.Nonnull;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
/**
* Provides simple implementation for DynamicProperty.
* Allows to manually provide DynamicProperty in tests.
* Be aware that this class does not synchronize value update and listener invocations.
* Listeners will be invoked during {@link #set(Object)} in the same thread.
*
* {@code
* MyService(DynamicProperty property){
* property.addAndCallListener(value -> initialize(value))
* }
* property = AtomicProperty(1)
* MyService myService = MyService(property)
* result = myService.doWork()
* assertThat(result, ...)
* }
*
* Be aware that listeners invoked in same thread, that calls {@link #set(Object)}
* This class does not provide any synchronization except holding volatile variable in order to stay lightweight
* Here is an example that leads to race condition:
*
*
{@code
* MyService(DynamicProperty property){
* property.addAndCallListener(value -> initialize(value))
* }
* //DO NOT DO THAT
* property = AtomicProperty(1)
* new Thread{ property.set(2) }.start()
* MyService myService = MyService(property)
* //Problem 1. It is not clear what value MyService will see 1 or 2 or both.
* //Problem 2. MyService can end up with a stale value of 1.
* //Problem 3. MyService method initialize could be invoked twice concurrently.
* //Problem 4. MyService method initialize could be invoked twice and see wrong order of changes:
* // at first it will see 2 and then 1.
* }
*
* In order to prevent all four problems user of AtomicProperty should organize proper thread safe usages of the property.
*
* @author Kamil Asfandiyarov
*/
public class AtomicProperty implements DynamicProperty {
private static final Logger log = LoggerFactory.getLogger(AtomicProperty.class);
private final Object changeValueAndAddListenerLock = new Object();
private final AtomicReference valueHolder = new AtomicReference<>();
private final Set>> subscriptions =
Collections.newSetFromMap(new ConcurrentHashMap<>());
private String name = null;
private final ReferenceCleaner referenceCleaner = ReferenceCleaner.getInstance();
public AtomicProperty() {
}
public AtomicProperty(T value) {
this.valueHolder.set(value);
}
public void setName(String name) {
this.name = name;
}
/**
* @param newValue
* @return old value
*/
public T set(T newValue) {
T oldValue;
synchronized (changeValueAndAddListenerLock) {
oldValue = valueHolder.getAndSet(newValue);
subscriptions.forEach(ref -> {
try {
var subscription = ref.get();
if (subscription != null) {
subscription.listener.onPropertyChanged(oldValue, newValue);
}
} catch (Exception exc) {
log.error("Failed to notify listener on property change." +
" Property name {}, old value {}, new value {}.",
name, oldValue, newValue, exc);
}
});
}
subscriptions.removeIf(ref -> ref.get() == null);
return oldValue;
}
@Override
public T get() {
return valueHolder.get();
}
private static class Subscription implements PropertySubscription {
private final AtomicProperty sourceProperty;
private PropertyListener listener;
private CleanableWeakReference> attachedSubscriptionReference;
Subscription(AtomicProperty sourceProperty) {
this.sourceProperty = sourceProperty;
}
@Override
public T get() {
return sourceProperty.get();
}
@Override
public PropertySubscription setAndCallListener(@Nonnull PropertyListener listener) {
this.listener = listener;
this.sourceProperty.attachSubscriptionAndCallListener(this);
return this;
}
@Override
public void close() {
sourceProperty.detachSubscription(this);
}
}
@Override
@Nonnull
public PropertySubscription createSubscription() {
return new Subscription<>(this);
}
private void detachSubscription(Subscription subscription) {
if(subscription.attachedSubscriptionReference != null) {
subscriptions.remove(subscription.attachedSubscriptionReference);
subscription.attachedSubscriptionReference = null;
}
}
private void attachSubscriptionAndCallListener(Subscription subscription){
detachSubscription(subscription);
synchronized (changeValueAndAddListenerLock) {
if(subscription.attachedSubscriptionReference != null){
subscriptions.remove(subscription.attachedSubscriptionReference);
subscription.attachedSubscriptionReference.cancelCleaningOrder();
subscription.attachedSubscriptionReference = null;
}
CleanableWeakReference> cleanableWeakReference = referenceCleaner.register(
subscription,
null,
(reference, meta) -> subscriptions.remove(reference)
);
subscription.attachedSubscriptionReference = cleanableWeakReference;
subscriptions.add(cleanableWeakReference);
subscription.listener.onPropertyChanged(null, valueHolder.get());
}
}
@Override
public void close() {
subscriptions.clear();
}
@Override
public String toString() {
return "AtomicProperty(" + valueHolder.get() + ")";
}
}