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:28 UTC

[1/6] incubator-brooklyn git commit: allow brooklyn.properties to be used in setup.script.vars

Repository: incubator-brooklyn
Updated Branches:
  refs/heads/master 0f50c58b6 -> a5db1546b


allow brooklyn.properties to be used in setup.script.vars


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

Branch: refs/heads/master
Commit: 92f775f89c93b620c3a1e9e73952ca095620dee0
Parents: ae69245
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Nov 19 21:07:57 2014 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Thu Nov 20 12:47:05 2014 +0000

----------------------------------------------------------------------
 core/src/main/java/brooklyn/util/text/TemplateProcessor.java       | 2 +-
 .../src/main/java/brooklyn/location/jclouds/JcloudsLocation.java   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/92f775f8/core/src/main/java/brooklyn/util/text/TemplateProcessor.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/text/TemplateProcessor.java b/core/src/main/java/brooklyn/util/text/TemplateProcessor.java
index f6c35c4..9f30c88 100644
--- a/core/src/main/java/brooklyn/util/text/TemplateProcessor.java
+++ b/core/src/main/java/brooklyn/util/text/TemplateProcessor.java
@@ -107,7 +107,7 @@ public class TemplateProcessor {
     }
 
     /** Processes template contents according to {@link EntityAndMapTemplateModel}. */
