/*
* Copyright 2016 higherfrequencytrading.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package net.openhft.chronicle.engine.map;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.engine.api.map.KeyValueStore;
import net.openhft.chronicle.engine.api.map.KeyValueStore.Entry;
import net.openhft.chronicle.engine.api.map.MapEvent;
import net.openhft.chronicle.engine.api.map.MapView;
import net.openhft.chronicle.engine.api.pubsub.*;
import net.openhft.chronicle.engine.api.tree.Asset;
import net.openhft.chronicle.engine.api.tree.RequestContext;
import net.openhft.chronicle.engine.cfg.SubscriptionStat;
import net.openhft.chronicle.engine.pubsub.MapSimpleSubscription;
import net.openhft.chronicle.engine.pubsub.SimpleSubscription;
import net.openhft.chronicle.engine.query.Filter;
import net.openhft.chronicle.network.api.session.SessionDetails;
import net.openhft.chronicle.network.api.session.SessionProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalTime;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import static java.lang.Boolean.TRUE;
import static net.openhft.chronicle.engine.api.pubsub.SubscriptionConsumer.notifyEachSubscriber;
/**
* Created by peter on 22/05/15.
*/
public class MapKVSSubscription<K, V> implements ObjectSubscription<K, V>,
RawKVSSubscription<K, V> {
private static final Logger LOG = LoggerFactory.getLogger(MapKVSSubscription.class);
private final Set<TopicSubscriber<K, V>> topicSubscribers = new CopyOnWriteArraySet<>();
private final Set<Subscriber<MapEvent<K, V>>> subscribers = new CopyOnWriteArraySet<>();
private final Set<Subscriber<K>> keySubscribers = new CopyOnWriteArraySet<>();
private final Set<EventConsumer<K, V>> downstream = new CopyOnWriteArraySet<>();
@Nullable
private final SessionProvider sessionProvider;
@Nullable
private final Asset asset;
private final Map<Subscriber, Subscriber> subscriptionDelegate = new IdentityHashMap<>();
private KeyValueStore<K, V> kvStore;
@Nullable
private Map<String, SubscriptionStat> subscriptionMonitoringMap = null;
public MapKVSSubscription(@NotNull RequestContext requestContext, @NotNull Asset asset) {
this(requestContext.viewType(), asset);
}
public MapKVSSubscription(@Nullable Class viewType, @Nullable Asset asset) {
this.asset = asset;
if (viewType != null && asset != null)
asset.addView(viewType, this);
sessionProvider = asset == null ? null : asset.findView(SessionProvider.class);
}
@Override
public void close() {
notifyEndOfSubscription(topicSubscribers);
notifyEndOfSubscription(subscribers);
notifyEndOfSubscription(keySubscribers);
notifyEndOfSubscription(downstream);
}
@Override
public void onEndOfSubscription() {
throw new UnsupportedOperationException("todo");
}
private void notifyEndOfSubscription(@NotNull Set<? extends ISubscriber> subscribers) {
subscribers.forEach(this::notifyEndOfSubscription);
subscribers.clear();
}
private void notifyEndOfSubscription(@NotNull ISubscriber subscriber) {
try {
subscriber.onEndOfSubscription();
} catch (Exception e) {
Jvm.warn().on(getClass(), "Failed to send endOfSubscription", e);
}
}
@Override
public boolean keyedView() {
return kvStore != null;
}
@Override
public void setKvStore(KeyValueStore<K, V> kvStore) {
this.kvStore = kvStore;
}
@Override
public void notifyEvent(@NotNull MapEvent<K, V> changeEvent) {
if (hasSubscribers())
notifyEvent0(changeEvent);
}
@Override
public int keySubscriberCount() {
return keySubscribers.size();
}
@Override
public int entrySubscriberCount() {
return subscribers.size();
}
@Override
public int topicSubscriberCount() {
return topicSubscribers.size();
}
@Override
public boolean hasSubscribers() {
return !keySubscribers.isEmpty() || hasValueSubscribers();
}
@Override
public boolean hasValueSubscribers() {
return !topicSubscribers.isEmpty() || !subscribers.isEmpty()
|| !downstream.isEmpty() || asset.hasChildren();
}
private void notifyEvent0(@NotNull MapEvent<K, V> changeEvent) {
notifyEvent1(changeEvent);
notifyEventToChild(changeEvent);
}
private void notifyEvent1(@NotNull MapEvent<K, V> changeEvent) {
K key = changeEvent.getKey();
if (!topicSubscribers.isEmpty()) {
V value = changeEvent.getValue();
notifyEachSubscriber(topicSubscribers, ts -> ts.onMessage(key, value));
}
if (!subscribers.isEmpty()) {
notifyEachSubscriber(subscribers, s -> s.onMessage(changeEvent));
}
if (!keySubscribers.isEmpty()) {
notifyEachSubscriber(keySubscribers, s -> s.onMessage(key));
}
if (!downstream.isEmpty()) {
notifyEachSubscriber(downstream, d -> d.notifyEvent(changeEvent));
}
}
private void notifyEventToChild(@NotNull MapEvent<K, V> changeEvent) {
K key = changeEvent.getKey();
if (asset == null || !asset.hasChildren()) {
return;
}
String keyStr = key.toString();
Asset child = asset.getChild(keyStr);
if (child == null) {
return;
}
@Nullable SubscriptionCollection subscription = child.subscription(false);
if (subscription instanceof MapSimpleSubscription) {
// System.out.println(changeEvent.toString().substring(0, 100));
((SimpleSubscription) subscription).notifyMessage(changeEvent.getValue());
}
}
@Override
public boolean needsPrevious() {
// todo optimise this to reduce false positives.
return !subscribers.isEmpty() || !downstream.isEmpty();
}
@Override
public void registerSubscriber(@NotNull RequestContext rc,
@NotNull Subscriber subscriber,
@NotNull Filter filter) {
@NotNull Class eClass = rc.elementType();
if (eClass == Entry.class || eClass == MapEvent.class)
registerSubscriber0(rc, subscriber, filter);
else
registerKeySubscriber(rc, subscriber, filter);
}
@NotNull
private <T> Subscriber<T> subscriber(@NotNull Subscriber<T> subscriber,
@NotNull Filter<T> filter) {
@NotNull final Subscriber<T> sub;
if (filter.isEmpty())
sub = subscriber;
else {
sub = new Filter.FilteredSubscriber<T>(filter, subscriber);
subscriptionDelegate.put(subscriber, sub);
}
return sub;
}
private void registerSubscriber0(@NotNull RequestContext rc,
@NotNull Subscriber<MapEvent<K, V>> subscriber,
@NotNull Filter<MapEvent<K, V>> filter) {
addToStats("subscription");
@NotNull final Subscriber<MapEvent<K, V>> sub = subscriber(subscriber, filter);
this.subscribers.add(sub);
@Nullable Boolean bootstrap = rc.bootstrap();
if (bootstrap != Boolean.FALSE && kvStore != null) {
try {
for (int i = 0; i < kvStore.segments(); i++)
kvStore.entriesFor(i, sub::onMessage);
if (TRUE.equals(rc.endSubscriptionAfterBootstrap())) {
sub.onEndOfSubscription();
LOG.info("onEndOfSubscription");
this.subscribers.remove(sub);
}
} catch (InvalidSubscriberException e) {
this.subscribers.remove(sub);
}
}
}
@Override
public void registerKeySubscriber(@NotNull RequestContext rc,
@NotNull Subscriber<K> subscriber,
@NotNull Filter<K> filter) {
addToStats("keySubscription");
@Nullable final Boolean bootstrap = rc.bootstrap();
@NotNull final Subscriber<K> sub = subscriber(subscriber, filter);
keySubscribers.add(sub);
if (bootstrap != Boolean.FALSE && kvStore != null) {
try {
for (int i = 0; i < kvStore.segments(); i++)
kvStore.keysFor(i, sub::onMessage);
if (TRUE.equals(rc.endSubscriptionAfterBootstrap())) {
sub.onEndOfSubscription();
keySubscribers.remove(sub);
}
} catch (InvalidSubscriberException e) {
keySubscribers.remove(sub);
}
}
}
@Override
public void registerTopicSubscriber(@NotNull RequestContext rc, @NotNull TopicSubscriber subscriber) {
addToStats("topicSubscription");
@Nullable Boolean bootstrap = rc.bootstrap();
topicSubscribers.add((TopicSubscriber<K, V>) subscriber);
if (bootstrap != Boolean.FALSE && kvStore != null) {
try {
for (int i = 0; i < kvStore.segments(); i++)
kvStore.entriesFor(i, e -> subscriber.onMessage(e.getKey(), e.getValue()));
} catch (InvalidSubscriberException dontAdd) {
topicSubscribers.remove(subscriber);
}
}
}
@Override
public void registerDownstream(@NotNull EventConsumer<K, V> subscription) {
downstream.add(subscription);
}
public void unregisterDownstream(EventConsumer<K, V> subscription) {
downstream.remove(subscription);
}
@Override
public void unregisterSubscriber(@NotNull Subscriber subscriber) {
final Subscriber delegate = subscriptionDelegate.remove(subscriber);
@NotNull final Subscriber s = delegate != null ? delegate : subscriber;
boolean subscription = subscribers.remove(s);
boolean keySubscription = keySubscribers.remove(s);
if (subscription) removeFromStats("subscription");
if (keySubscription) removeFromStats("keySubscription");
//noinspection ConstantConditions
if (s != null)
s.onEndOfSubscription();
}
@Override
public void unregisterTopicSubscriber(@NotNull TopicSubscriber subscriber) {
topicSubscribers.remove(subscriber);
removeFromStats("topicSubscription");
subscriber.onEndOfSubscription();
}
//Needs some refactoring - need a definitive way of knowing when this map should become available
//3 combinations, not lookedUP, exists or does not exist
@Nullable
private Map getSubscriptionMap() {
if (subscriptionMonitoringMap != null) return subscriptionMonitoringMap;
@Nullable Asset subscriptionAsset = asset.root().getAsset("proc/subscriptions");
if (subscriptionAsset != null && subscriptionAsset.getView(MapView.class) != null) {
subscriptionMonitoringMap = subscriptionAsset.getView(MapView.class);
}
return subscriptionMonitoringMap;
}
private void addToStats(String subType) {
if (sessionProvider == null) return;
@Nullable SessionDetails sessionDetails = sessionProvider.get();
if (sessionDetails != null) {
@Nullable String userId = sessionDetails.userId();
@Nullable Map<String, SubscriptionStat> subStats = getSubscriptionMap();
if (subStats != null) {
SubscriptionStat stat = subStats.get(userId + "~" + subType);
if (stat == null) {
stat = new SubscriptionStat();
stat.setFirstSubscribed(LocalTime.now());
}
stat.setTotalSubscriptions(stat.getTotalSubscriptions() + 1);
stat.setActiveSubscriptions(stat.getActiveSubscriptions() + 1);
stat.setRecentlySubscribed(LocalTime.now());
subStats.put(userId + "~" + subType, stat);
}
}
}
private void removeFromStats(String subType) {
if (sessionProvider == null) return;
@Nullable SessionDetails sessionDetails = sessionProvider.get();
if (sessionDetails != null) {
@Nullable String userId = sessionDetails.userId();
@Nullable Map<String, SubscriptionStat> subStats = getSubscriptionMap();
if (subStats != null) {
SubscriptionStat stat = subStats.get(userId + "~" + subType);
if (stat == null) {
throw new AssertionError("There should be an active subscription");
}
stat.setActiveSubscriptions(stat.getActiveSubscriptions() - 1);
stat.setRecentlySubscribed(LocalTime.now());
subStats.put(userId + "~" + subType, stat);
}
}
}
}