/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.notification.internal;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.WeakHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.core.communication.api.PlatformService;
import de.rcenvironment.core.notification.Notification;
import de.rcenvironment.core.notification.NotificationHeader;
import de.rcenvironment.core.notification.NotificationService;
import de.rcenvironment.core.notification.NotificationSubscriber;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.toolkitbridge.transitional.StatsCounter;
import de.rcenvironment.core.utils.common.rpc.RemoteOperationException;
import de.rcenvironment.core.utils.common.security.AllowRemoteAccess;
import de.rcenvironment.toolkit.modules.concurrency.api.BatchAggregator;
import de.rcenvironment.toolkit.modules.concurrency.api.BatchProcessor;
/**
* Implementation of the {@link NotificationService}.
*
* @author Andre Nurzenski
* @author Doreen Seider
* @author Robert Mischke
*/
public class NotificationServiceImpl implements NotificationService {
private static final boolean TOPIC_STATISTICS_ENABLED = false;
/**
* Helper class to hold local information about subscribers. This includes a set of the subscribed topics, and a {@link BatchAggregator}
* to group messages to this subscriber.
*
* @author Robert Mischke
*/
private static final class LocalSubscriberMetaData {
private final Set<NotificationTopic> subscribedTopics;
private final BatchAggregator<Notification> batchAggregator;
/**
* @param batchAggregator the aggregator instance to use for this subscriber
*/
LocalSubscriberMetaData(BatchAggregator<Notification> batchAggregator) {
this.batchAggregator = batchAggregator;
this.subscribedTopics = new HashSet<NotificationTopic>();
}
/**
* Adds a {@link NotificationTopic} that this subscriber has registered for. Used via {@link #getSubscribedTopics()} to unsubscribe
* from all topics if necessary.
*
* @param topic the already-subscribed topic
*/
public void addSubscribedTopic(NotificationTopic topic) {
synchronized (subscribedTopics) {
subscribedTopics.add(topic);
}
}
/**
* Adds a {@link NotificationTopic} that this subscriber is no longer registered for.
*
* @param topic the topic to disconnect from this subscriber
*/
public boolean removeSubscribedTopic(NotificationTopic topic) {
synchronized (subscribedTopics) {
return subscribedTopics.remove(topic);
}
}
public Collection<NotificationTopic> getSubscribedTopics() {
synchronized (subscribedTopics) {
// copy to immutable collection to prevent concurrent modifications
return new ArrayList<NotificationTopic>(subscribedTopics);
}
}
public BatchAggregator<Notification> getBatchAggregator() {
return batchAggregator;
}
}
/**
* A {@link BatchProcessor} implementation that sends out batches of {@link Notification}s to a single {@link NotificationSubscriber}.
*
* @author Robert Mischke
*/
private final class NotificationBatchSender implements BatchProcessor<Notification> {
private NotificationSubscriber subscriber;
/**
* @param subscriber the subscriber to send received batches to
*/
NotificationBatchSender(NotificationSubscriber subscriber) {
this.subscriber = subscriber;
}
@Override
public void processBatch(List<Notification> batch) {
sendNotificationsToSubscriber(subscriber, batch);
}
}
// the maximum number of notifications to aggregate to a single batch
// NOTE: arbitrary value; adjust when useful/necessary
private static final int MAX_NOTIFICATION_BATCH_SIZE = 50;
// the maximum time a notification may be delayed by batch aggregation
// NOTE: arbitrary value; adjust when useful/necessary
private static final long MAX_NOTIFICATION_LATENCY = 100;
private static final Log LOGGER = LogFactory.getLog(NotificationServiceImpl.class);
/** Local topics. */
private Map<String, NotificationTopic> topics = Collections.synchronizedMap(new HashMap<String, NotificationTopic>());
/** Current number of all notifications. */
private Map<String, Long> currentNumbers = Collections.synchronizedMap(new HashMap<String, Long>());
/** Buffer sizes of all notifications. */
private Map<String, Integer> bufferSizes = Collections.synchronizedMap(new HashMap<String, Integer>());
/** Stored notifications. */
private Map<String, SortedMap<NotificationHeader, Notification>> allNotifications =
Collections.synchronizedMap(new HashMap<String, SortedMap<NotificationHeader, Notification>>());
private WeakHashMap<NotificationSubscriber, LocalSubscriberMetaData> subscriberMap =
new WeakHashMap<NotificationSubscriber, NotificationServiceImpl.LocalSubscriberMetaData>();
private PlatformService platformService;
protected void bindPlatformService(PlatformService newPlatformService) {
platformService = newPlatformService;
}
@Override
public void setBufferSize(String notificationId, int bufferSize) {
if (bufferSize != 0) {
bufferSizes.put(notificationId, new Integer(bufferSize));
// synchronize explicitly to avoid potential "lost update" problem
synchronized (allNotifications) {
if (!allNotifications.containsKey(notificationId)) {
allNotifications.put(notificationId, new TreeMap<NotificationHeader, Notification>());
}
}
}
}
@Override
public void removePublisher(String notificationId) {
synchronized (topics) {
NotificationTopic topic = getNotificationTopic(notificationId);
if (topic != null) {
topics.remove(topic.getName());
currentNumbers.remove(notificationId);
bufferSizes.remove(notificationId);
allNotifications.remove(notificationId);
}
}
}
@Override
public synchronized <T extends Serializable> void send(String notificationId, T notificationBody) {
if (TOPIC_STATISTICS_ENABLED) {
if (StatsCounter.isEnabled()) {
StatsCounter.count("Notifications sent by id", notificationId);
StatsCounter.countClass("Notifications sent by body type", notificationBody);
}
}
if (getNotificationTopic(notificationId) == null) {
registerNotificationTopic(notificationId);
}
Long currentEdition = currentNumbers.get(notificationId);
Notification notification = new Notification(notificationId, currentEdition.longValue() + 1,
platformService.getLocalInstanceNodeSessionId(), notificationBody);
SortedMap<NotificationHeader, Notification> notifications = allNotifications.get(notificationId);
if (notifications != null) {
Integer bufferSize = bufferSizes.get(notificationId);
if (bufferSize > 0 && notifications.size() >= bufferSize) {
if (notifications.remove(notifications.firstKey()) != null) {
notifications.put(notification.getHeader(), notification);
}
} else {
notifications.put(notification.getHeader(), notification);
}
}
for (NotificationTopic matchingTopic : getMatchingNotificationTopics(notificationId)) {
for (NotificationSubscriber subscriber : matchingTopic.getSubscribers()) {
if (TOPIC_STATISTICS_ENABLED) {
if (StatsCounter.isEnabled()) {
StatsCounter.count("Notifications enqueued by type", notificationId);
}
}
sendNotificationToSubscriber(notification, subscriber);
}
}
// TODO review: is this guaranteed to be consistent with asynchronous sending? -- misc_ro
currentNumbers.put(notificationId, notification.getHeader().getNumber());
}
private void sendNotificationToSubscriber(Notification notification, NotificationSubscriber subscriber) {
getLocalSubscriberMetaData(subscriber).getBatchAggregator().enqueue(notification);
}
@Override
@AllowRemoteAccess
public Map<String, Long> subscribe(String notificationId, NotificationSubscriber subscriber) {
Map<String, Long> lastNumbers = new HashMap<String, Long>();
NotificationTopic topic;
synchronized (topics) {
topic = getNotificationTopic(notificationId);
if (topic == null) {
topic = registerNotificationTopic(notificationId);
if (TOPIC_STATISTICS_ENABLED) {
if (StatsCounter.isEnabled()) {
StatsCounter.count("Register Topic", notificationId);
}
}
}
}
topic.add(subscriber);
getLocalSubscriberMetaData(subscriber).addSubscribedTopic(topic);
synchronized (currentNumbers) {
for (String tmpId : currentNumbers.keySet()) {
if (tmpId.matches(notificationId)) {
lastNumbers.put(tmpId, currentNumbers.get(tmpId));
}
}
}
return lastNumbers;
}
@Override
@AllowRemoteAccess
public void unsubscribe(String notificationId, NotificationSubscriber subscriber) {
synchronized (topics) {
NotificationTopic topic = getNotificationTopic(notificationId);
if (topic != null) {
topic.remove(subscriber);
getLocalSubscriberMetaData(subscriber).removeSubscribedTopic(topic);
}
}
}
@Override
public Notification getNotification(NotificationHeader header) {
Notification notification = null;
Map<NotificationHeader, Notification> notifications = allNotifications.get(header.getNotificationIdentifier());
if (notifications != null) {
notification = notifications.get(header);
}
return notification;
}
@Override
public Map<String, SortedSet<NotificationHeader>> getNotificationHeaders(String notificationId) {
Map<String, SortedSet<NotificationHeader>> allHeaders = new HashMap<String, SortedSet<NotificationHeader>>();
// note: access to iterators of synchronized maps must be synchronized explicitly
synchronized (allNotifications) {
// TODO iterating over map entries would probably be more efficient
for (String tmpId : allNotifications.keySet()) {
if (tmpId.matches(notificationId)) {
Map<NotificationHeader, Notification> notifications = allNotifications.get(tmpId);
SortedSet<NotificationHeader> headers = new TreeSet<NotificationHeader>(notifications.keySet());
allHeaders.put(tmpId, headers);
}
}
}
return allHeaders;
}
@Override
@AllowRemoteAccess
public Map<String, List<Notification>> getNotifications(String notificationId) {
Map<String, List<Notification>> allNotificationsToGet = new HashMap<String, List<Notification>>();
// note: access to iterators of synchronized maps must be synchronized explicitly
synchronized (allNotifications) {
// TODO iterating over map entries would probably be more efficient
for (String tmpId : allNotifications.keySet()) {
if (tmpId.matches(notificationId)) {
Map<NotificationHeader, Notification> notifications = allNotifications.get(tmpId);
List<Notification> notificationsToGet = new ArrayList<Notification>(notifications.values());
allNotificationsToGet.put(tmpId, notificationsToGet);
}
}
}
return allNotificationsToGet;
}
/**
* Sends a single {@link Notification} to a {@link NotificationSubscriber}.
*
* @param subscriber the subscriber to send the notification to
* @param matchingTopic the matching topic that caused the subscriber to receive this notification
* @param notifications the notifications, ie the actual content
*/
private void sendNotificationsToSubscriber(NotificationSubscriber subscriber, List<Notification> notifications) {
try {
try {
subscriber.receiveBatchedNotifications(notifications);
} catch (RuntimeException e) {
// TODO >=8.0.0: safeguard code added in 7.0.0 transition; remove if never observed in 7.x cycle
LOGGER.error("Unexpected RTE thrown from receiveBatchedNotifications()", e);
throw new RemoteOperationException(e.toString());
}
} catch (RemoteOperationException e) {
// not much information available, so use identity to tell subscribers apart in log
int subscriberIdentity = System.identityHashCode(subscriber);
Collection<NotificationTopic> subscribedTopics = getLocalSubscriberMetaData(subscriber).getSubscribedTopics();
if (subscribedTopics.isEmpty()) {
LOGGER.debug("Tried to remove subscriber " + subscriberIdentity
+ " after a callback failure but it had no (or no more) topics to unsubscribe from; triggering error: " + e.toString());
} else {
for (NotificationTopic topic : subscribedTopics) {
unsubscribe(topic.getName(), subscriber);
LOGGER.debug("Removed subscriber " + subscriberIdentity + " from topic "
+ topic.getName() + " after a callback failure: " + e.toString());
}
}
}
}
private NotificationTopic registerNotificationTopic(String notificationId) {
NotificationTopic topic = new NotificationTopic(notificationId);
synchronized (topics) {
topics.put(topic.getName(), topic);
}
currentNumbers.put(notificationId, new Long(NO_MISSED));
return topic;
}
private NotificationTopic getNotificationTopic(String notificationId) {
synchronized (topics) {
return topics.get(notificationId);
}
}
private LocalSubscriberMetaData getLocalSubscriberMetaData(NotificationSubscriber subscriber) {
synchronized (subscriberMap) {
LocalSubscriberMetaData metaData = subscriberMap.get(subscriber);
if (metaData == null) {
final BatchProcessor<Notification> batchProcessor = new NotificationBatchSender(subscriber);
final BatchAggregator<Notification> batchAggregator =
ConcurrencyUtils.getFactory().createBatchAggregator(MAX_NOTIFICATION_BATCH_SIZE, MAX_NOTIFICATION_LATENCY,
batchProcessor);
metaData = new LocalSubscriberMetaData(batchAggregator);
subscriberMap.put(subscriber, metaData);
}
return metaData;
}
}
private Set<NotificationTopic> getMatchingNotificationTopics(String currentNotificationId) {
// TODO (p2) >8.0.0: this is performed on every send() call, and is quite CPU and GC intensive; rework approach
Set<NotificationTopic> matchingTopics = new HashSet<NotificationTopic>();
synchronized (topics) {
for (NotificationTopic topic : topics.values()) {
if (topic.getNotificationIdFilter().matcher(currentNotificationId).matches()) {
matchingTopics.add(topic);
}
}
return matchingTopics;
}
}
}