/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.brooklyn.core.mgmt.internal;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.elvis;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.join;
import static org.apache.brooklyn.util.JavaGroovyEquivalents.mapOf;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.mgmt.ExecutionManager;
import org.apache.brooklyn.api.mgmt.SubscriptionHandle;
import org.apache.brooklyn.api.mgmt.SubscriptionManager;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.api.sensor.Sensor;
import org.apache.brooklyn.api.sensor.SensorEvent;
import org.apache.brooklyn.api.sensor.SensorEventListener;
import org.apache.brooklyn.core.entity.Entities;
import org.apache.brooklyn.core.sensor.BasicSensorEvent;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.task.BasicExecutionManager;
import org.apache.brooklyn.util.core.task.SingleThreadedScheduler;
import org.apache.brooklyn.util.text.Identifiers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimaps;
/**
* A {@link SubscriptionManager} that stores subscription details locally.
*/
public class LocalSubscriptionManager extends AbstractSubscriptionManager {
private static final Logger LOG = LoggerFactory.getLogger(LocalSubscriptionManager.class);
protected final ExecutionManager em;
private final String tostring = "SubscriptionContext("+Identifiers.getBase64IdFromValue(System.identityHashCode(this), 5)+")";
private final AtomicLong totalEventsPublishedCount = new AtomicLong();
private final AtomicLong totalEventsDeliveredCount = new AtomicLong();
@SuppressWarnings("rawtypes")
protected final ConcurrentMap<String, Subscription> allSubscriptions = new ConcurrentHashMap<String, Subscription>();
@SuppressWarnings("rawtypes")
protected final ConcurrentMap<Object, Set<Subscription>> subscriptionsBySubscriber = new ConcurrentHashMap<Object, Set<Subscription>>();
@SuppressWarnings("rawtypes")
protected final ConcurrentMap<Object, Set<Subscription>> subscriptionsByToken = new ConcurrentHashMap<Object, Set<Subscription>>();
public LocalSubscriptionManager(ExecutionManager m) {
this.em = m;
}
public long getNumSubscriptions() {
return allSubscriptions.size();
}
public long getTotalEventsPublished() {
return totalEventsPublishedCount.get();
}
public long getTotalEventsDelivered() {
return totalEventsDeliveredCount.get();
}
@SuppressWarnings("unchecked")
protected synchronized <T> SubscriptionHandle subscribe(Map<String, Object> flags, final Subscription<T> s) {
Entity producer = s.producer;
Sensor<T> sensor= s.sensor;
s.subscriber = getSubscriber(flags, s);
if (flags.containsKey("subscriberExecutionManagerTag")) {
s.subscriberExecutionManagerTag = flags.remove("subscriberExecutionManagerTag");
s.subscriberExecutionManagerTagSupplied = true;
} else {
s.subscriberExecutionManagerTag =
s.subscriber instanceof Entity ? "subscription-delivery-entity-"+((Entity)s.subscriber).getId()+"["+s.subscriber+"]" :
s.subscriber instanceof String ? "subscription-delivery-string["+s.subscriber+"]" :
"subscription-delivery-object["+s.subscriber+"]";
s.subscriberExecutionManagerTagSupplied = false;
}
s.eventFilter = (Predicate<SensorEvent<T>>) flags.remove("eventFilter");
boolean notifyOfInitialValue = Boolean.TRUE.equals(flags.remove("notifyOfInitialValue"));
s.flags = flags;
if (LOG.isDebugEnabled()) LOG.debug("Creating subscription {} for {} on {} {} in {}", new Object[] {s.id, s.subscriber, producer, sensor, this});
allSubscriptions.put(s.id, s);
addToMapOfSets(subscriptionsByToken, makeEntitySensorToken(s.producer, s.sensor), s);
if (s.subscriber!=null) {
addToMapOfSets(subscriptionsBySubscriber, s.subscriber, s);
}
if (!s.subscriberExecutionManagerTagSupplied && s.subscriberExecutionManagerTag!=null) {
((BasicExecutionManager) em).setTaskSchedulerForTag(s.subscriberExecutionManagerTag, SingleThreadedScheduler.class);
}
if (notifyOfInitialValue) {
if (producer == null) {
LOG.warn("Cannot notifyOfInitialValue for subscription with wildcard producer: "+s);
} else if (sensor == null) {
LOG.warn("Cannot notifyOfInitialValue for subscription with wilcard sensor: "+s);
} else if (!(sensor instanceof AttributeSensor)) {
LOG.warn("Cannot notifyOfInitialValue for subscription with non-attribute sensor: "+s);
} else {
if (LOG.isTraceEnabled()) LOG.trace("sending initial value of {} -> {} to {}", new Object[] {s.producer, s.sensor, s});
Map<String, Object> tagsMap = MutableMap.of("tag", s.subscriberExecutionManagerTag);
em.submit(tagsMap, new Runnable() {
@Override
public String toString() {
return "LSM.publishInitialValue("+s.producer+", "+s.sensor+")";
}
public void run() {
Object val = s.producer.getAttribute((AttributeSensor<?>) s.sensor);
@SuppressWarnings("rawtypes") // TODO s.listener.onEvent gives compilation error if try to use <T>
SensorEvent event = new BasicSensorEvent(s.sensor, s.producer, val);
if (s.eventFilter!=null && !s.eventFilter.apply(event))
return;
try {
s.listener.onEvent(event);
} catch (Throwable t) {
if (event!=null && event.getSource()!=null && Entities.isNoLongerManaged(event.getSource())) {
LOG.debug("Error processing initial-value subscription to "+LocalSubscriptionManager.this+", after entity unmanaged: "+t, t);
} else {
LOG.warn("Error processing initial-value subscription to "+LocalSubscriptionManager.this+": "+t, t);
}
}
}});
}
}
return s;
}
@SuppressWarnings("unchecked")
public Set<SubscriptionHandle> getSubscriptionsForSubscriber(Object subscriber) {
return (Set<SubscriptionHandle>) ((Set<?>) elvis(subscriptionsBySubscriber.get(subscriber), Collections.emptySet()));
}
public synchronized Set<SubscriptionHandle> getSubscriptionsForEntitySensor(Entity source, Sensor<?> sensor) {
Set<SubscriptionHandle> subscriptions = new LinkedHashSet<SubscriptionHandle>();
subscriptions.addAll(elvis(subscriptionsByToken.get(makeEntitySensorToken(source, sensor)), Collections.emptySet()));
subscriptions.addAll(elvis(subscriptionsByToken.get(makeEntitySensorToken(null, sensor)), Collections.emptySet()));
subscriptions.addAll(elvis(subscriptionsByToken.get(makeEntitySensorToken(source, null)), Collections.emptySet()));
subscriptions.addAll(elvis(subscriptionsByToken.get(makeEntitySensorToken(null, null)), Collections.emptySet()));
return subscriptions;
}
/**
* Unsubscribe the given subscription id.
*
* @see #subscribe(Map, Entity, Sensor, SensorEventListener)
*/
@SuppressWarnings("rawtypes")
public synchronized boolean unsubscribe(SubscriptionHandle sh) {
if (!(sh instanceof Subscription)) throw new IllegalArgumentException("Only subscription handles of type Subscription supported: sh="+sh+"; type="+(sh != null ? sh.getClass().getCanonicalName() : null));
Subscription s = (Subscription) sh;
boolean result = allSubscriptions.remove(s.id) != null;
boolean b2 = removeFromMapOfCollections(subscriptionsByToken, makeEntitySensorToken(s.producer, s.sensor), s);
assert result==b2;
if (s.subscriber!=null) {
boolean b3 = removeFromMapOfCollections(subscriptionsBySubscriber, s.subscriber, s);
assert b3 == b2;
}
// FIXME ALEX - this seems wrong
((BasicExecutionManager) em).setTaskSchedulerForTag(s.subscriberExecutionManagerTag, SingleThreadedScheduler.class);
return result;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public <T> void publish(final SensorEvent<T> event) {
// REVIEW 1459 - execution
// delivery in parallel/background, using execution manager
// subscriptions, should define SingleThreadedScheduler for any subscriber ID tag
// in order to ensure callbacks are invoked in the order they are submitted
// (recommend exactly one per subscription to prevent deadlock)
// this is done with:
// em.setTaskSchedulerForTag(subscriberId, SingleThreadedScheduler.class);
//note, generating the notifications must be done in the calling thread to preserve order
//e.g. emit(A); emit(B); should cause onEvent(A); onEvent(B) in that order
if (LOG.isTraceEnabled()) LOG.trace("{} got event {}", this, event);
totalEventsPublishedCount.incrementAndGet();
Set<Subscription> subs = (Set<Subscription>) ((Set<?>) getSubscriptionsForEntitySensor(event.getSource(), event.getSensor()));
if (groovyTruth(subs)) {
if (LOG.isTraceEnabled()) LOG.trace("sending {}, {} to {}", new Object[] {event.getSensor().getName(), event, join(subs, ",")});
for (Subscription s : subs) {
if (s.eventFilter!=null && !s.eventFilter.apply(event))
continue;
final Subscription sAtClosureCreation = s;
// Set<Object> tags = MutableSet.of();
// if (s.subscriberExecutionManagerTag!=null) tags.add(s.subscriberExecutionManagerTag);
// if (event.getSource()!=null) tags.add(BrooklynTaskTags.tagForContextEntity(event.getSource()));
// Map<String, Object> tagsMap = mapOf("tags", (Object)tags);
// use code above, instead of line below, if we want subscription deliveries associated with the entity;
// that will cause them to be cancelled when the entity is unmanaged
// (not sure that is useful, and likely NOT worth the expense, but it might be...) -Alex Oct 2014
Map<String, Object> tagsMap = mapOf("tag", s.subscriberExecutionManagerTag);
em.submit(tagsMap, new Runnable() {
@Override
public String toString() {
return "LSM.publish("+event+")";
}
public void run() {
try {
sAtClosureCreation.listener.onEvent(event);
} catch (Throwable t) {
if (event!=null && event.getSource()!=null && Entities.isNoLongerManaged(event.getSource())) {
LOG.debug("Error processing subscriptions to "+this+", after entity unmanaged: "+t, t);
} else {
LOG.warn("Error processing subscriptions to "+this+": "+t, t);
}
}
}});
totalEventsDeliveredCount.incrementAndGet();
}
}
}
@Override
public String toString() {
return tostring;
}
/**
* Copied from LanguageUtils.groovy, to remove dependency.
*
* Adds the given value to a collection in the map under the key.
*
* A collection (as {@link LinkedHashMap}) will be created if necessary,
* synchronized on map for map access/change and set for addition there
*
* @return the updated set (instance, not copy)
*
* @deprecated since 0.5; use {@link HashMultimap}, and {@link Multimaps#synchronizedSetMultimap(com.google.common.collect.SetMultimap)}
*/
@Deprecated
private static <K,V> Set<V> addToMapOfSets(Map<K,Set<V>> map, K key, V valueInCollection) {
Set<V> coll;
synchronized (map) {
coll = map.get(key);
if (coll==null) {
coll = new LinkedHashSet<V>();
map.put(key, coll);
}
if (coll.isEmpty()) {
synchronized (coll) {
coll.add(valueInCollection);
}
//if collection was empty then add to the collection while holding the map lock, to prevent removal
return coll;
}
}
synchronized (coll) {
if (!coll.isEmpty()) {
coll.add(valueInCollection);
return coll;
}
}
//if was empty, recurse, because someone else might be removing the collection
return addToMapOfSets(map, key, valueInCollection);
}
/**
* Copied from LanguageUtils.groovy, to remove dependency.
*
* Removes the given value from a collection in the map under the key.
*
* @return the updated set (instance, not copy)
*
* @deprecated since 0.5; use {@link ArrayListMultimap} or {@link HashMultimap}, and {@link Multimaps#synchronizedListMultimap(com.google.common.collect.ListMultimap)} etc
*/
@Deprecated
private static <K,V> boolean removeFromMapOfCollections(Map<K,? extends Collection<V>> map, K key, V valueInCollection) {
Collection<V> coll;
synchronized (map) {
coll = map.get(key);
if (coll==null) return false;
}
boolean result;
synchronized (coll) {
result = coll.remove(valueInCollection);
}
if (coll.isEmpty()) {
synchronized (map) {
synchronized (coll) {
if (coll.isEmpty()) {
//only remove from the map if no one is adding to the collection or to the map, and the collection is still in the map
if (map.get(key)==coll) {
map.remove(key);
}
}
}
}
}
return result;
}
}