You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2014/11/26 02:07:30 UTC

[3/6] incubator-brooklyn git commit: feed de-duplication

feed de-duplication

prevents problems, especially on rebind, where if the same feed is added multiple times, we get multiple running instances.
works primarily by using unique tags which are now inferred for feeds.
these can be explicitly set, if the default de-dupe behaviour is not right,
but in most cases it will be right, using a combo of the source and the sensor it writes to.
(since most sensors will only be written to by one source this is more than enough!)
also adds lots of equals and hashcodes, as this is used secondarily when there is no unique tag
(but now there always is so much of this is not needed)


Project: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/commit/2af34226
Tree: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/tree/2af34226
Diff: http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/diff/2af34226

Branch: refs/heads/master
Commit: 2af342266f7263c8415df212abfbb36cee99f8a8
Parents: 92f775f
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Nov 19 14:40:12 2014 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Thu Nov 20 12:50:39 2014 +0000

----------------------------------------------------------------------
 .../brooklyn/entity/basic/AbstractEntity.java   | 18 +++--
 .../java/brooklyn/event/feed/AbstractFeed.java  | 36 +++++++++
 .../java/brooklyn/event/feed/FeedConfig.java    | 82 ++++++++++++++++++++
 .../java/brooklyn/event/feed/PollConfig.java    | 16 ++--
 .../event/feed/function/FunctionFeed.java       | 13 +++-
 .../event/feed/function/FunctionPollConfig.java | 13 ++++
 .../java/brooklyn/event/feed/http/HttpFeed.java |  6 ++
 .../event/feed/http/HttpPollConfig.java         | 10 ++-
 .../brooklyn/event/feed/shell/ShellFeed.java    |  6 ++
 .../event/feed/shell/ShellPollConfig.java       |  6 ++
 .../java/brooklyn/event/feed/ssh/SshFeed.java   |  6 ++
 .../brooklyn/event/feed/ssh/SshPollConfig.java  | 12 +++
 .../windows/WindowsPerformanceCounterFeed.java  |  6 ++
 .../WindowsPerformanceCounterPollConfig.java    |  6 +-
 .../java/brooklyn/policy/basic/AdjunctType.java | 10 +--
 .../event/feed/function/FunctionFeedTest.java   | 24 +++++-
 .../brooklyn/event/feed/http/HttpFeedTest.java  | 15 ++++
 .../feed/shell/ShellFeedIntegrationTest.java    | 21 +++++
 .../event/feed/ssh/SshFeedIntegrationTest.java  | 16 ++++
 .../entity/monitoring/zabbix/ZabbixFeed.java    |  7 ++
 .../monitoring/zabbix/ZabbixPollConfig.java     |  6 ++
 .../brooklyn/entity/chef/ChefAttributeFeed.java |  9 ++-
 .../entity/chef/ChefAttributePollConfig.java    |  7 +-
 .../event/feed/jmx/JmxAttributePollConfig.java  |  7 +-
 .../java/brooklyn/event/feed/jmx/JmxFeed.java   |  6 ++
 .../jmx/JmxNotificationSubscriptionConfig.java  | 14 ++++
 .../event/feed/jmx/JmxOperationPollConfig.java  |  4 +
 .../main/java/brooklyn/util/guava/Maybe.java    | 16 ++++
 .../java/brooklyn/util/javalang/Equals.java     | 35 +++++++++
 .../main/java/brooklyn/util/text/Strings.java   | 22 ++++++
 30 files changed, 421 insertions(+), 34 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java b/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
