/*
* 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.core.threads.EventLoop;
import net.openhft.chronicle.core.threads.InvalidEventHandlerException;
import net.openhft.chronicle.core.util.ObjectUtils;
import net.openhft.chronicle.engine.api.map.KeyValueStore;
import net.openhft.chronicle.engine.api.map.MapEvent;
import net.openhft.chronicle.engine.api.map.MapView;
import net.openhft.chronicle.engine.api.pubsub.ISubscriber;
import net.openhft.chronicle.engine.api.pubsub.InvalidSubscriberException;
import net.openhft.chronicle.engine.api.pubsub.Subscriber;
import net.openhft.chronicle.engine.api.pubsub.TopicSubscriber;
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.query.Filter;
import net.openhft.chronicle.engine.tree.ChronicleQueueView;
import net.openhft.chronicle.engine.tree.QueueView;
import net.openhft.chronicle.network.api.session.SessionDetails;
import net.openhft.chronicle.network.api.session.SessionProvider;
import net.openhft.chronicle.queue.ExcerptTailer;
import net.openhft.chronicle.wire.WireKey;
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 java.util.concurrent.atomic.AtomicBoolean;
/**
* Created by peter on 22/05/15.
*/
// todo review thread safety
public class QueueObjectSubscription<T, M> implements ObjectSubscription<T, M> {
private static final Logger LOG = LoggerFactory.getLogger(QueueObjectSubscription.class);
private final Set<TopicSubscriber<T, M>> topicSubscribers = new CopyOnWriteArraySet<>();
private final Set<Subscriber<ExcerptTailer>> subscribers = new CopyOnWriteArraySet<>();
private final Set<EventConsumer<T, M>> downstream = new CopyOnWriteArraySet<>();
@Nullable
private final SessionProvider sessionProvider;
@Nullable
private final Asset asset;
private final Map<Subscriber, Subscriber> subscriptionDelegate = new IdentityHashMap<>();
private final Class<T> topicType;
@Nullable
private Map<String, SubscriptionStat> subscriptionMonitoringMap = null;
private EventLoop eventLoop;
public QueueObjectSubscription(@NotNull RequestContext requestContext, @NotNull Asset asset) {
this(requestContext.topicType(), requestContext.viewType(), asset);
}
public QueueObjectSubscription(Class topicType, @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);
eventLoop = asset.root().acquireView(EventLoop.class);
this.topicType = topicType;
}
@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 (RuntimeException e) {
Jvm.warn().on(getClass(), "Failed to send end of subscription", e);
}
}
@Override
public void setKvStore(KeyValueStore<T, M> kvStore) {
// throw new UnsupportedOperationException();
}
@Override
public void notifyEvent(MapEvent<T, M> changeEvent) {
// throw new UnsupportedOperationException();
}
@Override
public int entrySubscriberCount() {
return 0;
}
@Override
public int topicSubscriberCount() {
return topicSubscribers.size();
}
@Override
public boolean hasSubscribers() {
return !topicSubscribers.isEmpty() || !subscribers.isEmpty()
|| !downstream.isEmpty()
|| asset.hasChildren();
}
@Override
public boolean needsPrevious() {
// todo optimise this to reduce false positives.
return !subscribers.isEmpty() || !downstream.isEmpty();
}
@Override
public void registerSubscriber(@NotNull final RequestContext rc,
@NotNull final Subscriber subscriber,
@NotNull final Filter filter) {
@NotNull final QueueView<T, M> chronicleQueue = asset.acquireView(QueueView.class, rc);
@Nullable final T topic = ObjectUtils.convertTo(topicType, rc.name());
eventLoop.addHandler(() -> {
QueueView.Excerpt<T, M> excerpt = chronicleQueue.getExcerpt(topic);
if (excerpt == null)
return false;
final M e = excerpt.message();
if (e == null)
return false;
subscriber.accept(e);
return true;
});
}
@Override
public void registerTopicSubscriber(@NotNull RequestContext rc, @NotNull final TopicSubscriber<T, M> subscriber) {
addToStats("topicSubscription");
topicSubscribers.add(subscriber);
@NotNull AtomicBoolean terminate = new AtomicBoolean();
@NotNull final ChronicleQueueView<T, M> chronicleQueue = (ChronicleQueueView) asset.acquireView
(QueueView.class, rc);
QueueView.Tailer<T, M> iterator = chronicleQueue.tailer();
eventLoop.addHandler(() -> {
// this will be set to true if onMessage throws InvalidSubscriberException
if (terminate.get())
throw new InvalidEventHandlerException();
boolean busy = false;
long start = System.nanoTime();
do {
@Nullable final QueueView.Excerpt<T, M> next = iterator.read();
if (next == null)
return busy;
try {
M message = next.message();
T topic = next.topic();
subscriber.onMessage(topic, message);
} catch (InvalidSubscriberException e) {
topicSubscribers.add(subscriber);
terminate.set(true);
} catch (RuntimeException e) {
Jvm.warn().on(getClass(), e);
terminate.set(true);
}
busy = true;
} while (System.nanoTime() - start < 5000);
return busy;
});
}
@NotNull
private T toT(@NotNull CharSequence eventName) {
if (topicType == CharSequence.class)
return (T) eventName;
else if (topicType == String.class)
return (T) eventName.toString();
else if (topicType == WireKey.class)
return (T) (WireKey) (() -> eventName.toString());
else
throw new UnsupportedOperationException("unable to convert " + eventName + " to type " + topicType);
}
@Override
public void registerDownstream(@NotNull EventConsumer<T, M> subscription) {
downstream.add(subscription);
}
public void unregisterDownstream(EventConsumer<T, M> subscription) {
downstream.remove(subscription);
}
@Override
public void unregisterSubscriber(@NotNull Subscriber subscriber) {
final Subscriber delegate = subscriptionDelegate.get(subscriber);
@NotNull final Subscriber s = delegate != null ? delegate : subscriber;
boolean subscription = subscribers.remove(s);
if (subscription) removeFromStats("subscription");
s.onEndOfSubscription();
}
@Override
public int keySubscriberCount() {
throw new UnsupportedOperationException();
}
@Override
public void registerKeySubscriber(@NotNull RequestContext rc, @NotNull Subscriber<T> subscriber, @NotNull Filter<T> filter) {
throw new UnsupportedOperationException();
}
@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);
}
}
}
}