/*
* 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.entity.group;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.brooklyn.api.entity.Entity;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.entity.Group;
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.config.ConfigKey;
import org.apache.brooklyn.core.BrooklynLogging;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.Attributes;
import org.apache.brooklyn.core.policy.AbstractPolicy;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.javalang.JavaClassNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.TypeToken;
/**
* Abstract class which helps track membership of a group, invoking (empty) methods in this class
* on MEMBER{ADDED,REMOVED} events, as well as SERVICE_UP {true,false} for those members.
*/
public abstract class AbstractMembershipTrackingPolicy extends AbstractPolicy {
private static final Logger LOG = LoggerFactory.getLogger(AbstractMembershipTrackingPolicy.class);
public enum EventType { ENTITY_CHANGE, ENTITY_ADDED, ENTITY_REMOVED }
@SuppressWarnings("serial")
public static final ConfigKey<Set<Sensor<?>>> SENSORS_TO_TRACK = ConfigKeys.newConfigKey(
new TypeToken<Set<Sensor<?>>>() {},
"sensorsToTrack",
"Sensors of members to be monitored (implicitly adds service-up to this list, but that behaviour may be deleted in a subsequent release!)",
ImmutableSet.<Sensor<?>>of());
public static final ConfigKey<Boolean> NOTIFY_ON_DUPLICATES = ConfigKeys.newBooleanConfigKey("notifyOnDuplicates",
"Whether to notify listeners when a sensor is published with the same value as last time",
false);
public static final ConfigKey<Group> GROUP = ConfigKeys.newConfigKey(Group.class, "group");
private ConcurrentMap<String,Map<Sensor<Object>, Object>> entitySensorCache = new ConcurrentHashMap<String, Map<Sensor<Object>, Object>>();
public AbstractMembershipTrackingPolicy(Map<?,?> flags) {
super(flags);
}
public AbstractMembershipTrackingPolicy() {
super();
}
protected Set<Sensor<?>> getSensorsToTrack() {
return ImmutableSet.<Sensor<?>>builder()
.addAll(getRequiredConfig(SENSORS_TO_TRACK))
.add(Attributes.SERVICE_UP)
.build();
}
@Override
public void setEntity(EntityLocal entity) {
super.setEntity(entity);
Group group = getGroup();
if (group != null) {
if (uniqueTag==null) {
uniqueTag = JavaClassNames.simpleClassName(this)+":"+group;
}
subscribeToGroup(group);
} else {
LOG.warn("Deprecated use of "+AbstractMembershipTrackingPolicy.class.getSimpleName()+"; group should be set as config");
}
}
/**
* Sets the group to be tracked; unsubscribes from any previous group, and subscribes to this group.
*
* Note this must be called *after* adding the policy to the entity.
*
* @param group
*
* @deprecated since 0.7; instead set the group as config
*/
@Deprecated
public void setGroup(Group group) {
// relies on doReconfigureConfig to make the actual change
LOG.warn("Deprecated use of setGroup in "+AbstractMembershipTrackingPolicy.class.getSimpleName()+"; group should be set as config");
config().set(GROUP, group);
}
@Override
protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
if (GROUP.getName().equals(key.getName())) {
Preconditions.checkNotNull(val, "%s value must not be null", GROUP.getName());
Preconditions.checkNotNull(val, "%s value must be a group, but was %s (of type %s)", GROUP.getName(), val, val.getClass());
if (val.equals(getConfig(GROUP))) {
if (LOG.isDebugEnabled()) LOG.debug("No-op for reconfigure group of "+AbstractMembershipTrackingPolicy.class.getSimpleName()+"; group is still "+val);
} else {
if (LOG.isInfoEnabled()) LOG.info("Membership tracker "+AbstractMembershipTrackingPolicy.class+", resubscribing to group "+val+", previously was "+getGroup());
unsubscribeFromGroup();
subscribeToGroup((Group)val);
}
} else {
throw new UnsupportedOperationException("reconfiguring "+key+" unsupported for "+this);
}
}
/**
* Unsubscribes from the group.
*
* @deprecated since 0.7; misleading method name; either remove the policy, or suspend/resume
*/
@Deprecated
public void reset() {
unsubscribeFromGroup();
}
@Override
public void suspend() {
unsubscribeFromGroup();
super.suspend();
}
@Override
public void resume() {
boolean wasSuspended = isSuspended();
super.resume();
Group group = getGroup();
if (wasSuspended && group != null) {
subscribeToGroup(group);
}
}
protected Group getGroup() {
return getConfig(GROUP);
}
protected void subscribeToGroup(final Group group) {
Preconditions.checkNotNull(group, "The group must not be null");
BrooklynLogging.log(LOG, BrooklynLogging.levelDebugOrTraceIfReadOnly(group),
"Subscribing to group "+group+", for memberAdded, memberRemoved, and {}", getSensorsToTrack());
subscriptions().subscribe(group, DynamicGroup.MEMBER_ADDED, new SensorEventListener<Entity>() {
@Override public void onEvent(SensorEvent<Entity> event) {
onEntityEvent(EventType.ENTITY_ADDED, event.getValue());
}
});
subscriptions().subscribe(group, DynamicGroup.MEMBER_REMOVED, new SensorEventListener<Entity>() {
@Override public void onEvent(SensorEvent<Entity> event) {
entitySensorCache.remove(event.getSource().getId());
onEntityEvent(EventType.ENTITY_REMOVED, event.getValue());
}
});
for (Sensor<?> sensor : getSensorsToTrack()) {
subscriptions().subscribeToMembers(group, sensor, new SensorEventListener<Object>() {
@Override public void onEvent(SensorEvent<Object> event) {
boolean notifyOnDuplicates = getRequiredConfig(NOTIFY_ON_DUPLICATES);
String entityId = event.getSource().getId();
if (!notifyOnDuplicates) {
Map<Sensor<Object>, Object> newMap = MutableMap.<Sensor<Object>, Object>of();
// NOTE: putIfAbsent returns null if the key is not present, or the *previous* value if present
Map<Sensor<Object>, Object> sensorCache = entitySensorCache.putIfAbsent(entityId, newMap);
if (sensorCache == null) {
sensorCache = newMap;
}
boolean oldExists = sensorCache.containsKey(event.getSensor());
Object oldVal = sensorCache.put(event.getSensor(), event.getValue());
if (oldExists && Objects.equal(event.getValue(), oldVal)) {
// ignore if value has not changed
return;
}
}
onEntityEvent(EventType.ENTITY_CHANGE, event.getSource());
}
});
}
for (Entity it : group.getMembers()) { onEntityEvent(EventType.ENTITY_ADDED, it); }
}
protected void unsubscribeFromGroup() {
Group group = getGroup();
if (getSubscriptionTracker() != null && group != null) subscriptions().unsubscribe(group);
}
/** All entity events pass through this method. Default impl delegates to onEntityXxxx methods, whose default behaviours are no-op.
* Callers may override this to intercept all entity events in a single place, and to suppress subsequent processing if desired.
*/
protected void onEntityEvent(EventType type, Entity entity) {
switch (type) {
case ENTITY_CHANGE: onEntityChange(entity); break;
case ENTITY_ADDED: onEntityAdded(entity); break;
case ENTITY_REMOVED: onEntityRemoved(entity); break;
}
}
/**
* Called when a member's "up" sensor changes.
*/
protected void onEntityChange(Entity member) {}
/**
* Called when a member is added.
* Note that the change event may arrive before this event; implementations here should typically look at the last value.
*/
protected void onEntityAdded(Entity member) {}
/**
* Called when a member is removed.
* Note that entity change events may arrive after this event; they should typically be ignored.
* The entity could already be unmanaged at this point so limited functionality is available (i.e. can't access config keys).
*/
protected void onEntityRemoved(Entity member) {}
}