index 4590990..5dfec58 100644
--- a/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
+++ b/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
@@ -1247,9 +1247,14 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
         }
     }
     private <T extends EntityAdjunct> T findApparentlyEqual(Collection<? extends T> itemsCopy, T newItem) {
-        // TODO workaround for issue where enrichers can get added multiple times on rebind,
-        // if it's added in onBecomingManager or connectSensors; the right fix will be more disciplined about how/where these are added
-        // (easier done when sensor feeds are persisted)
+        // TODO workaround for issue where enrichers/feeds/policies can get added multiple times on rebind,
+        // if it's added in onBecomingManager or connectSensors; 
+        // the right fix will be more disciplined about how/where these are added;
+        // furthermore unique tags should be preferred;
+        // when they aren't supplied, a reflection equals is done ignoring selected fields,
+        // which is okay but not great ... and if it misses something (e.g. because an 'equals' isn't implemented)
+        // then you can get a new instance on every rebind
+        // (and currently these aren't readily visible, except looking at the counts or in persisted state) 
         Class<?> beforeEntityAdjunct = newItem.getClass();
         while (beforeEntityAdjunct.getSuperclass()!=null && !beforeEntityAdjunct.getSuperclass().equals(AbstractEntityAdjunct.class))
             beforeEntityAdjunct = beforeEntityAdjunct.getSuperclass();
@@ -1267,8 +1272,11 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
                         // from aggregator
                         "transformation",
                         // from averager
-                        "values", "timestamps", "lastAverage")) {
-                    
+                        "values", "timestamps", "lastAverage",
+                        // from some feeds
+                        "poller",
+                        "pollerStateMutex"
+                        )) {
                     
                     return oldItem;
                 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/AbstractFeed.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/AbstractFeed.java b/core/src/main/java/brooklyn/event/feed/AbstractFeed.java
index 5bb6da5..5303ac7 100644
--- a/core/src/main/java/brooklyn/event/feed/AbstractFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/AbstractFeed.java
@@ -20,6 +20,8 @@ package brooklyn.event.feed;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import java.util.Collection;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -33,6 +35,8 @@ import brooklyn.entity.rebind.RebindSupport;
 import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.mementos.FeedMemento;
 import brooklyn.policy.basic.AbstractEntityAdjunct;
+import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.text.Strings;
 
 /** 
  * Captures common fields and processes for sensor feeds.
@@ -79,7 +83,39 @@ public abstract class AbstractFeed extends AbstractEntityAdjunct implements Feed
             ((EntityInternal)entity).feeds().addFeed(this);
         }
     }
+
+    protected void initUniqueTag(String uniqueTag, Object ...valsForDefault) {
+        if (Strings.isNonBlank(uniqueTag)) this.uniqueTag = uniqueTag;
+        else if (Strings.isBlank(this.uniqueTag)) this.uniqueTag = getDefaultUniqueTag(valsForDefault);
+    }
+
+    @Override
+    public String getUniqueTag() {
+        if (Strings.isBlank(uniqueTag)) initUniqueTag(null);
+        return super.getUniqueTag();
+    }
     
+    protected String getDefaultUniqueTag(Object ...valsForDefault) {
+        StringBuilder sb = new StringBuilder();
+        sb.append(JavaClassNames.simpleClassName(this));
+        if (valsForDefault.length==0) {
+            sb.append("@");
+            sb.append(hashCode());
+        } else if (valsForDefault.length==1 && valsForDefault[0] instanceof Collection){
+            sb.append(Strings.toUniqueString(valsForDefault[0], 80));
+        } else {
+            sb.append("[");
+            boolean first = true;
+            for (Object x: valsForDefault) {
+                if (!first) sb.append(";");
+                else first = false;
+                sb.append(Strings.toUniqueString(x, 80));
+            }
+            sb.append("]");
+        }
+        return sb.toString(); 
+    }
+
     @Override
     public boolean isActivated() {
         return activated;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/FeedConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/FeedConfig.java b/core/src/main/java/brooklyn/event/feed/FeedConfig.java
index 1c2e6a0..54eeea1 100644
--- a/core/src/main/java/brooklyn/event/feed/FeedConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/FeedConfig.java
@@ -23,9 +23,13 @@ import brooklyn.entity.basic.Entities;
 import brooklyn.event.AttributeSensor;
 import brooklyn.event.basic.Sensors;
 import brooklyn.event.feed.http.HttpPollConfig;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.text.Strings;
 
 import com.google.common.base.Function;
 import com.google.common.base.Functions;
+import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 
 /**
@@ -180,4 +184,82 @@ public class FeedConfig<V, T, F extends FeedConfig<V, T, F>> {
     public boolean hasCheckSuccessHandler() {
         return this.checkSuccess != null;
     }
+
+    
+    @Override
+    public String toString() {
+        StringBuilder result = new StringBuilder();
+        result.append(toStringBaseName());
+        result.append("[");
+        boolean contents = false;
+        Object source = toStringPollSource();
+        AttributeSensor<T> s = getSensor();
+        if (Strings.isNonBlank(Strings.toString(source))) {
+            result.append(Strings.toUniqueString(source, 40));
+            if (s!=null) {
+                result.append("->");
+                result.append(s.getName());
+            }
+            contents = true;
+        } else if (s!=null) {
+            result.append(s.getName());
+            contents = true;
+        }
+        MutableList<Object> fields = toStringOtherFields();
+        if (fields!=null) {
+            for (Object field: fields) {
+                if (Strings.isNonBlank(Strings.toString(field))) {
+                    if (contents) result.append(";");
+                    contents = true;
+                    result.append(field);
+                }
+            }
+        }
+        result.append("]");
+        return result.toString();
+    }
+
+    /** can be overridden to supply a simpler base name than the class name */
+    protected String toStringBaseName() {
+        return JavaClassNames.simpleClassName(this);
+    }
+    /** can be overridden to supply add'l info for the {@link #toString()}; subclasses can add to the returned value */
+    protected MutableList<Object> toStringOtherFields() {
+        return MutableList.<Object>of();
+    }
+    /** can be overridden to supply add'l info for the {@link #toString()}, placed before the sensor with -> */
+    protected Object toStringPollSource() {
+        return null;
+    }
+    /** all configs should supply a unique tag element, inserted into the feed */
+    protected String getUniqueTag() {
+        return toString();
+    }
+
+    /** returns fields which should be used for equality, including by default {@link #toStringOtherFields()} and {@link #toStringPollSource()};
+     * subclasses can add to the returned value */
+    protected MutableList<Object> equalsFields() {
+        MutableList<Object> result = MutableList.of().appendIfNotNull(getSensor()).appendIfNotNull(toStringPollSource());
+        for (Object field: toStringOtherFields()) result.appendIfNotNull(field);
+        return result;
+    }
+
+    @Override
+    public int hashCode() { 
+        int hc = super.hashCode();
+        for (Object f: equalsFields())
+            hc = Objects.hashCode(hc, f);
+        return hc;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!super.equals(obj)) return false;
+        PollConfig<?,?,?> other = (PollConfig<?,?,?>) obj;
+        if (!Objects.equal(getUniqueTag(), other.getUniqueTag())) return false;
+        if (!Objects.equal(equalsFields(), other.equalsFields())) return false;
+        return true;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/PollConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/PollConfig.java b/core/src/main/java/brooklyn/event/feed/PollConfig.java
index 73543a4..882d641 100644
--- a/core/src/main/java/brooklyn/event/feed/PollConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/PollConfig.java
@@ -23,7 +23,7 @@ import static com.google.common.base.Preconditions.checkArgument;
 import java.util.concurrent.TimeUnit;
 
 import brooklyn.event.AttributeSensor;
-import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.time.Duration;
 
 /**
@@ -70,10 +70,16 @@ public class PollConfig<V, T, F extends PollConfig<V, T, F>> extends FeedConfig<
         return self();
     }
     
-    @Override
-    public String toString() {
-        if (description!=null) return description;
-        return JavaClassNames.simpleClassName(this);
+    public String getDescription() {
+        return description;
+    }
+    
+    @Override protected MutableList<Object> toStringOtherFields() {
+        return super.toStringOtherFields().appendIfNotNull(description);
     }
 
+    @Override
+    protected MutableList<Object> equalsFields() {
+        return super.equalsFields().appendIfNotNull(period);
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/function/FunctionFeed.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/function/FunctionFeed.java b/core/src/main/java/brooklyn/event/feed/function/FunctionFeed.java
index 410fcf4..6acf408 100644
--- a/core/src/main/java/brooklyn/event/feed/function/FunctionFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/function/FunctionFeed.java
@@ -83,6 +83,7 @@ public class FunctionFeed extends AbstractFeed {
     private static final Logger log = LoggerFactory.getLogger(FunctionFeed.class);
 
     // Treat as immutable once built
+    @SuppressWarnings("serial")
     public static final ConfigKey<SetMultimap<FunctionPollIdentifier, FunctionPollConfig<?,?>>> POLLS = ConfigKeys.newConfigKey(
             new TypeToken<SetMultimap<FunctionPollIdentifier, FunctionPollConfig<?,?>>>() {},
             "polls");
@@ -91,12 +92,17 @@ public class FunctionFeed extends AbstractFeed {
         return new Builder();
     }
     
+    public static Builder builder(String uniqueTag) {
+        return new Builder().uniqueTag(uniqueTag);
+    }
+    
     public static class Builder {
         private EntityLocal entity;
         private boolean onlyIfServiceUp = false;
         private long period = 500;
         private TimeUnit periodUnits = TimeUnit.MILLISECONDS;
         private List<FunctionPollConfig<?,?>> polls = Lists.newArrayList();
+        private String uniqueTag;
         private volatile boolean built;
 
         public Builder entity(EntityLocal val) {
@@ -123,6 +129,10 @@ public class FunctionFeed extends AbstractFeed {
             polls.add(config);
             return this;
         }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
         public FunctionFeed build() {
             built = true;
             FunctionFeed result = new FunctionFeed(this);
@@ -153,7 +163,7 @@ public class FunctionFeed extends AbstractFeed {
             return (other instanceof FunctionPollIdentifier) && Objects.equal(job, ((FunctionPollIdentifier)other).job);
         }
     }
-    
+
     /**
      * For rebind; do not call directly; use builder
      */
@@ -172,6 +182,7 @@ public class FunctionFeed extends AbstractFeed {
             polls.put(new FunctionPollIdentifier(job), configCopy);
         }
         setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls.values());
     }
 
     @SuppressWarnings({ "unchecked", "rawtypes" })

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/function/FunctionPollConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/function/FunctionPollConfig.java b/core/src/main/java/brooklyn/event/feed/function/FunctionPollConfig.java
index b2ff3f1..0f12672 100644
--- a/core/src/main/java/brooklyn/event/feed/function/FunctionPollConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/function/FunctionPollConfig.java
@@ -27,6 +27,7 @@ import brooklyn.event.AttributeSensor;
 import brooklyn.event.feed.FeedConfig;
 import brooklyn.event.feed.PollConfig;
 import brooklyn.util.GroovyJavaMethods;