-    public static String processTemplateContents(String templateContents, ManagementContextInternal managementContext, Map<String,? extends Object> extraSubstitutions) {
+    public static String processTemplateContents(String templateContents, ManagementContext managementContext, Map<String,? extends Object> extraSubstitutions) {
         return processTemplateContents(templateContents, new EntityAndMapTemplateModel(managementContext, extraSubstitutions));
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/92f775f8/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java
----------------------------------------------------------------------
diff --git a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java
index 26dc4ef..0017025 100644
--- a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java
+++ b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java
@@ -694,7 +694,7 @@ public class JcloudsLocation extends AbstractCloudMachineProvisioningLocation im
                             ? Splitter.on(",").withKeyValueSeparator(":").split(setupVarsString)
                             : ImmutableMap.<String, String>of();
                     String scriptContent =  ResourceUtils.create(this).getResourceAsString(setupScript);
-                    String script = TemplateProcessor.processTemplateContents(scriptContent, substitutions);
+                    String script = TemplateProcessor.processTemplateContents(scriptContent, getManagementContext(), substitutions);
                     sshMachineLocation.execCommands("Customizing node " + this, ImmutableList.of(script));
                 }
                 


[4/6] incubator-brooklyn git commit: de-dupes mark two - persist uniqueTag

Posted by he...@apache.org.
de-dupes mark two - persist uniqueTag

previously uniqueTag was not being persisted so we were getting duplication in some situations,
or were relying on the equality check; now we persist this, and we say something is apparently
equal even if it doesn't have the unique tag (and we transfer the tag across).

note that rebinded adjuncts are added *after* those done in the entity itself (via the rebind() call there);
this seems surprising, but should be okay as ideally we shouldn't be adding new adjuncts at all.
(but we might need some special logic in order to put in place "change my adjuncts on rebind" behaviour at an entity.)


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

Branch: refs/heads/master
Commit: ff0abc34b317907f79cf26bb4664da6324bb90ee
Parents: 2af3422
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Fri Nov 21 14:43:24 2014 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Fri Nov 21 14:56:15 2014 +0000

----------------------------------------------------------------------
 api/src/main/java/brooklyn/entity/Feed.java     |   5 +-
 .../main/java/brooklyn/mementos/Memento.java    |   6 +
 .../java/brooklyn/policy/EntityAdjunct.java     |   2 +-
 .../brooklyn/basic/AbstractBrooklynObject.java  |  15 +-
 .../brooklyn/entity/basic/AbstractEntity.java   |  32 +++-
 .../java/brooklyn/entity/basic/Entities.java    |   3 +-
 .../AbstractBrooklynObjectRebindSupport.java    |   6 +
 .../entity/rebind/RebindManagerImpl.java        |  11 +-
 .../entity/rebind/dto/AbstractMemento.java      |  61 ++++---
 .../entity/rebind/dto/BasicLocationMemento.java |   1 -
 .../entity/rebind/dto/MementosGenerators.java   |   8 +-
 .../java/brooklyn/event/feed/AbstractFeed.java  |  56 +++---
 .../java/brooklyn/event/feed/FeedConfig.java    |   6 +
 .../main/java/brooklyn/event/feed/Poller.java   |  26 ++-
 .../event/feed/function/FunctionPollConfig.java |   8 +
 .../internal/BrooklynGarbageCollector.java      |  13 +-
 .../policy/basic/AbstractEntityAdjunct.java     |   8 +-
 .../brooklyn/entity/rebind/RebindFeedTest.java  | 178 +++++++++++++++++--
 .../entity/rebind/RebindTestFixture.java        |  40 ++++-
 .../event/feed/function/FunctionFeedTest.java   |  38 ++--
 .../brooklyn/management/ha/HotStandbyTest.java  |  48 ++++-
 .../java/brooklyn/util/guava/Functionals.java   |  21 +++
 22 files changed, 470 insertions(+), 122 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/api/src/main/java/brooklyn/entity/Feed.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/Feed.java b/api/src/main/java/brooklyn/entity/Feed.java
index 10d1fbd..029ed78 100644
--- a/api/src/main/java/brooklyn/entity/Feed.java
+++ b/api/src/main/java/brooklyn/entity/Feed.java
@@ -48,8 +48,9 @@ public interface Feed extends EntityAdjunct, Rebindable {
     boolean isActivated();
     
     /** 
-     * @eturn true iff the feed is running
-     */
+     * @return true iff the feed is actually running (like {@link #isActivated()}, but including other items like poll jobs not cancelled)
+     * @deprecated since 0.7.0 use {@link #isRunning()}
+     */ @Deprecated
     boolean isActive();
     
     void start();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/api/src/main/java/brooklyn/mementos/Memento.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/mementos/Memento.java b/api/src/main/java/brooklyn/mementos/Memento.java
index bb6f8b1..dfe6d4a 100644
--- a/api/src/main/java/brooklyn/mementos/Memento.java
+++ b/api/src/main/java/brooklyn/mementos/Memento.java
@@ -22,7 +22,9 @@ import java.io.Serializable;
 import java.util.Collection;
 import java.util.Map;
 
+import brooklyn.entity.Entity;
 import brooklyn.entity.rebind.RebindSupport;
+import brooklyn.policy.EntityAdjunct;
 
 /**
  * Represents the internal state of something in brooklyn, so that it can be reconstructed (e.g. after restarting brooklyn).
@@ -74,4 +76,8 @@ public interface Memento extends Serializable {
     public Class<?> getTypeClass();
 
     public Collection<Object> getTags();
+    
+    /** Null for {@link Entity}, but important for adjuncts; see {@link EntityAdjunct#getUniqueTag()} */
+    public String getUniqueTag();
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/api/src/main/java/brooklyn/policy/EntityAdjunct.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/policy/EntityAdjunct.java b/api/src/main/java/brooklyn/policy/EntityAdjunct.java
index ed42605..4405ea2 100644
--- a/api/src/main/java/brooklyn/policy/EntityAdjunct.java
+++ b/api/src/main/java/brooklyn/policy/EntityAdjunct.java
@@ -45,7 +45,7 @@ public interface EntityAdjunct extends BrooklynObject {
     boolean isDestroyed();
     
     /**
-     * Whether the adjunct is available/active
+     * Whether the adjunct is available/active, ie started and not stopped or interrupted
      */
     boolean isRunning();
     

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/basic/AbstractBrooklynObject.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/basic/AbstractBrooklynObject.java b/core/src/main/java/brooklyn/basic/AbstractBrooklynObject.java
index ff1bbd8..9195b35 100644
--- a/core/src/main/java/brooklyn/basic/AbstractBrooklynObject.java
+++ b/core/src/main/java/brooklyn/basic/AbstractBrooklynObject.java
@@ -26,6 +26,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import brooklyn.basic.internal.ApiObjectsFactory;
+import brooklyn.entity.basic.AbstractEntity;
 import brooklyn.entity.proxying.InternalFactory;
 import brooklyn.entity.rebind.RebindManagerImpl;
 import brooklyn.management.ManagementContext;
@@ -131,10 +132,18 @@ public abstract class AbstractBrooklynObject implements BrooklynObjectInternal {
     }
 
     /**
-     * Called by framework on rebind (in new-style instances),
-     * after configuring but before the instance is managed (or is attached to an entity, depending on its type),
-     * and before a reference to this policy is shared.
+     * Called by framework on rebind (in new-style instances):
+     * <ul>
+     * <li> after configuring, but
+     * <li> before the instance is managed, and
+     * <li> before adjuncts are attached to entities, and
+     * <li> before a reference to an object is shared.
+     * </ul>
      * Note that {@link #init()} will not be called on rebind.
+     * <p>
+     * If you need to intercept behaviour <i>after</i> adjuncts are attached,
+     * consider {@link AbstractEntity#onManagementStarting()} 
+     * (but probably worth raising a discussion on the mailing list!)
      */
     public void rebind() {
         // no-op

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/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 5dfec58..de28236 100644
--- a/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
+++ b/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
@@ -81,6 +81,7 @@ import brooklyn.policy.EntityAdjunct;
 import brooklyn.policy.Policy;
 import brooklyn.policy.PolicySpec;
 import brooklyn.policy.basic.AbstractEntityAdjunct;
+import brooklyn.policy.basic.AbstractEntityAdjunct.AdjunctTagSupport;
 import brooklyn.policy.basic.AbstractPolicy;
 import brooklyn.util.BrooklynLanguageExtensions;
 import brooklyn.util.collections.MutableList;
@@ -1226,13 +1227,24 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
     }
     
     private <T extends EntityAdjunct> T findApparentlyEqualAndWarnIfNotSameUniqueTag(Collection<? extends T> items, T newItem) {
-        T oldItem = findApparentlyEqual(items, newItem);
+        T oldItem = findApparentlyEqual(items, newItem, true);
         
         if (oldItem!=null) {
+            String oldItemTag = oldItem.getUniqueTag();
             String newItemTag = newItem.getUniqueTag();
-            if (newItemTag!=null) {
-                return oldItem;
+            if (oldItemTag!=null || newItemTag!=null) {
+                if (Objects.equal(oldItemTag, newItemTag)) {
+                    // if same tag, return old item for replacing without comment
+                    return oldItem;
+                }
+                // if one has a tag bug not the other, and they are apparently equal,
+                // transfer the tag across
+                T tagged = oldItemTag!=null ? oldItem : newItem;
+                T tagless = oldItemTag!=null ? newItem : oldItem;
+                LOG.warn("Apparently equal items "+oldItem+" and "+newItem+"; but one has a unique tag "+tagged.getUniqueTag()+"; applying to the other");
+                ((AdjunctTagSupport)tagless.tags()).setUniqueTag(tagged.getUniqueTag());
             }
+            
             if (isRebinding()) {
                 LOG.warn("Adding to "+this+", "+newItem+" appears identical to existing "+oldItem+"; will replace. "
                     + "Underlying addition should be modified so it is not added twice during rebind or unique tag should be used to indicate it is identical.");
@@ -1246,7 +1258,7 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
             return null;
         }
     }
-    private <T extends EntityAdjunct> T findApparentlyEqual(Collection<? extends T> itemsCopy, T newItem) {
+    private <T extends EntityAdjunct> T findApparentlyEqual(Collection<? extends T> itemsCopy, T newItem, boolean transferUniqueTag) {
         // 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;
@@ -1261,10 +1273,16 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
         
         String newItemTag = newItem.getUniqueTag();
         for (T oldItem: itemsCopy) {
-            if (oldItem.getUniqueTag()!=null) {
-                if ((oldItem.getUniqueTag().equals(newItemTag)))
+            String oldItemTag = oldItem.getUniqueTag();
+            if (oldItemTag!=null && newItemTag!=null) { 
+                if (oldItemTag.equals(newItemTag)) {
                     return oldItem;
-            } else if (newItemTag==null && oldItem.getClass().equals(newItem.getClass())) {
+                } else {
+                    continue;
+                }
+            }
+            // either does not have a unique tag, do deep equality
+            if (oldItem.getClass().equals(newItem.getClass())) {
                 if (EqualsBuilder.reflectionEquals(oldItem, newItem, false,
                         // internal admin in 'beforeEntityAdjunct' should be ignored
                         beforeEntityAdjunct,

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/entity/basic/Entities.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/Entities.java b/core/src/main/java/brooklyn/entity/basic/Entities.java
index 51f86f6..7736d50 100644
--- a/core/src/main/java/brooklyn/entity/basic/Entities.java
+++ b/core/src/main/java/brooklyn/entity/basic/Entities.java
@@ -742,8 +742,9 @@ public class Entities {
         for (Location loc : mgmt.getLocationManager().getLocations()) {
             destroyCatching(loc);
         }
-        if (mgmt instanceof ManagementContextInternal)
+        if (mgmt instanceof ManagementContextInternal) {
             ((ManagementContextInternal)mgmt).terminate();
+        }
         if (error!=null) throw Exceptions.propagate(error);
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/entity/rebind/AbstractBrooklynObjectRebindSupport.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/AbstractBrooklynObjectRebindSupport.java b/core/src/main/java/brooklyn/entity/rebind/AbstractBrooklynObjectRebindSupport.java
index edd70c3..0d6e663 100644
--- a/core/src/main/java/brooklyn/entity/rebind/AbstractBrooklynObjectRebindSupport.java
+++ b/core/src/main/java/brooklyn/entity/rebind/AbstractBrooklynObjectRebindSupport.java
@@ -24,6 +24,9 @@ import org.slf4j.LoggerFactory;
 import brooklyn.basic.AbstractBrooklynObject;
 import brooklyn.entity.rebind.dto.MementosGenerators;
 import brooklyn.mementos.Memento;
+import brooklyn.policy.EntityAdjunct;
+import brooklyn.policy.basic.AbstractEntityAdjunct.AdjunctTagSupport;
+import brooklyn.util.text.Strings;
 
 public abstract class AbstractBrooklynObjectRebindSupport<T extends Memento> implements RebindSupport<T> {
 
@@ -63,6 +66,9 @@ public abstract class AbstractBrooklynObjectRebindSupport<T extends Memento> imp
     protected abstract void addCustoms(RebindContext rebindContext, T memento);
     
     protected void addTags(RebindContext rebindContext, T memento) {
+        if (instance instanceof EntityAdjunct && Strings.isNonBlank(memento.getUniqueTag())) {
+            ((AdjunctTagSupport)(instance.tags())).setUniqueTag(memento.getUniqueTag());
+        }
         for (Object tag : memento.getTags()) {
             instance.tags().addTag(tag);
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
index 2a282b0..f53f93a 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -58,8 +58,8 @@ import brooklyn.entity.proxying.InternalLocationFactory;
 import brooklyn.entity.proxying.InternalPolicyFactory;
 import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
 import brooklyn.entity.rebind.persister.BrooklynPersistenceUtils;
-import brooklyn.entity.rebind.persister.PersistenceActivityMetrics;
 import brooklyn.entity.rebind.persister.BrooklynPersistenceUtils.CreateBackupMode;
+import brooklyn.entity.rebind.persister.PersistenceActivityMetrics;
 import brooklyn.event.feed.AbstractFeed;
 import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.location.Location;
@@ -565,7 +565,7 @@ public class RebindManagerImpl implements RebindManager {
             //  3. instantiate entities+locations so that inter-entity references can subsequently be set during deserialize (and entity config/state is set).
             //  4. deserialize the memento
             //  5. instantiate policies+enricherss+feeds (could perhaps merge this with (3), depending how they are implemented)
-            //  6. reconstruct the entities etc (i.e. calling init on the already-instantiated instances).
+            //  6. reconstruct the entities etc (i.e. calling rebind() on the already-instantiated instances)
             //  7. add policies+enrichers+feeds to all the entities.
             //  8. manage the entities
             
@@ -1473,6 +1473,10 @@ public class RebindManagerImpl implements RebindManager {
         return (readOnlyRebindCount < 5) || (readOnlyRebindCount%1000==0);
     }
 
+    public int getReadOnlyRebindCount() {
+        return readOnlyRebindCount;
+    }
+    
     @Override
     public Map<String, Object> getMetrics() {
         Map<String,Object> result = MutableMap.of();
@@ -1480,6 +1484,9 @@ public class RebindManagerImpl implements RebindManager {
         result.put("rebind", rebindMetrics.asMap());
         result.put("persist", persistMetrics.asMap());
         
+        if (readOnlyRebindCount>=0)
+            result.put("rebindReadOnlyCount", readOnlyRebindCount);
+        
         // include first rebind counts, so we know whether we rebinded or not
         result.put("firstRebindCounts", MutableMap.of(
             "applications", firstRebindAppCount,

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/entity/rebind/dto/AbstractMemento.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/dto/AbstractMemento.java b/core/src/main/java/brooklyn/entity/rebind/dto/AbstractMemento.java
index c7ec1d9..5603e65 100644
--- a/core/src/main/java/brooklyn/entity/rebind/dto/AbstractMemento.java
+++ b/core/src/main/java/brooklyn/entity/rebind/dto/AbstractMemento.java
@@ -44,13 +44,17 @@ public abstract class AbstractMemento implements Memento, Serializable {
         protected Class<?> typeClass;
         protected String displayName;
         protected String catalogItemId;
-        protected Map<String, Object> fields = Maps.newLinkedHashMap();
+        protected Map<String, Object> customFields = Maps.newLinkedHashMap();
         protected List<Object> tags = Lists.newArrayList();
+        
+        // only supported for EntityAdjuncts
+        protected String uniqueTag;
 
         @SuppressWarnings("unchecked")
         protected B self() {
             return (B) this;
         }
+        @SuppressWarnings("deprecation")
         public B from(Memento other) {
             brooklynVersion = other.getBrooklynVersion();
             id = other.getId();
@@ -58,34 +62,38 @@ public abstract class AbstractMemento implements Memento, Serializable {
             typeClass = other.getTypeClass();
             displayName = other.getDisplayName();
             catalogItemId = other.getCatalogItemId();
-            fields.putAll(other.getCustomFields());
+            customFields.putAll(other.getCustomFields());
             tags.addAll(other.getTags());
+            uniqueTag = other.getUniqueTag();
             return self();
         }
-        public B brooklynVersion(String val) {
-            brooklynVersion = val; return self();
-        }
-        public B id(String val) {
-            id = val; return self();
-        }
-        public B type(String val) {
-            type = val; return self();
-        }
-        public B typeClass(Class<?> val) {
-            typeClass = val; return self();
-        }
-        public B displayName(String val) {
-            displayName = val; return self();
-        }
-        public B catalogItemId(String val) {
-            catalogItemId = val; return self();
-        }
+        // this method set is incomplete; and they are not used, as the protected fields are set directly
+        // kept in case we want to expose this elsewhere, but we should complete the list
+//        public B brooklynVersion(String val) {
+//            brooklynVersion = val; return self();
+//        }
+//        public B id(String val) {
+//            id = val; return self();
+//        }
+//        public B type(String val) {
+//            type = val; return self();
+//        }
+//        public B typeClass(Class<?> val) {
+//            typeClass = val; return self();
+//        }
+//        public B displayName(String val) {
+//            displayName = val; return self();
+//        }
+//        public B catalogItemId(String val) {
+//            catalogItemId = val; return self();
+//        }
+        
         /**
          * @deprecated since 0.7.0; use config/attributes so generic persistence will work, rather than requiring "custom fields"
          */
         @Deprecated
         public B customFields(Map<String,?> vals) {
-            fields.putAll(vals); return self();
+            customFields.putAll(vals); return self();
         }
     }
     
@@ -95,6 +103,9 @@ public abstract class AbstractMemento implements Memento, Serializable {
     private String displayName;
     private String catalogItemId;
     private List<Object> tags;
+    
+    // for EntityAdjuncts; not used for entity
+    private String uniqueTag;
 
     private transient Class<?> typeClass;
 
@@ -110,8 +121,9 @@ public abstract class AbstractMemento implements Memento, Serializable {
         typeClass = builder.typeClass;
         displayName = builder.displayName;
         catalogItemId = builder.catalogItemId;
-        setCustomFields(builder.fields);
+        setCustomFields(builder.customFields);
         tags = toPersistedList(builder.tags);
+        uniqueTag = builder.uniqueTag;
     }
 
     // "fields" is not included as a field here, so that it is serialized after selected subclass fields
@@ -157,6 +169,11 @@ public abstract class AbstractMemento implements Memento, Serializable {
     public List<Object> getTags() {
         return fromPersistedList(tags);
     }
+
+    @Override
+    public String getUniqueTag() {
+        return uniqueTag;
+    }
     
     @Deprecated
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/entity/rebind/dto/BasicLocationMemento.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/dto/BasicLocationMemento.java b/core/src/main/java/brooklyn/entity/rebind/dto/BasicLocationMemento.java
index 7a0b0bd..12c958c 100644
--- a/core/src/main/java/brooklyn/entity/rebind/dto/BasicLocationMemento.java
+++ b/core/src/main/java/brooklyn/entity/rebind/dto/BasicLocationMemento.java
@@ -55,7 +55,6 @@ public class BasicLocationMemento extends AbstractTreeNodeMemento implements Loc
             locationConfig.putAll(other.getLocationConfig());
             locationConfigUnused.addAll(other.getLocationConfigUnused());
             locationConfigDescription = other.getLocationConfigDescription();
-            fields.putAll(other.getCustomFields());
             return self();
         }
         public LocationMemento build() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/entity/rebind/dto/MementosGenerators.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/dto/MementosGenerators.java b/core/src/main/java/brooklyn/entity/rebind/dto/MementosGenerators.java
index 7e8d598..8ec638a 100644
--- a/core/src/main/java/brooklyn/entity/rebind/dto/MementosGenerators.java
+++ b/core/src/main/java/brooklyn/entity/rebind/dto/MementosGenerators.java
@@ -44,6 +44,7 @@ import brooklyn.location.basic.AbstractLocation;
 import brooklyn.location.basic.LocationInternal;
 import brooklyn.management.ManagementContext;
 import brooklyn.management.Task;
+import brooklyn.management.internal.NonDeploymentManagementContext;
 import brooklyn.mementos.BrooklynMemento;
 import brooklyn.mementos.CatalogItemMemento;
 import brooklyn.mementos.EnricherMemento;
@@ -53,6 +54,7 @@ import brooklyn.mementos.LocationMemento;
 import brooklyn.mementos.Memento;
 import brooklyn.mementos.PolicyMemento;
 import brooklyn.policy.Enricher;
+import brooklyn.policy.EntityAdjunct;
 import brooklyn.policy.Policy;
 import brooklyn.policy.basic.AbstractPolicy;
 import brooklyn.util.collections.MutableMap;
@@ -178,7 +180,7 @@ public class MementosGenerators {
         for (Location location : entity.getLocations()) {
             builder.locations.add(location.getId()); 
         }
-        
+
         for (Entity child : entity.getChildren()) {
             builder.children.add(child.getId()); 
         }
@@ -385,7 +387,9 @@ public class MementosGenerators {
         builder.catalogItemId = instance.getCatalogItemId();
         builder.type = instance.getClass().getName();
         builder.typeClass = instance.getClass();
-        
+        if (instance instanceof EntityAdjunct) {
+            builder.uniqueTag = ((EntityAdjunct)instance).getUniqueTag();
+        }
         for (Object tag : instance.tags().getTags()) {
             builder.tags.add(tag); 
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/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 5303ac7..d487a9a 100644
--- a/core/src/main/java/brooklyn/event/feed/AbstractFeed.java
+++ b/core/src/main/java/brooklyn/event/feed/AbstractFeed.java
@@ -86,15 +86,9 @@ public abstract class AbstractFeed extends AbstractEntityAdjunct implements Feed
 
     protected void initUniqueTag(String uniqueTag, Object ...valsForDefault) {
         if (Strings.isNonBlank(uniqueTag)) this.uniqueTag = uniqueTag;
-        else if (Strings.isBlank(this.uniqueTag)) this.uniqueTag = getDefaultUniqueTag(valsForDefault);
+        else 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));
@@ -117,29 +111,6 @@ public abstract class AbstractFeed extends AbstractEntityAdjunct implements Feed
     }
 
     @Override
-    public boolean isActivated() {
-        return activated;
-    }
-    
-    @Override
-    public boolean isActive() {
-        return activated && !suspended;
-    }
-    
-    public EntityLocal getEntity() {
-        return entity;
-    }
-    
-    protected boolean isConnected() {
-        // TODO Default impl will result in multiple logs for same error if becomes unreachable
-        // (e.g. if ssh gets NoRouteToHostException, then every AttributePollHandler for that
-        // feed will log.warn - so if polling for 10 sensors/attributes will get 10 log messages).
-        // Would be nice if reduced this logging duplication.
-        // (You can reduce it by providing a better 'isConnected' implementation of course.)
-        return isActivated() && entity!=null && !((EntityInternal)entity).getManagementSupport().isNoLongerManaged();
-    }
-
-    @Override
     public void start() {
         if (log.isDebugEnabled()) log.debug("Starting feed {} for {}", this, entity);
         if (activated) { 
@@ -206,13 +177,36 @@ public abstract class AbstractFeed extends AbstractEntityAdjunct implements Feed
     }
 
     @Override
+    public boolean isActivated() {
+        return activated;
+    }
+    
+    @Override
+    public boolean isActive() {
+        return isRunning();
+    }
+    
+    public EntityLocal getEntity() {
+        return entity;
+    }
+    
+    protected boolean isConnected() {
+        // TODO Default impl will result in multiple logs for same error if becomes unreachable
+        // (e.g. if ssh gets NoRouteToHostException, then every AttributePollHandler for that
+        // feed will log.warn - so if polling for 10 sensors/attributes will get 10 log messages).
+        // Would be nice if reduced this logging duplication.
+        // (You can reduce it by providing a better 'isConnected' implementation of course.)
+        return isRunning() && entity!=null && !((EntityInternal)entity).getManagementSupport().isNoLongerManaged();
+    }
+
+    @Override
     public boolean isSuspended() {
         return suspended;
     }
 
     @Override
     public boolean isRunning() {
-        return !isSuspended() && !isDestroyed();
+        return isActivated() && !isSuspended() && !isDestroyed() && getPoller()!=null && getPoller().isRunning();
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/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 54eeea1..64642e1 100644
--- a/core/src/main/java/brooklyn/event/feed/FeedConfig.java
+++ b/core/src/main/java/brooklyn/event/feed/FeedConfig.java
@@ -24,6 +24,7 @@ import brooklyn.event.AttributeSensor;
 import brooklyn.event.basic.Sensors;
 import brooklyn.event.feed.http.HttpPollConfig;
 import brooklyn.util.collections.MutableList;
+import brooklyn.util.guava.Functionals;
 import brooklyn.util.javalang.JavaClassNames;
 import brooklyn.util.text.Strings;
 
@@ -106,6 +107,11 @@ public class FeedConfig<V, T, F extends FeedConfig<V, T, F>> {
     }
     /** as {@link #checkSuccess(Predicate)} */
     public F checkSuccess(final Function<? super V,Boolean> val) {
+        return checkSuccess(Functionals.predicate(val));
+    }
+    @SuppressWarnings("unused")
+    /** @deprecated since 0.7.0, kept for rebind */ @Deprecated
+    private F checkSuccessLegacy(final Function<? super V,Boolean> val) {
         return checkSuccess(new Predicate<V>() {
             @Override
             public boolean apply(V input) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/event/feed/Poller.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/feed/Poller.java b/core/src/main/java/brooklyn/event/feed/Poller.java
index 8aa8252..6fa9147 100644
--- a/core/src/main/java/brooklyn/event/feed/Poller.java
+++ b/core/src/main/java/brooklyn/event/feed/Poller.java
@@ -56,7 +56,7 @@ public class Poller<V> {
     private final Set<PollJob<V>> pollJobs = new LinkedHashSet<PollJob<V>>();
     private final Set<Task<?>> oneOffTasks = new LinkedHashSet<Task<?>>();
     private final Set<ScheduledTask> tasks = new LinkedHashSet<ScheduledTask>();
-    private volatile boolean running = false;
+    private volatile boolean started = false;
     
     private static class PollJob<V> {
         final PollHandler<? super V> handler;
@@ -106,7 +106,7 @@ public class Poller<V> {
     
     /** Submits a one-off poll job; recommended that callers supply to-String so that task has a decent description */
     public void submit(Callable<?> job) {
-        if (running) {
+        if (started) {
             throw new IllegalStateException("Cannot submit additional tasks after poller has started");
         }
         oneOffJobs.add(job);
@@ -116,7 +116,7 @@ public class Poller<V> {
         scheduleAtFixedRate(job, handler, Duration.millis(period));
     }
     public void scheduleAtFixedRate(Callable<V> job, PollHandler<? super V> handler, Duration period) {
-        if (running) {
+        if (started) {
             throw new IllegalStateException("Cannot schedule additional tasks after poller has started");
         }
         PollJob<V> foo = new PollJob<V>(job, handler, period);
@@ -129,12 +129,12 @@ public class Poller<V> {
         // Is that ok, are can we do better?
         
         if (log.isDebugEnabled()) log.debug("Starting poll for {} (using {})", new Object[] {entity, this});
-        if (running) { 
+        if (started) { 
             throw new IllegalStateException(String.format("Attempt to start poller %s of entity %s when already running", 
                     this, entity));
         }
         
-        running = true;
+        started = true;
         
         for (final Callable<?> oneOffJob : oneOffJobs) {
             Task<?> task = Tasks.builder().dynamic(false).body((Callable<Object>) oneOffJob).name("Poll").description("One-time poll job "+oneOffJob).build();
@@ -168,12 +168,12 @@ public class Poller<V> {
     
     public void stop() {
         if (log.isDebugEnabled()) log.debug("Stopping poll for {} (using {})", new Object[] {entity, this});
-        if (!running) { 
+        if (!started) { 
             throw new IllegalStateException(String.format("Attempt to stop poller %s of entity %s when not running", 
                     this, entity));
         }
         
-        running = false;
+        started = false;
         for (Task<?> task : oneOffTasks) {
             if (task != null) task.cancel(true);
         }
@@ -185,7 +185,17 @@ public class Poller<V> {
     }
 
     public boolean isRunning() {
-        return running;
+        boolean hasActiveTasks = false;
+        for (Task<?> task: tasks) {
+            if (task.isBegun() && !task.isDone()) {
+                hasActiveTasks = true;
+                break;
+            }
+        }
+        if (!started && hasActiveTasks) {
+            log.warn("Poller should not be running, but has active tasks, tasks: "+tasks);
+        }
+        return started && hasActiveTasks;
     }
     
     protected boolean isEmpty() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/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 0f12672..8db02b1 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.guava.Functionals;
 import brooklyn.util.javalang.JavaClassNames;
 
 import com.google.common.base.Supplier;
@@ -74,6 +75,13 @@ public class FunctionPollConfig<S, T> extends PollConfig<S, T, FunctionPollConfi
      */
     @SuppressWarnings("unchecked")
     public <newS> FunctionPollConfig<newS, T> supplier(final Supplier<? extends newS> val) {
+        this.callable = Functionals.callable( checkNotNull(val, "supplier") );
+        return (FunctionPollConfig<newS, T>) this;
+    }
+    
+    /** @deprecated since 0.7.0, kept for legacy compatibility when deserializing */
+    @SuppressWarnings({ "unchecked", "unused" })
+    private <newS> FunctionPollConfig<newS, T> supplierLegacy(final Supplier<? extends newS> val) {
         checkNotNull(val, "supplier");
         this.callable = new Callable<newS>() {
             @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/management/internal/BrooklynGarbageCollector.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/BrooklynGarbageCollector.java b/core/src/main/java/brooklyn/management/internal/BrooklynGarbageCollector.java
index 0eb96e0..402fd4d 100644
--- a/core/src/main/java/brooklyn/management/internal/BrooklynGarbageCollector.java
+++ b/core/src/main/java/brooklyn/management/internal/BrooklynGarbageCollector.java
@@ -216,13 +216,16 @@ public class BrooklynGarbageCollector {
         if (LOG.isDebugEnabled())
             LOG.debug(prefix+" - using "+getUsageString());
     }
-    
-    public String getUsageString() {
-        return
-            Strings.makeSizeString(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())+" / "+
+
+    public static String makeBasicUsageString() {
+        return Strings.makeSizeString(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory())+" / "+
             Strings.makeSizeString(Runtime.getRuntime().totalMemory()) + " memory" +
             " ("+Strings.makeSizeString(MemoryUsageTracker.SOFT_REFERENCES.getBytesUsed()) + " soft); "+
-            Thread.activeCount()+" threads; "+
+            Thread.activeCount()+" threads";
+    }
+    
+    public String getUsageString() {
+        return makeBasicUsageString()+"; "+
             "storage: " + storage.getStorageMetrics() + "; " +
             "tasks: " +
             executionManager.getNumActiveTasks()+" active, "+

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java b/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
index 60597b5..a005e34 100644
--- a/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
+++ b/core/src/main/java/brooklyn/policy/basic/AbstractEntityAdjunct.java
@@ -365,13 +365,19 @@ public abstract class AbstractEntityAdjunct extends AbstractBrooklynObject imple
         return new AdjunctTagSupport();
     }
 
-    protected class AdjunctTagSupport extends BasicTagSupport {
+    public class AdjunctTagSupport extends BasicTagSupport {
         @Override
         public Set<Object> getTags() {
             ImmutableSet.Builder<Object> rb = ImmutableSet.builder().addAll(super.getTags());
             if (getUniqueTag()!=null) rb.add(getUniqueTag());
             return rb.build();
         }
+        public String getUniqueTag() {
+            return AbstractEntityAdjunct.this.getUniqueTag();
+        }
+        public void setUniqueTag(String uniqueTag) {
+            AbstractEntityAdjunct.this.uniqueTag = uniqueTag;
+        }
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/test/java/brooklyn/entity/rebind/RebindFeedTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindFeedTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindFeedTest.java
index e3d3a2a..18c1edc 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindFeedTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindFeedTest.java
@@ -23,10 +23,10 @@ import static org.testng.Assert.assertEquals;
 import java.net.URL;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.Callable;
 
 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;
@@ -49,15 +49,20 @@ import brooklyn.event.feed.ssh.SshValueFunctions;
 import brooklyn.location.Location;
 import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
 import brooklyn.location.basic.SshMachineLocation;
-import brooklyn.management.Task;
+import brooklyn.management.internal.BrooklynGarbageCollector;
 import brooklyn.test.EntityTestUtils;
 import brooklyn.test.entity.TestEntity;
 import brooklyn.test.entity.TestEntityImpl.TestEntityWithoutEnrichers;
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.http.BetterMockWebServer;
-import brooklyn.util.repeat.Repeater;
 import brooklyn.util.task.BasicExecutionManager;
+import brooklyn.util.text.Identifiers;
+import brooklyn.util.text.Strings;
 import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
 
+import com.google.common.base.Function;
+import com.google.common.base.Supplier;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.Callables;
@@ -104,6 +109,7 @@ public class RebindFeedTest extends RebindTestFixtureWithApp {
         assertEquals(origEntity.feeds().getFeeds().size(), 1);
 
         final long taskCountBefore = ((BasicExecutionManager)origManagementContext.getExecutionManager()).getNumIncompleteTasks();
+        log.info("Count of incomplete tasks before "+taskCountBefore);
         
         log.info("Tasks before rebind: "+
             ((BasicExecutionManager)origManagementContext.getExecutionManager()).getAllTasks());
@@ -124,21 +130,8 @@ public class RebindFeedTest extends RebindTestFixtureWithApp {
         Entities.unmanage(origApp);
         origApp = null;
         origManagementContext.getRebindManager().stop();
-        Repeater.create().every(Duration.millis(20)).limitTimeTo(Duration.TEN_SECONDS).until(new Callable<Boolean>() {
-            @Override
-            public Boolean call() throws Exception {
-                origManagementContext.getGarbageCollector().gcIteration();
-                long taskCountAfterAtOld = ((BasicExecutionManager)origManagementContext.getExecutionManager()).getNumIncompleteTasks();
-                List<Task<?>> tasks = ((BasicExecutionManager)origManagementContext.getExecutionManager()).getAllTasks();
-                int unendedTasks = 0;
-                for (Task<?> t: tasks) {
-                    if (!t.isDone()) unendedTasks++;
-                }
-                log.info("Incomplete task count from "+taskCountBefore+" to "+taskCountAfterAtOld+", "+unendedTasks+" unended; tasks remembered are: "+
-                    tasks);
-                return taskCountAfterAtOld==0;
-            }
-        }).runRequiringTrue();
+        
+        waitForTaskCountToBecome(origManagementContext, 0);
     }
 
     @Test(groups="Integration", invocationCount=50)
@@ -187,6 +180,78 @@ public class RebindFeedTest extends RebindTestFixtureWithApp {
         EntityTestUtils.assertAttributeEqualsEventually(newEntity, SENSOR_INT, (Integer)0);
     }
 
+    @Test
+    public void testReRebindDedupesCorrectlyBasedOnTagOrContentsPersisted() throws Exception {
+        doReReReRebindDedupesCorrectlyBasedOnTagOrContentsPersisted(-1, 2, false);
+    }
+    
+    @Test(groups="Integration")
+    public void testReReReReRebindDedupesCorrectlyBasedOnTagOrContentsPersisted() throws Exception {
+        doReReReRebindDedupesCorrectlyBasedOnTagOrContentsPersisted(1000*1000, 50, true);
+    }
+    
+    public void doReReReRebindDedupesCorrectlyBasedOnTagOrContentsPersisted(int datasize, int iterations, boolean soakTest) throws Exception {
+        final int SYSTEM_TASK_COUNT = 2;  // normally 1, persistence; but as long as less than 4 (the original) we're fine
+        final int MAX_ALLOWED_LEAK = 50*1000*1000;  // memory can vary wildly; but our test should eventually hit GB if there's a leak so this is fine
+        
+        TestEntity origEntity = origApp.createAndManageChild(EntitySpec.create(TestEntity.class).impl(MyEntityWithNewFeedsEachTimeImpl.class)
+            .configure(MyEntityWithNewFeedsEachTimeImpl.DATA_SIZE, datasize)
+            .configure(MyEntityWithNewFeedsEachTimeImpl.MAKE_NEW, true));
+        origApp.start(ImmutableList.<Location>of());
+
+        List<Feed> knownFeeds = MutableList.of();
+        TestEntity currentEntity = origEntity;
+        Collection<Feed> currentFeeds = currentEntity.feeds().getFeeds();
+        
+        int expectedCount = 4;
+        assertEquals(currentFeeds.size(), expectedCount);
+        knownFeeds.addAll(currentFeeds);
+        assertEquals(countActive(knownFeeds), expectedCount);
+        origEntity.setConfig(MyEntityWithNewFeedsEachTimeImpl.MAKE_NEW, !soakTest);
+        
+        long usedOriginally = -1;
+        
+        for (int i=0; i<iterations; i++) {
+            log.info("rebinding, iteration "+i);
+            newApp = rebind();
+            
+            // should get 2 new ones
+            if (!soakTest) expectedCount += 2;
+            
+            currentEntity = (TestEntity) Iterables.getOnlyElement(newApp.getChildren());
+            currentFeeds = currentEntity.feeds().getFeeds();
+            assertEquals(currentFeeds.size(), expectedCount, "feeds are: "+currentFeeds);
+            knownFeeds.addAll(currentFeeds);
+
+            switchOriginalToNewManagementContext();
+            waitForTaskCountToBecome(origManagementContext, expectedCount + SYSTEM_TASK_COUNT);
+            assertEquals(countActive(knownFeeds), expectedCount);
+            knownFeeds.clear();
+            knownFeeds.addAll(currentFeeds);
+            
+            if (soakTest) {
+                System.gc(); System.gc();
+                if (usedOriginally<0) {
+                    Time.sleep(Duration.millis(200));  // give things time to settle; means this number should be larger than others, if anything
+                    usedOriginally = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
+                    log.info("Usage after first rebind: "+BrooklynGarbageCollector.makeBasicUsageString()+" ("+Strings.makeJavaSizeString(usedOriginally)+")");
+                } else {
+                    long usedNow = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
+                    log.info("Usage: "+BrooklynGarbageCollector.makeBasicUsageString()+" ("+Strings.makeJavaSizeString(usedNow)+")");
+                    Assert.assertFalse(usedNow-usedOriginally > MAX_ALLOWED_LEAK, "Leaked too much memory: "+Strings.makeJavaSizeString(usedNow)+" now used, was "+Strings.makeJavaSizeString(usedOriginally));
+                }
+            }
+        }
+    }
+    
+    private int countActive(List<Feed> knownFeeds) {
+        int activeCount=0;
+        for (Feed f: knownFeeds) {
+            if (f.isRunning()) activeCount++;
+        }
+        return activeCount;
+    }
+
     public static class MyEntityWithHttpFeedImpl extends TestEntityWithoutEnrichers {
         public static final ConfigKey<URL> BASE_URL = ConfigKeys.newConfigKey(URL.class, "rebindFeedTest.baseUrl");
         
@@ -238,4 +303,81 @@ public class RebindFeedTest extends RebindTestFixtureWithApp {
                     .build());
         }
     }
+    
+    public static class MyEntityWithNewFeedsEachTimeImpl extends TestEntityWithoutEnrichers {
+        public static final ConfigKey<Integer> DATA_SIZE = ConfigKeys.newIntegerConfigKey("datasize", "size of data", -1);
+        public static final ConfigKey<Boolean> MAKE_NEW = ConfigKeys.newBooleanConfigKey("makeNew", "whether to make the 'new' ones each time", true);
+        
+        @Override
+        public void init() {
+            super.init();
+            connectSensors();
+        }
+
+        @Override
+        public void rebind() {
+            super.rebind();
+            connectSensors();
+        }
+        
+        public static class BigStringSupplier implements Supplier<String> {
+            final String prefix;
+            final int size;
+            // just to take up memory/disk space
+            final String sample;
+            public BigStringSupplier(String prefix, int size) {
+                this.prefix = prefix;
+                this.size = size;
+                sample = get();
+            }
+            public String get() {
+                return prefix + (size>=0 ? "-"+Identifiers.makeRandomId(size) : "");
+            }
+            @Override
+            public boolean equals(Object obj) {
+                return (obj instanceof BigStringSupplier) && prefix.equals(((BigStringSupplier)obj).prefix);
+            }
+            @Override
+            public int hashCode() {
+                return prefix.hashCode();
+            }
+        }
+        
+        public void connectSensors() {
+            final Duration PERIOD = Duration.FIVE_SECONDS;
+            int size = getConfig(DATA_SIZE);
+            boolean makeNew = getConfig(MAKE_NEW);
+
+            if (makeNew) addFeed(FunctionFeed.builder().entity(this).period(PERIOD)
+                .poll(FunctionPollConfig.forSensor(SENSOR_STRING)
+                    .supplier(new BigStringSupplier("new-each-time-entity-"+this+"-created-"+System.currentTimeMillis()+"-"+Identifiers.makeRandomId(4), size))
+                    .onResult(new IdentityFunctionLogging())).build());
+
+            addFeed(FunctionFeed.builder().entity(this).period(PERIOD)
+                .poll(FunctionPollConfig.forSensor(SENSOR_STRING)
+                    .supplier(new BigStringSupplier("same-each-time-entity-"+this, size))
+                    .onResult(new IdentityFunctionLogging())).build());
+
+            if (makeNew) addFeed(FunctionFeed.builder().entity(this).period(PERIOD)
+                .uniqueTag("new-each-time-"+Identifiers.makeRandomId(4)+"-"+System.currentTimeMillis())
+                .poll(FunctionPollConfig.forSensor(SENSOR_STRING)
+                    .supplier(new BigStringSupplier("new-each-time-entity-"+this, size))
+                    .onResult(new IdentityFunctionLogging())).build());
+
+            addFeed(FunctionFeed.builder().entity(this).period(PERIOD)
+                .uniqueTag("same-each-time-entity-"+this)
+                .poll(FunctionPollConfig.forSensor(SENSOR_STRING)
+                    .supplier(new BigStringSupplier("same-each-time-entity-"+this, size))
+                    .onResult(new IdentityFunctionLogging())).build());
+        }
+    }
+    
+    public static class IdentityFunctionLogging implements Function<Object,String> {
+        @Override
+        public String apply(Object input) {
+            System.out.println(Strings.maxlen(Strings.toString(input), 80));
+            return Strings.toString(input);
+        }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/core/src/test/java/brooklyn/entity/rebind/RebindTestFixture.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindTestFixture.java b/core/src/test/java/brooklyn/entity/rebind/RebindTestFixture.java
index 24bf357..4337c62 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindTestFixture.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindTestFixture.java
@@ -21,7 +21,9 @@ package brooklyn.entity.rebind;
 import static org.testng.Assert.assertEquals;
 
 import java.io.File;
+import java.util.List;
 import java.util.Set;
+import java.util.concurrent.Callable;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -31,18 +33,23 @@ import org.testng.annotations.BeforeMethod;
 import brooklyn.catalog.BrooklynCatalog;
 import brooklyn.catalog.CatalogItem;
 import brooklyn.catalog.internal.CatalogUtils;
+import brooklyn.entity.Application;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityFunctions;
 import brooklyn.entity.basic.StartableApplication;
 import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
 import brooklyn.entity.rebind.persister.FileBasedObjectStore;
 import brooklyn.entity.rebind.persister.PersistMode;
+import brooklyn.entity.trait.Startable;
 import brooklyn.management.ManagementContext;
+import brooklyn.management.Task;
 import brooklyn.management.ha.HighAvailabilityMode;
 import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.mementos.BrooklynMementoManifest;
 import brooklyn.util.os.Os;
+import brooklyn.util.repeat.Repeater;
+import brooklyn.util.task.BasicExecutionManager;
 import brooklyn.util.text.Identifiers;
 import brooklyn.util.time.Duration;
 
@@ -95,6 +102,37 @@ public abstract class RebindTestFixture<T extends StartableApplication> {
                 .buildUnstarted();
     }
 
+    /** terminates the original management context (not destroying items) and points it at the new one (and same for apps); 
+     * then clears the variables for the new one, so you can re-rebind */
+    protected void switchOriginalToNewManagementContext() {
+        origManagementContext.getRebindManager().stopPersistence();
+        for (Application e: origManagementContext.getApplications()) ((Startable)e).stop();
+        waitForTaskCountToBecome(origManagementContext, 0);
+        origManagementContext.terminate();
+        origManagementContext = (LocalManagementContext) newManagementContext;
+        origApp = newApp;
+        newManagementContext = null;
+        newApp = null;
+    }
+
+    public static void waitForTaskCountToBecome(final ManagementContext mgmt, final int allowedMax) {
+        Repeater.create().every(Duration.millis(20)).limitTimeTo(Duration.TEN_SECONDS).until(new Callable<Boolean>() {
+            @Override
+            public Boolean call() throws Exception {
+                ((LocalManagementContext)mgmt).getGarbageCollector().gcIteration();
+                long taskCountAfterAtOld = ((BasicExecutionManager)mgmt.getExecutionManager()).getNumIncompleteTasks();
+                List<Task<?>> tasks = ((BasicExecutionManager)mgmt.getExecutionManager()).getAllTasks();
+                int unendedTasks = 0;
+                for (Task<?> t: tasks) {
+                    if (!t.isDone()) unendedTasks++;
+                }
+                LOG.info("Count of incomplete tasks now "+taskCountAfterAtOld+", "+unendedTasks+" unended; tasks remembered are: "+
+                    tasks);
+                return taskCountAfterAtOld<=allowedMax;
+            }
+        }).runRequiringTrue();
+    }
+
     protected boolean useLiveManagementContext() {
         return false;
     }
@@ -179,7 +217,7 @@ public abstract class RebindTestFixture<T extends StartableApplication> {
     @SuppressWarnings("unchecked")
     protected T rebind(RebindOptions options) throws Exception {
         if (newApp != null || newManagementContext != null) {
-            throw new IllegalStateException("already rebound");
+            throw new IllegalStateException("already rebound - use switchOriginalToNewManagementContext() if you are trying to rebind multiple times");
         }
         
         options = RebindOptions.create(options);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/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 f2e71a4..1b67b8b 100644
--- a/core/src/test/java/brooklyn/event/feed/function/FunctionFeedTest.java
+++ b/core/src/test/java/brooklyn/event/feed/function/FunctionFeedTest.java
@@ -36,6 +36,7 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.entity.BrooklynAppUnitTestSupport;
+import brooklyn.entity.Feed;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityInternal.FeedSupport;
 import brooklyn.entity.basic.EntityLocal;
@@ -55,6 +56,7 @@ import com.google.common.base.Functions;
 import com.google.common.base.Predicates;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.util.concurrent.Callables;
 
 public class FunctionFeedTest extends BrooklynAppUnitTestSupport {
@@ -104,6 +106,28 @@ public class FunctionFeedTest extends BrooklynAppUnitTestSupport {
     }
     
     @Test
+    public void testFeedDeDupe() throws Exception {
+        testPollsFunctionRepeatedlyToSetAttribute();
+        entity.addFeed(feed);
+        log.info("Feed 0 is: "+feed);
+        Feed feed0 = feed;
+        
+        testPollsFunctionRepeatedlyToSetAttribute();
+        entity.addFeed(feed);
+        log.info("Feed 1 is: "+feed);
+        Feed feed1 = feed;
+        Assert.assertFalse(feed1==feed0);
+
+        FeedSupport feeds = ((EntityInternal)entity).feeds();
+        Assert.assertEquals(feeds.getFeeds().size(), 1, "Wrong feed count: "+feeds.getFeeds());
+
+        // a couple extra checks, compared to the de-dupe test in other *FeedTest classes
+        Feed feedAdded = Iterables.getOnlyElement(feeds.getFeeds());
+        Assert.assertTrue(feedAdded==feed1);
+        Assert.assertFalse(feedAdded==feed0);
+    }
+    
+    @Test
     public void testCallsOnSuccessWithResultOfCallable() throws Exception {
         feed = FunctionFeed.builder()
                 .entity(entity)
@@ -224,20 +248,6 @@ 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/ff0abc34/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
index d76faeb..9c36eb8 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -27,6 +27,7 @@ import java.util.Date;
 import java.util.Deque;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -42,7 +43,9 @@ import brooklyn.entity.basic.Entities;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.rebind.PersistenceExceptionHandlerImpl;
 import brooklyn.entity.rebind.RebindFeedTest.MyEntityWithFunctionFeedImpl;
+import brooklyn.entity.rebind.RebindFeedTest.MyEntityWithNewFeedsEachTimeImpl;
 import brooklyn.entity.rebind.RebindManagerImpl;
+import brooklyn.entity.rebind.RebindTestFixture;
 import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
 import brooklyn.entity.rebind.persister.InMemoryObjectStore;
 import brooklyn.entity.rebind.persister.ListeningObjectStore;
@@ -60,6 +63,7 @@ import brooklyn.test.entity.TestEntity;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.repeat.Repeater;
 import brooklyn.util.text.ByteSizeStrings;
 import brooklyn.util.time.Duration;
 import brooklyn.util.time.Time;
@@ -599,20 +603,58 @@ public class HotStandbyTest {
     }
     
     @Test
-    public void testHotStandbyDoesNoStartFeeds() throws Exception {
+    public void testHotStandbyDoesNotStartFeeds() throws Exception {
         HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
         TestApplication app = createFirstAppAndPersist(n1);
         TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class).impl(MyEntityWithFunctionFeedImpl.class));
         forcePersistNow(n1);
+        Assert.assertTrue(entity.feeds().getFeeds().size() > 0, "Feeds: "+entity.feeds().getFeeds());
         for (Feed feed : entity.feeds().getFeeds()) {
-            assertTrue(feed.isActive(), "Feed expected running, but it is non-running");
+            assertTrue(feed.isRunning(), "Feed expected running, but it is non-running");
         }
 
         HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
         TestEntity entityRO = (TestEntity) n2.mgmt.lookup(entity.getId(), Entity.class);
+        Assert.assertTrue(entityRO.feeds().getFeeds().size() > 0, "Feeds: "+entity.feeds().getFeeds());
         for (Feed feedRO : entityRO.feeds().getFeeds()) {
-            assertFalse(feedRO.isActive(), "Feed expected non-active, but it is running");
+            assertFalse(feedRO.isRunning(), "Feed expected non-active, but it is running");
         }
     }
+    
+    @Test(groups="Integration")
+    public void testHotStandbyDoesNotStartFeedsRebindingManyTimes() throws Exception {
+        testHotStandbyDoesNotStartFeeds();
+        final HaMgmtNode hsb = createHotStandby(Duration.millis(10));
+        Repeater.create("until 10 rebinds").every(Duration.millis(100)).until(
+            new Callable<Boolean>() {
+                @Override
+                public Boolean call() throws Exception {
+                    return ((RebindManagerImpl)hsb.mgmt.getRebindManager()).getReadOnlyRebindCount() >= 10;
+                }
+            }).runRequiringTrue();
+        // make sure not too many tasks (allowing 5 for rebind etc; currently just 2)
+        RebindTestFixture.waitForTaskCountToBecome(hsb.mgmt, 5);
+    }
+
+    @Test(groups="Integration")
+    public void testHotStandbyDoesNotStartFeedsRebindingManyTimesWithAnotherFeedGenerator() throws Exception {
+        HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
+        TestApplication app = createFirstAppAndPersist(n1);
+        TestEntity entity = app.createAndManageChild(EntitySpec.create(TestEntity.class).impl(MyEntityWithNewFeedsEachTimeImpl.class));
+        forcePersistNow(n1);
+        Assert.assertTrue(entity.feeds().getFeeds().size() == 4, "Feeds: "+entity.feeds().getFeeds());
+        
+        final HaMgmtNode hsb = createHotStandby(Duration.millis(10));
+        Repeater.create("until 10 rebinds").every(Duration.millis(100)).until(
+            new Callable<Boolean>() {
+                @Override
+                public Boolean call() throws Exception {
+                    return ((RebindManagerImpl)hsb.mgmt.getRebindManager()).getReadOnlyRebindCount() >= 10;
+                }
+            }).runRequiringTrue();
+        // make sure not too many tasks (allowing 5 for rebind etc; currently just 2)
+        RebindTestFixture.waitForTaskCountToBecome(hsb.mgmt, 5);
+    }
+
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ff0abc34/utils/common/src/main/java/brooklyn/util/guava/Functionals.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/guava/Functionals.java b/utils/common/src/main/java/brooklyn/util/guava/Functionals.java
index dcbd9da..08e41b1 100644
--- a/utils/common/src/main/java/brooklyn/util/guava/Functionals.java
+++ b/utils/common/src/main/java/brooklyn/util/guava/Functionals.java
@@ -90,6 +90,9 @@ public class Functionals {
             @Override public O apply(I input) {
                 return supplier.get();
             }
+            @Override public String toString() {
+                return "function("+supplier+")";
+            }
         }
         return new SupplierAsFunction();
     }
@@ -120,6 +123,10 @@ public class Functionals {
             public T call() {
                 return supplier.get();
             }
+            @Override
+            public String toString() {
+                return "callable("+supplier+")";
+            }
         }
         return new SupplierAsCallable();
     }
@@ -127,4 +134,18 @@ public class Functionals {
         return callable(Suppliers.compose(f, Suppliers.ofInstance(x)));
     }
 
+    public static <T> Predicate<T> predicate(final Function<T,Boolean> f) {
+        class FunctionAsPredicate implements Predicate<T> {
+            @Override
+            public boolean apply(T input) {
+                return f.apply(input);
+            }
+            @Override
+            public String toString() {
+                return "predicate("+f+")";
+            }
+        }
+        return new FunctionAsPredicate();
+    }
+
 }


[5/6] incubator-brooklyn git commit: some base64 / data uri scheme tidy up as per code review

Posted by he...@apache.org.
some base64 / data uri scheme tidy up as per code review


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

Branch: refs/heads/master
Commit: 53ea41021aaef10de3585c66512f440e6339d68e
Parents: ff0abc3
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Fri Nov 21 15:22:55 2014 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Fri Nov 21 15:26:18 2014 +0000

----------------------------------------------------------------------
 .../brooklyn/util/text/DataUriSchemeParser.java | 28 +++++++++++++++-----
 .../java/brooklyn/util/ResourceUtilsTest.java   |  7 ++++-
 .../src/main/java/brooklyn/util/net/Urls.java   | 21 ++++++++++-----
 3 files changed, 41 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/53ea4102/core/src/main/java/brooklyn/util/text/DataUriSchemeParser.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/text/DataUriSchemeParser.java b/core/src/main/java/brooklyn/util/text/DataUriSchemeParser.java
index 2ce339d..393ab20 100644
--- a/core/src/main/java/brooklyn/util/text/DataUriSchemeParser.java
+++ b/core/src/main/java/brooklyn/util/text/DataUriSchemeParser.java
@@ -26,12 +26,11 @@ import java.nio.charset.Charset;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
-import org.bouncycastle.util.encoders.Base64;
-
 import brooklyn.util.exceptions.Exceptions;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.io.BaseEncoding;
 //import com.sun.jersey.core.util.Base64;
 
 /** implementation (currently hokey) of RFC-2397 data: URI scheme.
@@ -47,6 +46,7 @@ public class DataUriSchemeParser {
     private boolean isParsed = false;
     private boolean allowMissingComma = false;
     private boolean allowSlashesAfterColon = false;
+    private boolean allowOtherLaxities = false;
     
     private String mimeType;
     private byte[] data;
@@ -100,7 +100,7 @@ public class DataUriSchemeParser {
     // ---- config ------------------
     
     public synchronized DataUriSchemeParser lax() {
-        return allowMissingComma(true).allowSlashesAfterColon(true);
+        return allowMissingComma(true).allowSlashesAfterColon(true).allowOtherLaxities(true);
     }
         
     public synchronized DataUriSchemeParser allowMissingComma(boolean allowMissingComma) {
@@ -115,6 +115,12 @@ public class DataUriSchemeParser {
         return this;
     }
     
+    private synchronized DataUriSchemeParser allowOtherLaxities(boolean allowOtherLaxities) {
+        assertNotParsed();
+        this.allowOtherLaxities = allowOtherLaxities;
+        return this;
+    }
+    
     private void assertNotParsed() {
         if (isParsed) throw new IllegalStateException("Operation not permitted after parsing");
     }
@@ -223,15 +229,23 @@ public class DataUriSchemeParser {
 
     private void parseData() throws UnsupportedEncodingException, MalformedURLException {
         if (parameters.containsKey("base64")) {
-            String base64value = parameters.get("base64");
-            if (base64value!=null)
-                throw new MalformedURLException("base64 parameter must not take a value ("+base64value+") in data: URL");
-            data = Base64.decode(remainder());
+            checkNoParamValue("base64");
+            data = BaseEncoding.base64().decode(remainder());
+        } else if (parameters.containsKey("base64url")) {
+            checkNoParamValue("base64url");
+            data = BaseEncoding.base64Url().decode(remainder());
         } else {
             data = URLDecoder.decode(remainder(), getCharset()).getBytes(Charset.forName(getCharset()));
         }
     }
 
+    private void checkNoParamValue(String param) throws MalformedURLException {
+        if (allowOtherLaxities) return; 
+        String value = parameters.get(param);
+        if (value!=null)
+            throw new MalformedURLException(param+" parameter must not take a value ("+value+") in data: URL");
+    }
+
     private String remainder() {
         return url.substring(parseIndex);
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/53ea4102/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/util/ResourceUtilsTest.java b/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
index 0d4ddc1..a588513 100644
--- a/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
+++ b/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
@@ -39,6 +39,7 @@ import org.testng.annotations.Test;
 import brooklyn.util.net.Urls;
 import brooklyn.util.os.Os;
 import brooklyn.util.stream.Streams;
+import brooklyn.util.text.Identifiers;
 
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
@@ -145,7 +146,7 @@ public class ResourceUtilsTest {
     public void testClassLoaderDirNotFound() throws Exception {
         String d = utils.getClassLoaderDir("/somewhere/not/found/XXX.xxx");
         // above should fail
-        log.warn("Uh oh found iamginary resource in: "+d);
+        log.warn("Uh oh found imaginary resource in: "+d);
     }
 
     @Test(groups="Integration")
@@ -170,6 +171,10 @@ public class ResourceUtilsTest {
         assertEquals(utils.getResourceAsString("data://hello"), "hello");
         assertEquals(utils.getResourceAsString("data:hello world"), "hello world");
         assertEquals(utils.getResourceAsString(Urls.asDataUrlBase64("hello world")), "hello world");
+        
+        String longString = Identifiers.makeRandomId(256);
+        for (int a=32; a<128; a++) longString += (char)a;
+        assertEquals(utils.getResourceAsString(Urls.asDataUrlBase64(longString)), longString);
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/53ea4102/utils/common/src/main/java/brooklyn/util/net/Urls.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/net/Urls.java b/utils/common/src/main/java/brooklyn/util/net/Urls.java
index 63a60f9..917b58a 100644
--- a/utils/common/src/main/java/brooklyn/util/net/Urls.java
+++ b/utils/common/src/main/java/brooklyn/util/net/Urls.java
@@ -28,12 +28,11 @@ import java.net.URLEncoder;
 
 import javax.annotation.Nullable;
 
-import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
-
 import brooklyn.util.text.Strings;
 
 import com.google.common.base.Function;
 import com.google.common.base.Throwables;
+import com.google.common.io.BaseEncoding;
 import com.google.common.net.MediaType;
 
 public class Urls {
@@ -217,12 +216,20 @@ public class Urls {
     }
     
     /** 
-     * Creates a "data:..." scheme URL for use with supported parsers.
-     * (But note, by default Java's URL is not one of them.)
-     * It is not necessary to base64 encode it, but good practise;
-     * null type means no type included. */
+     * Creates a "data:..." scheme URL for use with supported parsers, using Base64 encoding.
+     * (But note, by default Java's URL is not one of them, although Brooklyn's ResourceUtils does support it.)
+     * <p>
+     * It is not necessary (at least for Brookyn's routines) to base64 encode it, but recommended as that is likely more
+     * portable and easier to work with if odd characters are included.
+     * <p>
+     * It is worth noting that Base64 uses '+' which can be replaced by ' ' in some URL parsing.  
+     * But in practice it does not seem to cause issues.
+     * An alternative is to use {@link BaseEncoding#base64Url()} but it is not clear how widely that is supported
+     * (nor what parameter should be given to indicate that type of encoding, as the spec calls for 'base64'!)
+     * <p>
+     * null type means no type info will be included in the URL. */
     public static String asDataUrlBase64(MediaType type, byte[] bytes) {
-        return "data:"+(type!=null ? type.withoutParameters().toString() : "")+";base64,"+new String(Base64Coder.encode(bytes));
+        return "data:"+(type!=null ? type.withoutParameters().toString() : "")+";base64,"+new String(BaseEncoding.base64().encode(bytes));
     }
 
 }


[6/6] incubator-brooklyn git commit: This closes #352

Posted by he...@apache.org.
This closes #352


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

Branch: refs/heads/master
Commit: a5db1546b9efd962c988c7e3f80068c74d810e99
Parents: 0f50c58 53ea410
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Nov 26 02:07:15 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Nov 26 02:07:15 2014 +0100

----------------------------------------------------------------------
 api/src/main/java/brooklyn/entity/Feed.java     |   5 +-
 .../main/java/brooklyn/mementos/Memento.java    |   6 +
 .../java/brooklyn/policy/EntityAdjunct.java     |   2 +-
 .../brooklyn/basic/AbstractBrooklynObject.java  |  15 +-
 .../brooklyn/entity/basic/AbstractEntity.java   |  50 ++++--
 .../java/brooklyn/entity/basic/Entities.java    |   3 +-
 .../AbstractBrooklynObjectRebindSupport.java    |   6 +
 .../entity/rebind/RebindManagerImpl.java        |  11 +-
 .../entity/rebind/dto/AbstractMemento.java      |  61 ++++---
 .../entity/rebind/dto/BasicLocationMemento.java |   1 -
 .../entity/rebind/dto/MementosGenerators.java   |   8 +-
 .../java/brooklyn/event/feed/AbstractFeed.java  |  76 +++++---
 .../java/brooklyn/event/feed/FeedConfig.java    |  88 +++++++++
 .../java/brooklyn/event/feed/PollConfig.java    |  16 +-
 .../main/java/brooklyn/event/feed/Poller.java   |  26 ++-
 .../event/feed/function/FunctionFeed.java       |  13 +-
 .../event/feed/function/FunctionPollConfig.java |  21 +++
 .../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 +-
 .../internal/BrooklynGarbageCollector.java      |  13 +-
 .../policy/basic/AbstractEntityAdjunct.java     |   8 +-
 .../java/brooklyn/policy/basic/AdjunctType.java |  10 +-
 .../brooklyn/util/text/DataUriSchemeParser.java |  28 ++-
 .../brooklyn/util/text/TemplateProcessor.java   |   2 +-
 .../brooklyn/entity/rebind/RebindFeedTest.java  | 178 +++++++++++++++++--
 .../entity/rebind/RebindTestFixture.java        |  40 ++++-
 .../event/feed/function/FunctionFeedTest.java   |  34 +++-
 .../brooklyn/event/feed/http/HttpFeedTest.java  |  15 ++
 .../feed/shell/ShellFeedIntegrationTest.java    |  21 +++
 .../event/feed/ssh/SshFeedIntegrationTest.java  |  16 ++
 .../brooklyn/management/ha/HotStandbyTest.java  |  48 ++++-
 .../java/brooklyn/util/ResourceUtilsTest.java   |  10 +-
 .../location/jclouds/JcloudsLocation.java       |   2 +-
 .../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 +
 .../java/brooklyn/util/guava/Functionals.java   |  21 +++
 .../main/java/brooklyn/util/guava/Maybe.java    |  16 ++
 .../java/brooklyn/util/javalang/Equals.java     |  35 ++++
 .../src/main/java/brooklyn/util/net/Urls.java   |  24 +++
 .../main/java/brooklyn/util/text/Strings.java   |  22 +++
 .../test/java/brooklyn/util/net/UrlsTest.java   |  10 +-
 53 files changed, 934 insertions(+), 145 deletions(-)
----------------------------------------------------------------------



[2/6] incubator-brooklyn git commit: utils for encoding base64 data strings

Posted by he...@apache.org.
utils for encoding base64 data strings


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

Branch: refs/heads/master
Commit: ae6924543241897943c8d998d57fc3cb6d964442
Parents: 068de70
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Nov 19 15:40:01 2014 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Thu Nov 20 12:47:05 2014 +0000

----------------------------------------------------------------------
 .../test/java/brooklyn/util/ResourceUtilsTest.java |  3 +++
 .../src/main/java/brooklyn/util/net/Urls.java      | 17 +++++++++++++++++
 .../src/test/java/brooklyn/util/net/UrlsTest.java  | 10 +++++++++-
 3 files changed, 29 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ae692454/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/util/ResourceUtilsTest.java b/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
index 56c51cc..0d4ddc1 100644
--- a/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
+++ b/core/src/test/java/brooklyn/util/ResourceUtilsTest.java
@@ -36,6 +36,7 @@ import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import brooklyn.util.net.Urls;
 import brooklyn.util.os.Os;
 import brooklyn.util.stream.Streams;
 
@@ -168,5 +169,7 @@ public class ResourceUtilsTest {
         assertEquals(utils.getResourceAsString("data:hello"), "hello");
         assertEquals(utils.getResourceAsString("data://hello"), "hello");
         assertEquals(utils.getResourceAsString("data:hello world"), "hello world");
+        assertEquals(utils.getResourceAsString(Urls.asDataUrlBase64("hello world")), "hello world");
     }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ae692454/utils/common/src/main/java/brooklyn/util/net/Urls.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/net/Urls.java b/utils/common/src/main/java/brooklyn/util/net/Urls.java
index 927051a..63a60f9 100644
--- a/utils/common/src/main/java/brooklyn/util/net/Urls.java
+++ b/utils/common/src/main/java/brooklyn/util/net/Urls.java
@@ -28,10 +28,13 @@ import java.net.URLEncoder;
 
 import javax.annotation.Nullable;
 
+import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
+
 import brooklyn.util.text.Strings;
 
 import com.google.common.base.Function;
 import com.google.common.base.Throwables;
+import com.google.common.net.MediaType;
 
 public class Urls {
 
@@ -208,4 +211,18 @@ public class Urls {
         }
     }
 
+    /** as {@link #asDataUrlBase64(String)} with plain text */
+    public static String asDataUrlBase64(String data) {
+        return asDataUrlBase64(MediaType.PLAIN_TEXT_UTF_8, data.getBytes());
+    }
+    
+    /** 
+     * Creates a "data:..." scheme URL for use with supported parsers.
+     * (But note, by default Java's URL is not one of them.)
+     * It is not necessary to base64 encode it, but good practise;
+     * null type means no type included. */
+    public static String asDataUrlBase64(MediaType type, byte[] bytes) {
+        return "data:"+(type!=null ? type.withoutParameters().toString() : "")+";base64,"+new String(Base64Coder.encode(bytes));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ae692454/utils/common/src/test/java/brooklyn/util/net/UrlsTest.java
----------------------------------------------------------------------
diff --git a/utils/common/src/test/java/brooklyn/util/net/UrlsTest.java b/utils/common/src/test/java/brooklyn/util/net/UrlsTest.java
index d0a615c..d27c3b1 100644
--- a/utils/common/src/test/java/brooklyn/util/net/UrlsTest.java
+++ b/utils/common/src/test/java/brooklyn/util/net/UrlsTest.java
@@ -66,5 +66,13 @@ public class UrlsTest {
         assertEquals(Urls.getBasename(""), "");
         assertEquals(Urls.getBasename(null), null);
     }
-    
+
+    @Test
+    public void testDataUrl() throws Exception {
+        String input = "hello world";
+        String url = Urls.asDataUrlBase64(input);
+        Assert.assertEquals(url, "data:text/plain;base64,aGVsbG8gd29ybGQ=");
+        // tests for parsing are in core in ResourceUtilsTest
+    }
+
 }


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

Posted by he...@apache.org.
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());
+    }
+
 }