+import brooklyn.util.javalang.JavaClassNames;
 
 import com.google.common.base.Supplier;
 
@@ -87,4 +88,16 @@ public class FunctionPollConfig<S, T> extends PollConfig<S, T, FunctionPollConfi
         this.callable = GroovyJavaMethods.callableFromClosure(checkNotNull(val, "closure"));
         return this;
     }
+
+    @Override protected String toStringBaseName() { return "fn"; }
+    @Override protected String toStringPollSource() {
+        if (callable==null) return null;
+        String cs = callable.toString();
+        if (!cs.contains( ""+Integer.toHexString(callable.hashCode()) )) {
+            return cs;
+        }
+        // if hashcode is in callable it's probably a custom internal; return class name
+        return JavaClassNames.simpleClassName(callable);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java b/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java
index 9547599..255443a 100644
--- a/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/http/HttpFeed.java
@@ -127,6 +127,7 @@ public class HttpFeed extends AbstractFeed {
         private Map<String, String> headers = Maps.newLinkedHashMap();
         private boolean suspended = false;
         private Credentials credentials;
+        private String uniqueTag;
         private volatile boolean built;
 
         public Builder entity(EntityLocal val) {
@@ -203,6 +204,10 @@ public class HttpFeed extends AbstractFeed {
             }
             return this;
         }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
         public HttpFeed build() {
             built = true;
             HttpFeed result = new HttpFeed(this);
@@ -300,6 +305,7 @@ public class HttpFeed extends AbstractFeed {
             polls.put(new HttpPollIdentifier(method, baseUriProvider, headers, body, credentials, connectionTimeout, socketTimeout), configCopy);
         }
         setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls.values());
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/http/HttpPollConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/http/HttpPollConfig.java b/core/src/main/java/brooklyn/event/feed/http/HttpPollConfig.java
index c091006..979e629 100644
--- a/core/src/main/java/brooklyn/event/feed/http/HttpPollConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/http/HttpPollConfig.java
@@ -25,6 +25,7 @@ import javax.annotation.Nullable;
 
 import brooklyn.event.AttributeSensor;
 import brooklyn.event.feed.PollConfig;
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.http.HttpToolResponse;
 import brooklyn.util.net.URLParamEncoder;
@@ -159,9 +160,12 @@ public class HttpPollConfig<T> extends PollConfig<HttpToolResponse, T, HttpPollC
         return MutableMap.<K,V>builder().putAll(map1).putAll(map2).build();
     }
 
+    @Override protected String toStringBaseName() { return "http"; }
+    @Override protected String toStringPollSource() { return suburl; }
     @Override
-    public String toString() {
-        return "http["+suburl+"]";
+    protected MutableList<Object> equalsFields() {
+        return super.equalsFields().appendIfNotNull(method).appendIfNotNull(vars).appendIfNotNull(headers)
+            .appendIfNotNull(body).appendIfNotNull(connectionTimeout).appendIfNotNull(socketTimeout);
     }
-    
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/shell/ShellFeed.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/shell/ShellFeed.java b/core/src/main/java/brooklyn/event/feed/shell/ShellFeed.java
index df088b8..f92ec07 100644
--- a/core/src/main/java/brooklyn/event/feed/shell/ShellFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/shell/ShellFeed.java
@@ -113,6 +113,7 @@ public class ShellFeed extends AbstractFeed {
         private long period = 500;
         private TimeUnit periodUnits = TimeUnit.MILLISECONDS;
         private List<ShellPollConfig<?>> polls = Lists.newArrayList();
+        private String uniqueTag;
         private volatile boolean built;
         
         public Builder entity(EntityLocal val) {
@@ -131,6 +132,10 @@ public class ShellFeed extends AbstractFeed {
             polls.add(config);
             return this;
         }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
         public ShellFeed build() {
             built = true;
             ShellFeed result = new ShellFeed(this);
@@ -204,6 +209,7 @@ public class ShellFeed extends AbstractFeed {
             polls.put(new ShellPollIdentifier(command, env, dir, input, context, timeout), configCopy);
         }
         setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls.values());
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/shell/ShellPollConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/shell/ShellPollConfig.java b/core/src/main/java/brooklyn/event/feed/shell/ShellPollConfig.java
index 953d04a..5c24741 100644
--- a/core/src/main/java/brooklyn/event/feed/shell/ShellPollConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/shell/ShellPollConfig.java
@@ -29,6 +29,7 @@ import javax.annotation.Nullable;
 import brooklyn.event.AttributeSensor;
 import brooklyn.event.feed.PollConfig;
 import brooklyn.event.feed.ssh.SshPollValue;
+import brooklyn.util.collections.MutableList;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Maps;
@@ -116,4 +117,9 @@ public class ShellPollConfig<T> extends PollConfig<SshPollValue, T, ShellPollCon
         this.timeout = units.toMillis(timeout);
         return this;
     }
+
+    @Override protected String toStringBaseName() { return "shell"; }
+    @Override protected String toStringPollSource() { return command; }
+    @Override protected MutableList<Object> equalsFields() { return super.equalsFields().appendIfNotNull(command); }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/ssh/SshFeed.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/ssh/SshFeed.java b/core/src/main/java/brooklyn/event/feed/ssh/SshFeed.java
index 5b41d19..6931089 100644
--- a/core/src/main/java/brooklyn/event/feed/ssh/SshFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/ssh/SshFeed.java
@@ -117,6 +117,7 @@ public class SshFeed extends AbstractFeed {
         private Duration period = Duration.of(500, TimeUnit.MILLISECONDS);
         private List<SshPollConfig<?>> polls = Lists.newArrayList();
         private boolean execAsCommand = false;
+        private String uniqueTag;
         private volatile boolean built;
         
         public Builder entity(EntityLocal val) {
@@ -157,6 +158,10 @@ public class SshFeed extends AbstractFeed {
             execAsCommand = false;
             return this;
         }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
         public SshFeed build() {
             built = true;
             SshFeed result = new SshFeed(this);
@@ -220,6 +225,7 @@ public class SshFeed extends AbstractFeed {
             polls.put(new SshPollIdentifier(config.getCommandSupplier(), config.getEnvSupplier()), configCopy);
         }
         setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls.values());
     }
 
     protected SshMachineLocation getMachine() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/ssh/SshPollConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/ssh/SshPollConfig.java b/core/src/main/java/brooklyn/event/feed/ssh/SshPollConfig.java
index a52a960..78c7a78 100644
--- a/core/src/main/java/brooklyn/event/feed/ssh/SshPollConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/ssh/SshPollConfig.java
@@ -127,4 +127,16 @@ public class SshPollConfig<T> extends PollConfig<SshPollValue, T, SshPollConfig<
         return this;
     }
 
+    @Override protected String toStringBaseName() { return "ssh"; }
+    @Override protected Object toStringPollSource() {
+        if (getCommandSupplier()==null) return null;
+        String command = getCommandSupplier().get();
+        return command;
+    }
+    @Override protected MutableList<Object> equalsFields() { 
+        return super.equalsFields()
+            .appendIfNotNull(getCommandSupplier()!=null ? getCommandSupplier().get() : null)
+            .appendIfNotNull(getEnvSupplier()!=null ? getEnvSupplier().get() : null); 
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterFeed.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterFeed.java b/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterFeed.java
index c3bfeb3..468af57 100644
--- a/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterFeed.java
@@ -112,6 +112,7 @@ public class WindowsPerformanceCounterFeed extends AbstractFeed {
         private EntityLocal entity;
         private Set<WindowsPerformanceCounterPollConfig<?>> polls = Sets.newLinkedHashSet();
         private Duration period = Duration.of(30, TimeUnit.SECONDS);
+        private String uniqueTag;
         private volatile boolean built;
 
         public Builder entity(EntityLocal val) {
@@ -141,6 +142,10 @@ public class WindowsPerformanceCounterFeed extends AbstractFeed {
         public Builder period(long val, TimeUnit units) {
             return period(Duration.of(val, units));
         }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
         public WindowsPerformanceCounterFeed build() {
             built = true;
             WindowsPerformanceCounterFeed result = new WindowsPerformanceCounterFeed(this);
@@ -169,6 +174,7 @@ public class WindowsPerformanceCounterFeed extends AbstractFeed {
             polls.add(configCopy);
         }
         setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterPollConfig.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterPollConfig.java b/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterPollConfig.java
index fe4f897..a5d2abd 100644
--- a/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterPollConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/windows/WindowsPerformanceCounterPollConfig.java
@@ -46,9 +46,7 @@ public class WindowsPerformanceCounterPollConfig<T> extends PollConfig<Object, T
     public WindowsPerformanceCounterPollConfig<T> performanceCounterName(String val) {
         this.performanceCounterName = val; return this;
     }
+
+    @Override protected String toStringPollSource() { return performanceCounterName; }
     
-    @Override
-    public String toString() {
-        return "windowsPerformanceCounter["+performanceCounterName+"]";
-    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/main/java/brooklyn/policy/basic/AdjunctType.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/policy/basic/AdjunctType.java b/core/src/main/java/brooklyn/policy/basic/AdjunctType.java
index 7d019f7..3ac9c69 100644
--- a/core/src/main/java/brooklyn/policy/basic/AdjunctType.java
+++ b/core/src/main/java/brooklyn/policy/basic/AdjunctType.java
@@ -31,7 +31,6 @@ import org.slf4j.LoggerFactory;
 import brooklyn.config.ConfigKey;
 import brooklyn.config.ConfigKey.HasConfigKey;
 import brooklyn.policy.EntityAdjunct;
-import brooklyn.policy.PolicyType;
 
 import com.google.common.base.Joiner;
 import com.google.common.base.Objects;
@@ -94,10 +93,11 @@ public class AdjunctType implements Serializable {
     @Override
     public boolean equals(Object obj) {
         if (this == obj) return true;
-        if (!(obj instanceof PolicyType)) return false;
-        PolicyType o = (PolicyType) obj;
-        
-        return Objects.equal(name, o.getName()) && Objects.equal(getConfigKeys(), o.getConfigKeys());
+        if (getClass() != obj.getClass()) return false;
+        AdjunctType o = (AdjunctType) obj;
+        if (!Objects.equal(name, o.getName())) return false;
+        if (!Objects.equal(getConfigKeys(), o.getConfigKeys())) return false;
+        return true;
     }
     
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/test/java/brooklyn/event/feed/function/FunctionFeedTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/event/feed/function/FunctionFeedTest.java b/core/src/test/java/brooklyn/event/feed/function/FunctionFeedTest.java
index 90a0435..f2e71a4 100644
--- a/core/src/test/java/brooklyn/event/feed/function/FunctionFeedTest.java
+++ b/core/src/test/java/brooklyn/event/feed/function/FunctionFeedTest.java
@@ -28,11 +28,16 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.annotation.Nullable;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.EntityInternal.FeedSupport;
 import brooklyn.entity.basic.EntityLocal;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.event.AttributeSensor;
@@ -54,6 +59,8 @@ import com.google.common.util.concurrent.Callables;
 
 public class FunctionFeedTest extends BrooklynAppUnitTestSupport {
 
+    private static final Logger log = LoggerFactory.getLogger(FunctionFeedTest.class);
+    
     final static AttributeSensor<String> SENSOR_STRING = Sensors.newStringSensor("aString", "");
     final static AttributeSensor<Integer> SENSOR_INT = Sensors.newIntegerSensor("aLong", "");
 
@@ -188,7 +195,8 @@ public class FunctionFeedTest extends BrooklynAppUnitTestSupport {
         Asserts.succeedsEventually(new Runnable() {
             public void run() {
                 assertEquals(ints.subList(0, 2), ImmutableList.of(0, 1));
-                assertEquals(strings.subList(0, 2), ImmutableList.of("0", "1"));
+                assertTrue(strings.size()>=2, "wrong strings list: "+strings);
+                assertEquals(strings.subList(0, 2), ImmutableList.of("0", "1"), "wrong strings list: "+strings);
             }});
     }
     
@@ -216,6 +224,20 @@ public class FunctionFeedTest extends BrooklynAppUnitTestSupport {
                 .onFailureOrException(Functions.<Integer>constant(null));
     }
     
+    @Test
+    public void testFeedDeDupe() throws Exception {
+        testPollsFunctionRepeatedlyToSetAttribute();
+        entity.addFeed(feed);
+        log.info("Feed 0 is: "+feed);
+        
+        testPollsFunctionRepeatedlyToSetAttribute();
+        log.info("Feed 1 is: "+feed);
+        entity.addFeed(feed);
+                
+        FeedSupport feeds = ((EntityInternal)entity).feeds();
+        Assert.assertEquals(feeds.getFeeds().size(), 1, "Wrong feed count: "+feeds.getFeeds());
+    }
+    
     private static class IncrementingCallable implements Callable<Integer> {
         private final AtomicInteger next = new AtomicInteger(0);
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java b/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
index f112890..817fdb1 100644
--- a/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
+++ b/core/src/test/java/brooklyn/event/feed/http/HttpFeedTest.java
@@ -37,6 +37,7 @@ import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityFunctions;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
+import brooklyn.entity.basic.EntityInternal.FeedSupport;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.event.AttributeSensor;
 import brooklyn.event.basic.Sensors;
@@ -120,6 +121,20 @@ public class HttpFeedTest extends BrooklynAppUnitTestSupport {
     }
     
     @Test
+    public void testFeedDeDupe() throws Exception {
+        testPollsAndParsesHttpGetResponse();
+        entity.addFeed(feed);
+        log.info("Feed 0 is: "+feed);
+        
+        testPollsAndParsesHttpGetResponse();
+        log.info("Feed 1 is: "+feed);
+        entity.addFeed(feed);
+                
+        FeedSupport feeds = ((EntityInternal)entity).feeds();
+        Assert.assertEquals(feeds.getFeeds().size(), 1, "Wrong feed count: "+feeds.getFeeds());
+    }
+    
+    @Test
     public void testSetsConnectionTimeout() throws Exception {
         feed = HttpFeed.builder()
                 .entity(entity)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/test/java/brooklyn/event/feed/shell/ShellFeedIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/event/feed/shell/ShellFeedIntegrationTest.java b/core/src/test/java/brooklyn/event/feed/shell/ShellFeedIntegrationTest.java
index 08c4849..4d2ed42 100644
--- a/core/src/test/java/brooklyn/event/feed/shell/ShellFeedIntegrationTest.java
+++ b/core/src/test/java/brooklyn/event/feed/shell/ShellFeedIntegrationTest.java
@@ -23,11 +23,16 @@ import static org.testng.Assert.assertTrue;
 import java.util.Arrays;
 import java.util.concurrent.TimeUnit;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
 import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.basic.EntityInternal;
+import brooklyn.entity.basic.EntityInternal.FeedSupport;
 import brooklyn.entity.basic.EntityLocal;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.event.AttributeSensor;
@@ -47,6 +52,8 @@ import com.google.common.collect.ImmutableMap;
 
 public class ShellFeedIntegrationTest extends BrooklynAppUnitTestSupport {
 
+    private static final Logger log = LoggerFactory.getLogger(ShellFeedIntegrationTest.class);
+    
     final static AttributeSensor<String> SENSOR_STRING = Sensors.newStringSensor("aString", "");
     final static AttributeSensor<Integer> SENSOR_INT = Sensors.newIntegerSensor("anInt", "");
     final static AttributeSensor<Long> SENSOR_LONG = Sensors.newLongSensor("aLong", "");
@@ -84,6 +91,20 @@ public class ShellFeedIntegrationTest extends BrooklynAppUnitTestSupport {
         EntityTestUtils.assertAttributeEqualsEventually(entity, SENSOR_INT, 123);
     }
     
+    @Test(groups="Integration")
+    public void testFeedDeDupe() throws Exception {
+        testReturnsShellExitStatus();
+        entity.addFeed(feed);
+        log.info("Feed 0 is: "+feed);
+        
+        testReturnsShellExitStatus();
+        log.info("Feed 1 is: "+feed);
+        entity.addFeed(feed);
+                
+        FeedSupport feeds = ((EntityInternal)entity).feeds();
+        Assert.assertEquals(feeds.getFeeds().size(), 1, "Wrong feed count: "+feeds.getFeeds());
+    }
+    
     // TODO timeout no longer supported; would be nice to have a generic task-timeout feature,
     // now that the underlying impl uses SystemProcessTaskFactory
     @Test(enabled=false, groups={"Integration", "WIP"})

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/core/src/test/java/brooklyn/event/feed/ssh/SshFeedIntegrationTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/event/feed/ssh/SshFeedIntegrationTest.java b/core/src/test/java/brooklyn/event/feed/ssh/SshFeedIntegrationTest.java
index b5370ce..16bab62 100644
--- a/core/src/test/java/brooklyn/event/feed/ssh/SshFeedIntegrationTest.java
+++ b/core/src/test/java/brooklyn/event/feed/ssh/SshFeedIntegrationTest.java
@@ -31,7 +31,9 @@ import org.testng.annotations.Test;
 import brooklyn.entity.BrooklynAppUnitTestSupport;
 import brooklyn.entity.basic.Attributes;
 import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
+import brooklyn.entity.basic.EntityInternal.FeedSupport;
 import brooklyn.entity.proxying.EntityInitializer;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.event.AttributeSensor;
@@ -105,6 +107,20 @@ public class SshFeedIntegrationTest extends BrooklynAppUnitTestSupport {
     }
 
     @Test(groups="Integration")
+    public void testFeedDeDupe() throws Exception {
+        testReturnsSshStdoutAndInfersMachine();
+        entity.addFeed(feed);
+        log.info("Feed 0 is: "+feed);
+        
+        testReturnsSshStdoutAndInfersMachine();
+        log.info("Feed 1 is: "+feed);
+        entity.addFeed(feed);
+                
+        FeedSupport feeds = ((EntityInternal)entity).feeds();
+        Assert.assertEquals(feeds.getFeeds().size(), 1, "Wrong feed count: "+feeds.getFeeds());
+    }
+    
+    @Test(groups="Integration")
     public void testReturnsSshExitStatus() throws Exception {
         feed = SshFeed.builder()
                 .entity(entity)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixFeed.java
----------------------------------------------------------------------
diff --git a/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixFeed.java b/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixFeed.java
index e1b022d..5fe96cd 100644
--- a/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixFeed.java
+++ b/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixFeed.java
@@ -137,6 +137,7 @@ public class ZabbixFeed extends AbstractFeed {
         private Function<? super EntityLocal, String> uniqueHostnameGenerator = Functions.compose(
                 EntityFunctions.id(), 
                 EntityFunctions.locationMatching(Predicates.instanceOf(MachineLocation.class)));
+        private String uniqueTag;
 
         @SuppressWarnings("unchecked")
         protected B self() {
@@ -228,6 +229,11 @@ public class ZabbixFeed extends AbstractFeed {
             return self();
         }
         
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
+        
         @SuppressWarnings("unchecked")
         public T build() {
             // If server not set and other config not available, try to obtain from entity config
@@ -305,6 +311,7 @@ public class ZabbixFeed extends AbstractFeed {
             polls.add(configCopy);
         }
         setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixPollConfig.java
----------------------------------------------------------------------
diff --git a/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixPollConfig.java b/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixPollConfig.java
index dc31698..743df8b 100644
--- a/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixPollConfig.java
+++ b/sandbox/monitoring/src/main/java/brooklyn/entity/monitoring/zabbix/ZabbixPollConfig.java
@@ -24,6 +24,7 @@ import brooklyn.event.AttributeSensor;
 import brooklyn.event.feed.PollConfig;
 import brooklyn.event.feed.http.HttpValueFunctions;
 import brooklyn.event.feed.http.JsonFunctions;
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.http.HttpToolResponse;
 
 import com.google.common.base.Function;
@@ -66,4 +67,9 @@ public class ZabbixPollConfig<T> extends PollConfig<HttpToolResponse, T, ZabbixP
         return this;
     }
 
+    @Override
+    protected MutableList<Object> equalsFields() {
+        return super.equalsFields().appendIfNotNull(itemKey);
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/software/base/src/main/java/brooklyn/entity/chef/ChefAttributeFeed.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/entity/chef/ChefAttributeFeed.java b/software/base/src/main/java/brooklyn/entity/chef/ChefAttributeFeed.java
index d79262a..d55845e 100644
--- a/software/base/src/main/java/brooklyn/entity/chef/ChefAttributeFeed.java
+++ b/software/base/src/main/java/brooklyn/entity/chef/ChefAttributeFeed.java
@@ -25,7 +25,6 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.SortedSet;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 
@@ -118,6 +117,7 @@ public class ChefAttributeFeed extends AbstractFeed {
         private String nodeName;
         private Set<ChefAttributePollConfig> polls = Sets.newLinkedHashSet();
         private Duration period = Duration.of(30, TimeUnit.SECONDS);
+        private String uniqueTag;
         private volatile boolean built;
 
         public Builder entity(EntityLocal val) {
@@ -137,6 +137,7 @@ public class ChefAttributeFeed extends AbstractFeed {
             polls.add(config);
             return this;
         }
+        @SuppressWarnings("unchecked")
         public Builder addSensor(String chefAttributePath, AttributeSensor sensor) {
             return addSensor(new ChefAttributePollConfig(sensor).chefAttributePath(chefAttributePath));
         }
@@ -167,6 +168,10 @@ public class ChefAttributeFeed extends AbstractFeed {
         public Builder period(long val, TimeUnit units) {
             return period(Duration.of(val, units));
         }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
         public ChefAttributeFeed build() {
             built = true;
             ChefAttributeFeed result = new ChefAttributeFeed(this);
@@ -200,6 +205,7 @@ public class ChefAttributeFeed extends AbstractFeed {
             polls.add(configCopy);
         }
         setConfig(POLLS, polls);
+        initUniqueTag(builder.uniqueTag, polls);
     }
 
     @Override
@@ -208,7 +214,6 @@ public class ChefAttributeFeed extends AbstractFeed {
         final Set<ChefAttributePollConfig<?>> polls = getConfig(POLLS);
         
         long minPeriod = Integer.MAX_VALUE;
-        SortedSet<String> performanceCounterNames = Sets.newTreeSet();
         for (ChefAttributePollConfig<?> config : polls) {
             minPeriod = Math.min(minPeriod, config.getPeriod());
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/software/base/src/main/java/brooklyn/entity/chef/ChefAttributePollConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/entity/chef/ChefAttributePollConfig.java b/software/base/src/main/java/brooklyn/entity/chef/ChefAttributePollConfig.java
index b76b6ad..9d9ca82 100644
--- a/software/base/src/main/java/brooklyn/entity/chef/ChefAttributePollConfig.java
+++ b/software/base/src/main/java/brooklyn/entity/chef/ChefAttributePollConfig.java
@@ -47,8 +47,7 @@ public class ChefAttributePollConfig<T> extends PollConfig<Object, T, ChefAttrib
         this.chefAttributePath = val; return this;
     }
     
-    @Override
-    public String toString() {
-        return "chef["+chefAttributePath+"]";
-    }
+    @Override protected String toStringBaseName() { return "chef"; }
+    @Override protected String toStringPollSource() { return chefAttributePath; }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/software/base/src/main/java/brooklyn/event/feed/jmx/JmxAttributePollConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxAttributePollConfig.java b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxAttributePollConfig.java
index 886f03f..7eba61b 100644
--- a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxAttributePollConfig.java
+++ b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxAttributePollConfig.java
@@ -68,8 +68,7 @@ public class JmxAttributePollConfig<T> extends PollConfig<Object, T, JmxAttribut
         this.attributeName = val; return this;
     }
     
-    @Override
-    public String toString() {
-        return "jmx["+objectName+":"+attributeName+"]";
-    }
+    @Override protected String toStringBaseName() { return "jmx"; }
+    @Override protected String toStringPollSource() { return objectName+":"+attributeName; }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/software/base/src/main/java/brooklyn/event/feed/jmx/JmxFeed.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxFeed.java b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxFeed.java
index 132740c..de0a553 100644
--- a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxFeed.java
+++ b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxFeed.java
@@ -127,6 +127,7 @@ public class JmxFeed extends AbstractFeed {
         private List<JmxAttributePollConfig<?>> attributePolls = Lists.newArrayList();
         private List<JmxOperationPollConfig<?>> operationPolls = Lists.newArrayList();
         private List<JmxNotificationSubscriptionConfig<?>> notificationSubscriptions = Lists.newArrayList();
+        private String uniqueTag;
         private volatile boolean built;
         
         public Builder entity(EntityLocal val) {
@@ -160,6 +161,10 @@ public class JmxFeed extends AbstractFeed {
             notificationSubscriptions.add(config);
             return this;
         }
+        public Builder uniqueTag(String uniqueTag) {
+            this.uniqueTag = uniqueTag;
+            return this;
+        }
         public JmxFeed build() {
             built = true;
             JmxFeed result = new JmxFeed(this);
@@ -214,6 +219,7 @@ public class JmxFeed extends AbstractFeed {
             notificationSubscriptions.put(config.getNotificationFilter(), config);
         }
         setConfig(NOTIFICATION_SUBSCRIPTIONS, notificationSubscriptions);
+        initUniqueTag(builder.uniqueTag, attributePolls, operationPolls, notificationSubscriptions);
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/software/base/src/main/java/brooklyn/event/feed/jmx/JmxNotificationSubscriptionConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxNotificationSubscriptionConfig.java b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxNotificationSubscriptionConfig.java
index d7ebbae..d92017b 100644
--- a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxNotificationSubscriptionConfig.java
+++ b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxNotificationSubscriptionConfig.java
@@ -25,6 +25,7 @@ import javax.management.ObjectName;
 
 import brooklyn.event.AttributeSensor;
 import brooklyn.event.feed.FeedConfig;
+import brooklyn.util.collections.MutableList;
 
 import com.google.common.base.Function;
 import com.google.common.base.Functions;
@@ -35,6 +36,7 @@ public class JmxNotificationSubscriptionConfig<T> extends FeedConfig<javax.manag
     private NotificationFilter notificationFilter;
     private Function<Notification, T> onNotification;
 
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     public JmxNotificationSubscriptionConfig(AttributeSensor<T> sensor) {
         super(sensor);
         onSuccess((Function)Functions.identity());
@@ -78,4 +80,16 @@ public class JmxNotificationSubscriptionConfig<T> extends FeedConfig<javax.manag
     public JmxNotificationSubscriptionConfig<T> onNotification(Function<Notification,T> val) {
         this.onNotification = val; return this;
     }
+
+    @Override
+    protected Object toStringPollSource() {
+        return objectName;
+    }
+
+    @Override
+    protected MutableList<Object> equalsFields() {
+        return super.equalsFields()
+            .appendIfNotNull(notificationFilter).appendIfNotNull(onNotification);
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/software/base/src/main/java/brooklyn/event/feed/jmx/JmxOperationPollConfig.java
----------------------------------------------------------------------
diff --git a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxOperationPollConfig.java b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxOperationPollConfig.java
index 40b2573..c9768bc 100644
--- a/software/base/src/main/java/brooklyn/event/feed/jmx/JmxOperationPollConfig.java
+++ b/software/base/src/main/java/brooklyn/event/feed/jmx/JmxOperationPollConfig.java
@@ -114,4 +114,8 @@ public class JmxOperationPollConfig<T> extends PollConfig<Object, T, JmxOperatio
             return derivedSignature;
         }
     }
+
+    @Override protected String toStringBaseName() { return "jmx"; }
+    @Override protected String toStringPollSource() { return objectName+":"+operationName+(params!=null ? params : "[]"); }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/utils/common/src/main/java/brooklyn/util/guava/Maybe.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/guava/Maybe.java b/utils/common/src/main/java/brooklyn/util/guava/Maybe.java
index 8d18057..1c0fcda 100644
--- a/utils/common/src/main/java/brooklyn/util/guava/Maybe.java
+++ b/utils/common/src/main/java/brooklyn/util/guava/Maybe.java
@@ -33,6 +33,7 @@ import brooklyn.util.javalang.JavaClassNames;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Function;
+import com.google.common.base.Objects;
 import com.google.common.base.Optional;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Supplier;
@@ -277,4 +278,19 @@ public abstract class Maybe<T> implements Serializable, Supplier<T> {
         return JavaClassNames.simpleClassName(this)+"["+(isPresent()?"value="+get():"")+"]";
     }
 
+    @Override
+    public int hashCode() {
+        if (!isPresent()) return Objects.hashCode(31, isPresent());
+        return Objects.hashCode(31, get());
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof Maybe)) return false;
+        Maybe<?> other = (Maybe<?>)obj;
+        if (!isPresent()) 
+            return !other.isPresent();
+        return Objects.equal(get(), other.get());
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/utils/common/src/main/java/brooklyn/util/javalang/Equals.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/javalang/Equals.java b/utils/common/src/main/java/brooklyn/util/javalang/Equals.java
index 89736b4..621d1dd 100644
--- a/utils/common/src/main/java/brooklyn/util/javalang/Equals.java
+++ b/utils/common/src/main/java/brooklyn/util/javalang/Equals.java
@@ -18,12 +18,21 @@
  */
 package brooklyn.util.javalang;
 
+import java.lang.reflect.Field;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import brooklyn.util.exceptions.Exceptions;
+
 import com.google.common.annotations.Beta;
 import com.google.common.base.Objects;
 
 
 public class Equals {
 
+    private static final Logger log = LoggerFactory.getLogger(Equals.class);
+    
     /** Tests whether the objects given are either all null or all equal to the first argument */
     public static boolean objects(Object o1, Object o2, Object... oo) {
         if (!Objects.equal(o1, o2)) return false;
@@ -56,4 +65,30 @@ public class Equals {
         return true;        
     }
 
+    /** Useful for debugging EqualsBuilder.reflectionEquals */
+    public static void dumpReflectiveEquals(Object o1, Object o2) {
+        log.info("Comparing: "+o1+" "+o2);
+        Class<?> clazz = o1.getClass();
+        while (!(clazz.equals(Object.class))) {
+            log.info("  fields in: "+clazz);
+            for (Field f: clazz.getDeclaredFields()) {
+                f.setAccessible(true);
+                try {
+                    log.info( "    "+(Objects.equal(f.get(o1), f.get(o2)) ? "==" : "!=" ) +
+                        " "+ f.getName()+ " "+ f.get(o1) +" "+ f.get(o2) +
+                        " ("+ classOf(f.get(o1)) +" "+ classOf(f.get(o2)+")") );
+                } catch (Exception e) {
+                    Exceptions.propagateIfFatal(e);
+                    log.info( "    <error> "+e);
+                }
+            }
+            clazz = clazz.getSuperclass();
+        }
+    }
+
+    private static String classOf(Object o) {
+        if (o==null) return null;
+        return o.getClass().toString();
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/2af34226/utils/common/src/main/java/brooklyn/util/text/Strings.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/text/Strings.java b/utils/common/src/main/java/brooklyn/util/text/Strings.java
index 7145eaa..3117d7d 100644
--- a/utils/common/src/main/java/brooklyn/util/text/Strings.java
+++ b/utils/common/src/main/java/brooklyn/util/text/Strings.java
@@ -24,12 +24,14 @@ import java.text.NumberFormat;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.StringTokenizer;
 
 import javax.annotation.Nullable;
 
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.time.Time;
 
@@ -848,4 +850,24 @@ public class Strings {
             return null;
         }
     }
+    
+    /** Returns canonicalized string from the given object, made "unique" by:
+     * <li> putting sets into the toString order
+     * <li> appending a hash code if it's longer than the max (and the max is bigger than 0) */
+    public static String toUniqueString(Object x, int optionalMax) {
+        if (x instanceof Iterable && !(x instanceof List)) {
+            // unsorted collections should have a canonical order imposed
+            MutableList<String> result = MutableList.of();
+            for (Object xi: (Iterable<?>)x) {
+                result.add(toUniqueString(xi, optionalMax));
+            }
+            Collections.sort(result);
+            x = result.toString();
+        }
+        if (x==null) return "{null}";
+        String xs = x.toString();
+        if (xs.length()<=optionalMax || optionalMax<=0) return xs;
+        return maxlenWithEllipsis(xs, optionalMax-8)+"/"+Integer.toHexString(xs.hashCode());
+    }
+
 }