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/15 01:05:13 UTC

[04/21] incubator-brooklyn git commit: Use a linear range QuorumCheck to fail if there are too many dangling references.

Use a linear range QuorumCheck to fail if there are too many dangling references.

This introduces QuorumChecks.newLinearRange which allows specifying points in a place
to define a set of line segments defining the quorum.
This can be TypeCoerced from a string, e.g. `[[5,5],[10,10],[100,70],[200,140]]`
(with things like `all` and `atLeastOne` also supported).

Such a range is then used to cause rebind to fail if there are a lot of dangling references.
The precise number is allowing 2 for small values, scaling to 5% at big values,
but this can be tweaked using RebindManagerImpl.DANGLING_REFERENCES_MIN_REQUIRED_HEALTHY
aka rebind.failureMode.danglingRefs.minRequiredHealthy .  (e.g. set this to all in
brooklyn.properties to cause rebind to fail if there are any dangling references;
but note this can occasionally fail if something is deleted as different items are
not persisted simultaneously).

Also:
* There are also some tidies to how `RebindContext` is used, since many of the
  dangling references weren't being caught
* We are stricter about persisting proxies, since that doesn't work (type written as `$Proxy16`)
* We tolerate remote timestamps in persisted objects (since these can occur in backups, currently);
  we don't *use* those timestamps for anything (except in tests when they can be `prefer`red),
  but there is no harm in having them, if someone copied across backup persistence files


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

Branch: refs/heads/master
Commit: f39c6a3fc3f40cd3469c38b7d61908d317c1d7de
Parents: 7532be8
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Nov 12 09:02:42 2014 +0000
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Thu Nov 13 23:38:52 2014 +0000

----------------------------------------------------------------------
 .../brooklyn/entity/rebind/RebindContext.java   |  30 ++--
 .../entity/rebind/RebindExceptionHandler.java   |   2 +-
 .../brooklyn/config/BrooklynServerPaths.java    |   2 +-
 .../java/brooklyn/entity/basic/Entities.java    |   8 +-
 .../entity/rebind/BasicEntityRebindSupport.java |  15 +-
 .../rebind/BasicLocationRebindSupport.java      |   4 +-
 .../rebind/PersistenceExceptionHandlerImpl.java |   2 +-
 .../entity/rebind/RebindContextImpl.java        |  44 ++++--
 .../rebind/RebindContextLookupContext.java      |   6 +-
 .../rebind/RebindExceptionHandlerImpl.java      |  32 ++++-
 .../entity/rebind/RebindManagerImpl.java        |  26 +++-
 .../entity/rebind/dto/MementosGenerators.java   |   5 +
 .../persister/BrooklynPersistenceUtils.java     |   3 +
 .../ha/HighAvailabilityManagerImpl.java         |   2 +-
 ...ntPlaneSyncRecordPersisterToObjectStore.java |  13 +-
 .../java/brooklyn/util/flags/TypeCoercions.java |   8 ++
 .../HighAvailabilityManagerSplitBrainTest.java  |   2 +-
 .../ha/HighAvailabilityManagerTestFixture.java  |   2 +-
 .../brooklyn/management/ha/HotStandbyTest.java  |   2 +-
 .../brooklyn/management/ha/WarmStandbyTest.java |   2 +-
 .../brooklyn/util/collections/MutableList.java  |   7 +
 .../brooklyn/util/collections/QuorumCheck.java  | 142 ++++++++++++++++++-
 .../util/collections/QuorumChecksTest.java      | 105 ++++++++++++++
 23 files changed, 392 insertions(+), 72 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/api/src/main/java/brooklyn/entity/rebind/RebindContext.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindContext.java b/api/src/main/java/brooklyn/entity/rebind/RebindContext.java
index 1ddd178..17935b3 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindContext.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindContext.java
@@ -18,41 +18,35 @@
  */
 package brooklyn.entity.rebind;
 
+import java.util.Map;
+
 import brooklyn.basic.BrooklynObject;
-import brooklyn.catalog.CatalogItem;
-import brooklyn.entity.Entity;
-import brooklyn.entity.Feed;
-import brooklyn.location.Location;
-import brooklyn.policy.Enricher;
-import brooklyn.policy.Policy;
+import brooklyn.mementos.BrooklynMementoPersister.LookupContext;
+
+import com.google.common.annotations.Beta;
 
 /**
  * Gives access to things that are being currently rebinding. This is used during a
  * rebind to wire everything back together again, e.g. to find the necessary entity 
  * instances even before they are available through 
  * {@code managementContext.getEntityManager().getEnties()}.
- * 
+ * <p>
  * Users are not expected to implement this class. It is for use by {@link Rebindable} 
  * instances, and will generally be created by the {@link RebindManager}.
+ * <p>
  */
+@Beta
 public interface RebindContext {
 
-    Entity getEntity(String id);
-
-    Location getLocation(String id);
-
-    Policy getPolicy(String id);
-
-    Enricher getEnricher(String id);
-
-    Feed getFeed(String id);
+    /** Returns an unmodifiable view of all objects by ID */ 
+    Map<String,BrooklynObject> getAllBrooklynObjects();
     
-    CatalogItem<?, ?> getCatalogItem(String id);
-
     Class<?> loadClass(String typeName) throws ClassNotFoundException;
     
     RebindExceptionHandler getExceptionHandler();
     
     boolean isReadOnly(BrooklynObject item);
     
+    LookupContext lookup();
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/api/src/main/java/brooklyn/entity/rebind/RebindExceptionHandler.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindExceptionHandler.java b/api/src/main/java/brooklyn/entity/rebind/RebindExceptionHandler.java
index 34c4f05..61bcdaa 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindExceptionHandler.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindExceptionHandler.java
@@ -96,7 +96,7 @@ public interface RebindExceptionHandler {
     RuntimeException onFailed(Exception e);
 
     /** invoked before the rebind pass */
-    void onStart();
+    void onStart(RebindContext context);
     
     /** invoked after the complete rebind pass, always on success and possibly on failure */
     void onDone();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/config/BrooklynServerPaths.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/config/BrooklynServerPaths.java b/core/src/main/java/brooklyn/config/BrooklynServerPaths.java
index 2bcb763..ee294e9 100644
--- a/core/src/main/java/brooklyn/config/BrooklynServerPaths.java
+++ b/core/src/main/java/brooklyn/config/BrooklynServerPaths.java
@@ -205,7 +205,7 @@ public class BrooklynServerPaths {
         }
         @Override
         protected String getDefaultDirForAnyFilesystem() {
-            return backupContainerFor(super.getDefaultDirForAnyFilesystem());
+            return backupContainerFor(DEFAULT_PERSISTENCE_CONTAINER_NAME);
         }
         @Override
         protected String getDefaultContainerForAnyNonFilesystem() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/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 7d17b7a..d073372 100644
--- a/core/src/main/java/brooklyn/entity/basic/Entities.java
+++ b/core/src/main/java/brooklyn/entity/basic/Entities.java
@@ -773,10 +773,14 @@ public class Entities {
 
     /** Unwraps a proxy to retrieve the real item, if available.
      * <p>
-     * Only intended for use in tests. For normal operations, callers should ensure the method is
-     * available on an interface and accessed via the proxy. */
+     * Only intended for use in tests and occasional internal usage, e.g. persistence.
+     * For normal operations, callers should ensure the method is available on an interface and accessed via the proxy. */
     @Beta @VisibleForTesting
     public static AbstractEntity deproxy(Entity e) {
+        if (!(Proxy.isProxyClass(e.getClass()))) {
+            log.warn("Attempt to deproxy non-proxy "+e, new Throwable("Location of attempt to deproxy non-proxy "+e));
+            return (AbstractEntity) e;
+        }
         return (AbstractEntity) ((EntityProxyImpl)Proxy.getInvocationHandler(e)).getDelegate();
     }
     

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/entity/rebind/BasicEntityRebindSupport.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/BasicEntityRebindSupport.java b/core/src/main/java/brooklyn/entity/rebind/BasicEntityRebindSupport.java
index bb31a1f..6bb19cb 100644
--- a/core/src/main/java/brooklyn/entity/rebind/BasicEntityRebindSupport.java
+++ b/core/src/main/java/brooklyn/entity/rebind/BasicEntityRebindSupport.java
@@ -118,7 +118,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
     @Override
     public void addPolicies(RebindContext rebindContext, EntityMemento memento) {
         for (String policyId : memento.getPolicies()) {
-            AbstractPolicy policy = (AbstractPolicy) rebindContext.getPolicy(policyId);
+            AbstractPolicy policy = (AbstractPolicy) rebindContext.lookup().lookupPolicy(policyId);
             if (policy != null) {
                 try {
                     entity.addPolicy(policy);
@@ -128,6 +128,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
             } else {
                 LOG.warn("Policy not found; discarding policy {} of entity {}({})",
                         new Object[] {policyId, memento.getType(), memento.getId()});
+                rebindContext.getExceptionHandler().onDanglingPolicyRef(policyId);
             }
         }
     }
@@ -135,7 +136,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
     @Override
     public void addEnrichers(RebindContext rebindContext, EntityMemento memento) {
         for (String enricherId : memento.getEnrichers()) {
-            AbstractEnricher enricher = (AbstractEnricher) rebindContext.getEnricher(enricherId);
+            AbstractEnricher enricher = (AbstractEnricher) rebindContext.lookup().lookupEnricher(enricherId);
             if (enricher != null) {
                 try {
                     entity.addEnricher(enricher);
@@ -152,7 +153,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
     @Override
     public void addFeeds(RebindContext rebindContext, EntityMemento memento) {
         for (String feedId : memento.getFeeds()) {
-            AbstractFeed feed = (AbstractFeed) rebindContext.getFeed(feedId);
+            AbstractFeed feed = (AbstractFeed) rebindContext.lookup().lookupFeed(feedId);
             if (feed != null) {
                 try {
                     ((EntityInternal)entity).feeds().addFeed(feed);
@@ -178,7 +179,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
         if (memento.getMembers().size() > 0) {
             if (entity instanceof AbstractGroupImpl) {
                 for (String memberId : memento.getMembers()) {
-                    Entity member = rebindContext.getEntity(memberId);
+                    Entity member = rebindContext.lookup().lookupEntity(memberId);
                     if (member != null) {
                         ((AbstractGroupImpl)entity).addMemberInternal(member);
                     } else {
@@ -198,7 +199,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
     
     protected void addChildren(RebindContext rebindContext, EntityMemento memento) {
         for (String childId : memento.getChildren()) {
-            Entity child = rebindContext.getEntity(childId);
+            Entity child = rebindContext.lookup().lookupEntity(childId);
             if (child != null) {
                 entity.addChild(proxy(child));
             } else {
@@ -209,7 +210,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
     }
 
     protected void setParent(RebindContext rebindContext, EntityMemento memento) {
-        Entity parent = (memento.getParent() != null) ? rebindContext.getEntity(memento.getParent()) : null;
+        Entity parent = (memento.getParent() != null) ? rebindContext.lookup().lookupEntity(memento.getParent()) : null;
         if (parent != null) {
             entity.setParent(proxy(parent));
         } else if (memento.getParent() != null){
@@ -220,7 +221,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
     
     protected void addLocations(RebindContext rebindContext, EntityMemento memento) {
         for (String id : memento.getLocations()) {
-            Location loc = rebindContext.getLocation(id);
+            Location loc = rebindContext.lookup().lookupLocation(id);
             if (loc != null) {
                 ((EntityInternal)entity).addLocations(ImmutableList.of(loc));
             } else {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/entity/rebind/BasicLocationRebindSupport.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/BasicLocationRebindSupport.java b/core/src/main/java/brooklyn/entity/rebind/BasicLocationRebindSupport.java
index 2ca5d2a..fc7947b 100644
--- a/core/src/main/java/brooklyn/entity/rebind/BasicLocationRebindSupport.java
+++ b/core/src/main/java/brooklyn/entity/rebind/BasicLocationRebindSupport.java
@@ -116,7 +116,7 @@ public class BasicLocationRebindSupport extends AbstractBrooklynObjectRebindSupp
 
     protected void addChildren(RebindContext rebindContext, LocationMemento memento) {
         for (String childId : memento.getChildren()) {
-            Location child = rebindContext.getLocation(childId);
+            Location child = rebindContext.lookup().lookupLocation(childId);
             if (child != null) {
                 location.addChild(child);
             } else {
@@ -126,7 +126,7 @@ public class BasicLocationRebindSupport extends AbstractBrooklynObjectRebindSupp
     }
 
     protected void setParent(RebindContext rebindContext, LocationMemento memento) {
-        Location parent = (memento.getParent() != null) ? rebindContext.getLocation(memento.getParent()) : null;
+        Location parent = (memento.getParent() != null) ? rebindContext.lookup().lookupLocation(memento.getParent()) : null;
         if (parent != null) {
             location.setParent(parent);
         } else if (memento.getParent() != null) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/entity/rebind/PersistenceExceptionHandlerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/PersistenceExceptionHandlerImpl.java b/core/src/main/java/brooklyn/entity/rebind/PersistenceExceptionHandlerImpl.java
index bce5c83..a3f7655 100644
--- a/core/src/main/java/brooklyn/entity/rebind/PersistenceExceptionHandlerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/PersistenceExceptionHandlerImpl.java
@@ -90,7 +90,7 @@ public class PersistenceExceptionHandlerImpl implements PersistenceExceptionHand
             if (!isNew) {
                 if (LOG.isDebugEnabled()) LOG.debug("Repeating problem: "+errmsg, e);
             } else {
-                LOG.warn("Problem: "+errmsg, e);
+                LOG.warn("Problem persisting (ignoring): "+errmsg, e);
             }
         } else {
             if (!isNew) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/entity/rebind/RebindContextImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/RebindContextImpl.java b/core/src/main/java/brooklyn/entity/rebind/RebindContextImpl.java
index bcac4a7..a581c7d 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindContextImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindContextImpl.java
@@ -28,8 +28,10 @@ import brooklyn.catalog.CatalogItem;
 import brooklyn.entity.Entity;
 import brooklyn.entity.Feed;
 import brooklyn.location.Location;
+import brooklyn.mementos.BrooklynMementoPersister.LookupContext;
 import brooklyn.policy.Enricher;
 import brooklyn.policy.Policy;
+import brooklyn.util.collections.MutableMap;
 
 import com.google.common.collect.Maps;
 
@@ -44,6 +46,7 @@ public class RebindContextImpl implements RebindContext {
     
     private final ClassLoader classLoader;
     private final RebindExceptionHandler exceptionHandler;
+    private LookupContext lookupContext;
     
     private boolean allAreReadOnly = false;
     
@@ -92,69 +95,73 @@ public class RebindContextImpl implements RebindContext {
         catalogItems.remove(item.getId());
     }
 
-    @Override
     public Entity getEntity(String id) {
         return entities.get(id);
     }
 
-    @Override
     public Location getLocation(String id) {
         return locations.get(id);
     }
     
-    @Override
     public Policy getPolicy(String id) {
         return policies.get(id);
     }
     
-    @Override
     public Enricher getEnricher(String id) {
         return enrichers.get(id);
     }
 
-    @Override
     public CatalogItem<?, ?> getCatalogItem(String id) {
         return catalogItems.get(id);
     }
 
-    @Override
     public Feed getFeed(String id) {
         return feeds.get(id);
     }
     
-    @Override
     public Class<?> loadClass(String className) throws ClassNotFoundException {
         return classLoader.loadClass(className);
     }
 
-    @Override
     public RebindExceptionHandler getExceptionHandler() {
         return exceptionHandler;
     }
 
-    protected Collection<Location> getLocations() {
+    public Collection<Location> getLocations() {
         return locations.values();
     }
     
-    protected Collection<Entity> getEntities() {
+    public Collection<Entity> getEntities() {
         return entities.values();
     }
     
-    protected Collection<Policy> getPolicies() {
+    public Collection<Policy> getPolicies() {
         return policies.values();
     }
 
-    protected Collection<Enricher> getEnrichers() {
+    public Collection<Enricher> getEnrichers() {
         return enrichers.values();
     }
     
-    protected Collection<Feed> getFeeds() {
+    public Collection<Feed> getFeeds() {
         return feeds.values();
     }
 
-    protected Collection<CatalogItem<?, ?>> getCatalogItems() {
+    public Collection<CatalogItem<?, ?>> getCatalogItems() {
         return catalogItems.values();
     }
+    
+    @Override
+    public Map<String,BrooklynObject> getAllBrooklynObjects() {
+        MutableMap<String,BrooklynObject> result = MutableMap.of();
+        result.putAll(locations);
+        result.putAll(entities);
+        result.putAll(policies);
+        result.putAll(enrichers);
+        result.putAll(feeds);
+        result.putAll(catalogItems);
+        return result.asUnmodifiable();
+    }
 
     public void setAllReadOnly() {
         allAreReadOnly = true;
@@ -163,5 +170,14 @@ public class RebindContextImpl implements RebindContext {
     public boolean isReadOnly(BrooklynObject item) {
         return allAreReadOnly;
     }
+
+    public void setLookupContext(LookupContext lookupContext) {
+        this.lookupContext = lookupContext;
+    }
+    
+    @Override
+    public LookupContext lookup() {
+        return lookupContext;
+    }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/entity/rebind/RebindContextLookupContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/RebindContextLookupContext.java b/core/src/main/java/brooklyn/entity/rebind/RebindContextLookupContext.java
index bf80ef9..6392a5f 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindContextLookupContext.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindContextLookupContext.java
@@ -41,13 +41,13 @@ public class RebindContextLookupContext implements LookupContext {
     @Nullable
     protected final ManagementContext managementContext;
     
-    protected final RebindContext rebindContext;
+    protected final RebindContextImpl rebindContext;
     protected final RebindExceptionHandler exceptionHandler;
     
-    public RebindContextLookupContext(RebindContext rebindContext, RebindExceptionHandler exceptionHandler) {
+    public RebindContextLookupContext(RebindContextImpl rebindContext, RebindExceptionHandler exceptionHandler) {
         this(null, rebindContext, exceptionHandler);
     }
-    public RebindContextLookupContext(ManagementContext managementContext, RebindContext rebindContext, RebindExceptionHandler exceptionHandler) {
+    public RebindContextLookupContext(ManagementContext managementContext, RebindContextImpl rebindContext, RebindExceptionHandler exceptionHandler) {
         this.managementContext = managementContext;
         this.rebindContext = rebindContext;
         this.exceptionHandler = exceptionHandler;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java b/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
index e5f3111..ac41c8b 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
@@ -36,6 +36,8 @@ import brooklyn.entity.rebind.RebindManager.RebindFailureMode;
 import brooklyn.location.Location;
 import brooklyn.policy.Enricher;
 import brooklyn.policy.Policy;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.QuorumCheck;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.text.Strings;
 
@@ -51,6 +53,7 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
     protected final RebindManager.RebindFailureMode rebindFailureMode;
     protected final RebindFailureMode addPolicyFailureMode;
     protected final RebindFailureMode loadPolicyFailureMode;
+    protected final QuorumCheck danglingRefsQuorumRequiredHealthy;
 
     protected final Set<String> missingEntities = Sets.newConcurrentHashSet();
     protected final Set<String> missingLocations = Sets.newConcurrentHashSet();
@@ -63,6 +66,7 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
     protected final Set<Exception> loadPolicyFailures = Sets.newConcurrentHashSet();
     protected final List<Exception> exceptions = Collections.synchronizedList(Lists.<Exception>newArrayList());
     
+    protected RebindContext context;
     protected boolean started = false;
     protected boolean done = false;
     
@@ -75,6 +79,7 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
         private RebindManager.RebindFailureMode rebindFailureMode = RebindManager.RebindFailureMode.FAIL_AT_END;
         private RebindManager.RebindFailureMode addPolicyFailureMode = RebindManager.RebindFailureMode.CONTINUE;
         private RebindManager.RebindFailureMode deserializePolicyFailureMode = RebindManager.RebindFailureMode.CONTINUE;
+        private QuorumCheck danglingRefsQuorumRequiredHealthy = RebindManagerImpl.DANGLING_REFERENCES_MIN_REQUIRED_HEALTHY.getDefaultValue();
 
         public Builder danglingRefFailureMode(RebindManager.RebindFailureMode val) {
             danglingRefFailureMode = val;
@@ -92,6 +97,10 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
             deserializePolicyFailureMode = val;
             return this;
         }
+        public Builder danglingRefQuorumRequiredHealthy(QuorumCheck val) {
+            danglingRefsQuorumRequiredHealthy = val;
+            return this;
+        }
         public RebindExceptionHandler build() {
             return new RebindExceptionHandlerImpl(this);
         }
@@ -102,15 +111,18 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
         this.rebindFailureMode = checkNotNull(builder.rebindFailureMode, "rebindFailureMode");
         this.addPolicyFailureMode = checkNotNull(builder.addPolicyFailureMode, "addPolicyFailureMode");
         this.loadPolicyFailureMode = checkNotNull(builder.deserializePolicyFailureMode, "deserializePolicyFailureMode");
+        this.danglingRefsQuorumRequiredHealthy = checkNotNull(builder.danglingRefsQuorumRequiredHealthy, "danglingRefsQuorumRequiredHealthy");
     }
 
-    public void onStart() {
+    @Override
+    public void onStart(RebindContext context) {
         if (done) {
             throw new IllegalStateException(this+" has already been used on a finished run");
         }
         if (started) {
             throw new IllegalStateException(this+" has already been used on a started run");
         }
+        this.context = context;
         started = true;
     }
     
@@ -364,6 +376,24 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
             allExceptions.add(new IllegalStateException(this+" has already been informed of rebind done"));
         }
         done = true;
+        
+        List<String> danglingIds = MutableList.copyOf(missingEntities).appendAll(missingLocations).appendAll(missingPolicies).appendAll(missingEnrichers).appendAll(missingFeeds).appendAll(missingCatalogItems);
+        int totalDangling = danglingIds.size();
+        if (totalDangling>0) {
+            int totalFound = context.getAllBrooklynObjects().size();
+            int totalItems = totalFound + totalDangling;
+            if (context==null) {
+                allExceptions.add(new IllegalStateException("Dangling references ("+totalDangling+" of "+totalItems+") present without rebind context"));
+            } else {
+                if (!danglingRefsQuorumRequiredHealthy.isQuorate(totalFound, totalItems)) {
+                    LOG.warn("Dangling item"+Strings.s(totalDangling)+" ("+totalDangling+" of "+totalItems+") found on rebind exceeds quorum, assuming failed: "+danglingIds);
+                    allExceptions.add(new IllegalStateException("Too many dangling references: "+totalDangling+" of "+totalItems));
+                } else {
+                    LOG.info("Dangling item"+Strings.s(totalDangling)+" ("+totalDangling+" of "+totalItems+") found on rebind, assuming deleted: "+danglingIds);
+                }
+            }
+        }
+        
         if (e != null) {
             allExceptions.add(e);
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/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 3ba8bf1..ac9f726 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -91,6 +91,8 @@ import brooklyn.policy.Enricher;
 import brooklyn.policy.Policy;
 import brooklyn.policy.basic.AbstractPolicy;
 import brooklyn.util.collections.MutableMap;
+import brooklyn.util.collections.QuorumCheck;
+import brooklyn.util.collections.QuorumCheck.QuorumChecks;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.exceptions.RuntimeInterruptedException;
 import brooklyn.util.flags.FlagUtils;
@@ -136,7 +138,15 @@ public class RebindManagerImpl implements RebindManager {
     public static final ConfigKey<RebindFailureMode> LOAD_POLICY_FAILURE_MODE =
             ConfigKeys.newConfigKey(RebindFailureMode.class, "rebind.failureMode.loadPolicy",
                     "Action to take if a failure occurs when loading a policy or enricher", RebindFailureMode.CONTINUE);
-    
+
+    public static final ConfigKey<QuorumCheck> DANGLING_REFERENCES_MIN_REQUIRED_HEALTHY =
+        ConfigKeys.newConfigKey(QuorumCheck.class, "rebind.failureMode.danglingRefs.minRequiredHealthy",
+                "Number of items which must be rebinded at various sizes; "
+                + "a small number of dangling references is possible if items are in the process of being created or deleted, "
+                + "and that should be resolved on retry; the default set here allows max 2 dangling up to 10 items, "
+                + "then linear regression to allow max 5% at 100 items and above", 
+                QuorumChecks.newLinearRange("[[0,-2],[10,8],[100,95],[200,190]]"));
+
     public static final Logger LOG = LoggerFactory.getLogger(RebindManagerImpl.class);
 
     private final ManagementContextInternal managementContext;
@@ -163,6 +173,7 @@ public class RebindManagerImpl implements RebindManager {
     private RebindFailureMode rebindFailureMode;
     private RebindFailureMode addPolicyFailureMode;
     private RebindFailureMode loadPolicyFailureMode;
+    private QuorumCheck danglingRefsQuorumRequiredHealthy;
 
     /**
      * For tracking if rebinding, for {@link AbstractEnricher#isRebinding()} etc.
@@ -201,6 +212,8 @@ public class RebindManagerImpl implements RebindManager {
         rebindFailureMode = managementContext.getConfig().getConfig(REBIND_FAILURE_MODE);
         addPolicyFailureMode = managementContext.getConfig().getConfig(ADD_POLICY_FAILURE_MODE);
         loadPolicyFailureMode = managementContext.getConfig().getConfig(LOAD_POLICY_FAILURE_MODE);
+        
+        danglingRefsQuorumRequiredHealthy = managementContext.getConfig().getConfig(DANGLING_REFERENCES_MIN_REQUIRED_HEALTHY);
 
         LOG.debug("{} initialized, settings: policies={}, enrichers={}, feeds={}, catalog={}",
                 new Object[]{this, persistPoliciesEnabled, persistEnrichersEnabled, persistFeedsEnabled, persistCatalogItemsEnabled});
@@ -446,6 +459,7 @@ public class RebindManagerImpl implements RebindManager {
         final RebindExceptionHandler exceptionHandler = exceptionHandlerO!=null ? exceptionHandlerO :
             RebindExceptionHandlerImpl.builder()
                 .danglingRefFailureMode(danglingRefFailureMode)
+                .danglingRefQuorumRequiredHealthy(danglingRefsQuorumRequiredHealthy)
                 .rebindFailureMode(rebindFailureMode)
                 .addPolicyFailureMode(addPolicyFailureMode)
                 .loadPolicyFailureMode(loadPolicyFailureMode)
@@ -514,10 +528,11 @@ public class RebindManagerImpl implements RebindManager {
         RebindTracker.setRebinding();
         try {
             Stopwatch timer = Stopwatch.createStarted();
-            exceptionHandler.onStart();
-            
             Reflections reflections = new Reflections(classLoader);
             RebindContextImpl rebindContext = new RebindContextImpl(exceptionHandler, classLoader);
+            
+            exceptionHandler.onStart(rebindContext);
+            
             if (mode==ManagementNodeState.HOT_STANDBY) {
                 rebindContext.setAllReadOnly();
             } else {
@@ -525,6 +540,7 @@ public class RebindManagerImpl implements RebindManager {
             }
             
             LookupContext realLookupContext = new RebindContextLookupContext(managementContext, rebindContext, exceptionHandler);
+            rebindContext.setLookupContext(realLookupContext);
             
             // Mutli-phase deserialization.
             //
@@ -847,7 +863,7 @@ public class RebindManagerImpl implements RebindManager {
             // Reconstruct entities
             logRebindingDebug("RebindManager reconstructing entities");
             for (EntityMemento entityMemento : sortParentFirst(memento.getEntityMementos()).values()) {
-                Entity entity = rebindContext.getEntity(entityMemento.getId());
+                Entity entity = rebindContext.lookup().lookupEntity(entityMemento.getId());
                 logRebindingDebug("RebindManager reconstructing entity {}", entityMemento);
     
                 if (entity == null) {
@@ -1042,7 +1058,7 @@ public class RebindManagerImpl implements RebindManager {
 
     private BrooklynClassLoadingContext getLoadingContextFromCatalogItemId(String catalogItemId, ClassLoader classLoader, RebindContext rebindContext) {
         Preconditions.checkNotNull(catalogItemId, "catalogItemId required (should not be null)");
-        CatalogItem<?, ?> catalogItem = rebindContext.getCatalogItem(catalogItemId);
+        CatalogItem<?, ?> catalogItem = rebindContext.lookup().lookupCatalogItem(catalogItemId);
         if (catalogItem != null) {
             return CatalogUtils.newClassLoadingContext(managementContext, catalogItem);
         } else {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/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 94c2b59..7e8d598 100644
--- a/core/src/main/java/brooklyn/entity/rebind/dto/MementosGenerators.java
+++ b/core/src/main/java/brooklyn/entity/rebind/dto/MementosGenerators.java
@@ -21,6 +21,7 @@ package brooklyn.entity.rebind.dto;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import java.lang.reflect.Modifier;
+import java.lang.reflect.Proxy;
 import java.util.Map;
 import java.util.Set;
 
@@ -375,6 +376,10 @@ public class MementosGenerators {
     }
     
     private static void populateBrooklynObjectMementoBuilder(BrooklynObject instance, AbstractMemento.Builder<?> builder) {
+        if (Proxy.isProxyClass(instance.getClass())) {
+            throw new IllegalStateException("Attempt to create memento from proxy "+instance+" (would fail with wrong type)");
+        }
+        
         builder.id = instance.getId();
         builder.displayName = instance.getDisplayName();
         builder.catalogItemId = instance.getCatalogItemId();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynPersistenceUtils.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynPersistenceUtils.java b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynPersistenceUtils.java
index 356234d..54990b3 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynPersistenceUtils.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynPersistenceUtils.java
@@ -29,6 +29,8 @@ import brooklyn.config.BrooklynServerConfig;
 import brooklyn.config.BrooklynServerPaths;
 import brooklyn.entity.Entity;
 import brooklyn.entity.Feed;
+import brooklyn.entity.basic.AbstractEntity;
+import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.rebind.BrooklynObjectType;
 import brooklyn.entity.rebind.PersistenceExceptionHandler;
@@ -169,6 +171,7 @@ public class BrooklynPersistenceUtils {
         for (Location instance: mgmt.getLocationManager().getLocations())
             result.location(instance.getId(), serializer.toString(newObjectMemento(instance)));
         for (Entity instance: mgmt.getEntityManager().getEntities()) {
+            instance = Entities.deproxy(instance);
             result.entity(instance.getId(), serializer.toString(newObjectMemento(instance)));
             for (Feed instanceAdjunct: ((EntityInternal)instance).feeds().getFeeds())
                 result.feed(instanceAdjunct.getId(), serializer.toString(newObjectMemento(instanceAdjunct)));

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
index c756824..329578e 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -211,7 +211,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
      * Defaults to null which means to use the remote timestamp. 
      * Only for testing as this records the remote timestamp in the object.
      * <p>
-     * If this is supplied, one must also set {@link ManagementPlaneSyncRecordPersisterToObjectStore#allowRemoteTimestampInMemento()}. */
+     * If this is supplied, one must also set {@link ManagementPlaneSyncRecordPersisterToObjectStore#useRemoteTimestampInMemento()}. */
     @VisibleForTesting
     public HighAvailabilityManagerImpl setRemoteTicker(Ticker val) {
         this.optionalRemoteTickerUtc = val;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersisterToObjectStore.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersisterToObjectStore.java b/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersisterToObjectStore.java
index b9cf465..55b645c 100644
--- a/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersisterToObjectStore.java
+++ b/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersisterToObjectStore.java
@@ -102,7 +102,7 @@ public class ManagementPlaneSyncRecordPersisterToObjectStore implements Manageme
     
     @VisibleForTesting
     /** allows, when testing, to be able to override file times / blobstore times with time from the ticker */
-    private boolean allowRemoteTimestampInMemento = false;
+    private boolean preferRemoteTimestampInMemento = false;
 
     /**
      * @param mgmt not used much at present but handy to ensure we know it so that obj store is prepared
@@ -131,8 +131,8 @@ public class ManagementPlaneSyncRecordPersisterToObjectStore implements Manageme
     }
 
     @VisibleForTesting
-    public void allowRemoteTimestampInMemento() {
-        allowRemoteTimestampInMemento = true;
+    public void preferRemoteTimestampInMemento() {
+        preferRemoteTimestampInMemento = true;
     }
     
     @Override
@@ -210,11 +210,12 @@ public class ManagementPlaneSyncRecordPersisterToObjectStore implements Manageme
                 // shouldn't happen
                 throw Exceptions.propagate(new IllegalStateException("Node record "+nodeFile+" could not be deserialized when "+mgmt.getManagementNodeId()+" was scanning: "+nodeContents, problem));
             } else {
-                if (memento.getRemoteTimestamp()!=null) {
+                if (memento.getRemoteTimestamp()!=null && preferRemoteTimestampInMemento) {
                     // in test mode, the remote timestamp is stored in the file
-                    if (!allowRemoteTimestampInMemento)
-                        throw new IllegalStateException("Remote timestamps not allowed in memento: "+nodeContents);
                 } else {
+                    if (memento.getRemoteTimestamp()!=null) {
+                        LOG.debug("Ignoring remote timestamp in memento file ("+memento+"); looks like this data has been manually copied in");
+                    }
                     Date lastModifiedDate = objectAccessor.getLastModifiedDate();
                     ((BasicManagementNodeSyncRecord)memento).setRemoteTimestamp(lastModifiedDate!=null ? lastModifiedDate.getTime() : null);
                 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/flags/TypeCoercions.java b/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
index ec08ad0..26b9dea 100644
--- a/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
+++ b/core/src/main/java/brooklyn/util/flags/TypeCoercions.java
@@ -51,6 +51,8 @@ import brooklyn.entity.basic.EntityFactory;
 import brooklyn.event.AttributeSensor;
 import brooklyn.event.basic.BasicAttributeSensor;
 import brooklyn.util.JavaGroovyEquivalents;
+import brooklyn.util.collections.QuorumCheck;
+import brooklyn.util.collections.QuorumCheck.QuorumChecks;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.guava.Maybe;
 import brooklyn.util.javalang.Enums;
@@ -696,6 +698,12 @@ public class TypeCoercions {
                 return JavaStringEscapes.unwrapJsonishListIfPossible(input);
             }
         });
+        registerAdapter(String.class, QuorumCheck.class, new Function<String,QuorumCheck>() {
+            @Override
+            public QuorumCheck apply(final String input) {
+                return QuorumChecks.of(input);
+            }
+        });
         registerAdapter(String.class, Map.class, new Function<String,Map>() {
             @Override
             public Map apply(final String input) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
index 4c736ca..4ead25d 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
@@ -101,7 +101,7 @@ public class HighAvailabilityManagerSplitBrainTest {
             objectStore.injectManagementContext(mgmt);
             objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED);
             persister = new ManagementPlaneSyncRecordPersisterToObjectStore(mgmt, objectStore, classLoader);
-            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
+            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).preferRemoteTimestampInMemento();
             BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
             mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
             ha = ((HighAvailabilityManagerImpl)mgmt.getHighAvailabilityManager())

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
index 2637ae1..449b2d9 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
@@ -83,7 +83,7 @@ public abstract class HighAvailabilityManagerTestFixture {
         objectStore.injectManagementContext(managementContext);
         objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED);
         persister = new ManagementPlaneSyncRecordPersisterToObjectStore(managementContext, objectStore, classLoader);
-        ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
+        ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).preferRemoteTimestampInMemento();
         BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(
                 objectStore, 
                 managementContext.getBrooklynProperties(), 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/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 3cb6896..76fa635 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -90,7 +90,7 @@ public class HotStandbyTest {
             objectStore.injectManagementContext(mgmt);
             objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED);
             persister = new ManagementPlaneSyncRecordPersisterToObjectStore(mgmt, objectStore, classLoader);
-            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
+            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).preferRemoteTimestampInMemento();
             BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
             ((RebindManagerImpl)mgmt.getRebindManager()).setPeriodicPersistPeriod(persistOrRebindPeriod);
             mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java b/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java
index a7b6b8a..29bb45e 100644
--- a/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java
@@ -74,7 +74,7 @@ public class WarmStandbyTest {
             objectStore.injectManagementContext(mgmt);
             objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED);
             persister = new ManagementPlaneSyncRecordPersisterToObjectStore(mgmt, objectStore, classLoader);
-            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
+            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).preferRemoteTimestampInMemento();
             BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
             mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
             ha = ((HighAvailabilityManagerImpl)mgmt.getHighAvailabilityManager())

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/utils/common/src/main/java/brooklyn/util/collections/MutableList.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/collections/MutableList.java b/utils/common/src/main/java/brooklyn/util/collections/MutableList.java
index c467f0e..c5cab8d 100644
--- a/utils/common/src/main/java/brooklyn/util/collections/MutableList.java
+++ b/utils/common/src/main/java/brooklyn/util/collections/MutableList.java
@@ -179,6 +179,13 @@ public class MutableList<V> extends ArrayList<V> {
         public ImmutableList<V> buildImmutable() {
             return ImmutableList.copyOf(result);
         }
+
+        public Builder<V> addLists(Iterable<? extends V> ...items) {
+            for (Iterable<? extends V> item: items) {
+                addAll(item);
+            }
+            return this;
+        }
     }
     
     /** as {@link List#add(Object)} but fluent style */

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/utils/common/src/main/java/brooklyn/util/collections/QuorumCheck.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/collections/QuorumCheck.java b/utils/common/src/main/java/brooklyn/util/collections/QuorumCheck.java
index 98bfddc..1536f6f 100644
--- a/utils/common/src/main/java/brooklyn/util/collections/QuorumCheck.java
+++ b/utils/common/src/main/java/brooklyn/util/collections/QuorumCheck.java
@@ -19,6 +19,14 @@
 package brooklyn.util.collections;
 
 import java.io.Serializable;
+import java.util.Iterator;
+import java.util.List;
+
+import brooklyn.util.yaml.Yamls;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Iterables;
 
 /**
  * For checking if a group/cluster is quorate. That is, whether the group has sufficient
@@ -44,13 +52,13 @@ public interface QuorumCheck {
          * Checks all members that should be up are up, and that there is at least one such member.
          */
         public static QuorumCheck allAndAtLeastOne() {
-            return new NumericQuorumCheck(1, 1.0, false, "atLeastOne");
+            return new NumericQuorumCheck(1, 1.0, false, "allAndAtLeastOne");
         }
         /**
          * Requires at least one member that should be up is up.
          */
         public static QuorumCheck atLeastOne() {
-            return new NumericQuorumCheck(1, 0.0, false);
+            return new NumericQuorumCheck(1, 0.0, false, "atLeastOne");
         }
         /**
          * Requires at least one member to be up if the total size is non-zero.
@@ -58,17 +66,68 @@ public interface QuorumCheck {
          * "Empty" means that no members are supposed to be up  (e.g. there may be stopped members).
          */
         public static QuorumCheck atLeastOneUnlessEmpty() {
-            return new NumericQuorumCheck(1, 0.0, true);
+            return new NumericQuorumCheck(1, 0.0, true, "atLeastOneUnlessEmpty");
         }
         /**
          * Always "healthy"
          */
         public static QuorumCheck alwaysTrue() {
-            return new NumericQuorumCheck(0, 0.0, true);
+            return new NumericQuorumCheck(0, 0.0, true, "alwaysHealthy");
         }
+        
         public static QuorumCheck newInstance(int minRequiredSize, double minRequiredRatio, boolean allowEmpty) {
             return new NumericQuorumCheck(minRequiredSize, minRequiredRatio, allowEmpty);
         }
+        
+        /** See {@link QuorumChecks#newLinearRange(String,String)} */
+        public static QuorumCheck newLinearRange(String range) {
+            return newLinearRange(range, null);
+        }
+        
+        /** Given a JSON representation of a list of points (where a point is a list of 2 numbers),
+         * with the points in increasing x-coordinate value,
+         * this constructs a quorum check which does linear interpolation on those coordinates,
+         * with extensions to either side.
+         * The x-coordinate is taken as the total size, and the y-coordinate as the minimum required size.
+         * <p>
+         * It sounds complicated but it gives a very easy and powerful way to define quorum checks.
+         * For instance:
+         * <p>
+         * <code>[[0,0],[1,1]]</code> says that if 0 items are expected, at least 0 is required; 
+         *   if 1 is expected, 1 is required; and by extension if 10 are expected, 10 are required.
+         *   In other words, this is the same as {@link #all()}.
+         * <p>
+         * <code>[[0,1],[1,1],[2,2]]</code> is the same as the previous for x (number expected) greater-than or equal to 1;
+         * but if 0 is expected, 1 is required, and so it fails when 0 are present.
+         * In other words, {@link #allAndAtLeastOne()}.
+         * <p>
+         * <code>[[5,5],[10,10],[100,70],[200,140]]</code> has {@link #all()} behavior up to 10 expected 
+         * (line extended to the left, for less than 5); but then gently tapers off to requiring only 70% at 100
+         * (with 30 of 40 = 75% required at that intermediate point along the line [[10,10],[100,70]]);
+         * and then from 100 onwards it is a straight 70%.
+         * <p>
+         * The type of linear regression described in the last example is quite useful in practise, 
+         * to be stricter for smaller clusters (or possibly more lax for small values in some cases,
+         * such as when tolerating dangling references during rebind). 
+         */
+        @SuppressWarnings({ "rawtypes", "unchecked" })
+        public static QuorumCheck newLinearRange(String range, String name) {
+            return LinearRangeQuorumCheck.of(name, (Iterable)Iterables.getOnlyElement( Yamls.parseAll(range) ));
+        }
+        
+        private static final List<QuorumCheck> NAMED_CHECKS = MutableList
+                .of(all(), allAndAtLeastOne(), atLeastOne(), atLeastOneUnlessEmpty(), alwaysTrue());
+        
+        public static QuorumCheck of(String nameOrRange) {
+            if (nameOrRange==null) return null;
+            for (QuorumCheck qc: NAMED_CHECKS) {
+                if (qc instanceof NumericQuorumCheck) {
+                    if (Objects.equal(nameOrRange, ((NumericQuorumCheck)qc).getName()))
+                        return qc;
+                }
+            }
+            return newLinearRange(nameOrRange);
+        }
     }
     
     public static class NumericQuorumCheck implements QuorumCheck, Serializable {
@@ -96,11 +155,82 @@ public interface QuorumCheck {
             if (sizeHealthy < totalSize*minRequiredRatio-0.000000001) return false;
             return true;
         }
+
+        public String getName() {
+            return name;
+        }
         
         @Override
         public String toString() {
-            return "QuorumCheck[require="+minRequiredSize+","+((int)100*minRequiredRatio)+"%"+(allowEmpty ? "|0" : "")+"]";
+            return "QuorumCheck["+(name!=null?name+";":"")+"require="+minRequiredSize+","+((int)100*minRequiredRatio)+"%"+(allowEmpty ? "|0" : "")+"]";
         }
     }
-    
+
+    /** See {@link QuorumChecks#newLinearRange(String,String)} */
+    public static class LinearRangeQuorumCheck implements QuorumCheck, Serializable {
+
+        private static final long serialVersionUID = -6425548115925898645L;
+
+        private static class Point {
+            final double size, minRequiredAtSize;
+            public Point(double size, double minRequiredAtSize) { this.size = size; this.minRequiredAtSize = minRequiredAtSize; }
+            public static Point ofIntegerCoords(Iterable<Integer> coords) {
+                Preconditions.checkNotNull(coords==null, "coords");
+                Preconditions.checkArgument(Iterables.size(coords)==2, "A point must consist of two coordinates; invalid data: "+coords);
+                Iterator<Integer> ci = coords.iterator();
+                return new Point(ci.next(), ci.next());
+            }
+            public static List<Point> listOfIntegerCoords(Iterable<? extends Iterable<Integer>> points) {
+                MutableList<Point> result = MutableList.of();
+                for (Iterable<Integer> point: points) result.add(ofIntegerCoords(point));
+                return result.asUnmodifiable();
+            }
+            @Override
+            public String toString() {
+                return "("+size+","+minRequiredAtSize+")";
+            }
+        }
+        
+        protected final String name;
+        protected final List<Point> points;
+
+        public static LinearRangeQuorumCheck of(String name, Iterable<? extends Iterable<Integer>> points) {
+            return new LinearRangeQuorumCheck(name, Point.listOfIntegerCoords(points));
+        }
+        public static LinearRangeQuorumCheck of(Iterable<? extends Iterable<Integer>> points) {
+            return new LinearRangeQuorumCheck(null, Point.listOfIntegerCoords(points));
+        }
+        
+        protected LinearRangeQuorumCheck(String name, Iterable<Point> points) {
+            Preconditions.checkArgument(Iterables.size(points)>=2, "At least two points must be supplied for "+name+": "+points);
+            this.name = name;
+            this.points = MutableList.copyOf(points).asUnmodifiable();
+            // check valid
+            Point last = null;
+            for (Point p: points) {
+                if (last!=null) {
+                    if (p.size <= last.size) throw new IllegalStateException("Points must be supplied in order of increasing totalSize (x coordinate); instead have "+last+" and "+p);
+                }
+            }
+        }
+
+        @Override
+        public boolean isQuorate(int sizeHealthy, int totalSize) {
+            Point next = points.get(0);
+            Point prev = null;
+            for (int i=1; i<points.size(); i++) {
+                prev = next;
+                next = points.get(i);
+                if (next.size>totalSize) break;
+            }
+            double minRequiredAtSize = (totalSize-prev.size)/(next.size-prev.size) * (next.minRequiredAtSize-prev.minRequiredAtSize) + prev.minRequiredAtSize;
+            return (sizeHealthy > minRequiredAtSize-0.000000001);
+        }
+        
+        @Override
+        public String toString() {
+            return "LinearRangeQuorumCheck["+(name!=null ? name+":" : "")+points+"]";
+        }
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/f39c6a3f/utils/common/src/test/java/brooklyn/util/collections/QuorumChecksTest.java
----------------------------------------------------------------------
diff --git a/utils/common/src/test/java/brooklyn/util/collections/QuorumChecksTest.java b/utils/common/src/test/java/brooklyn/util/collections/QuorumChecksTest.java
new file mode 100644
index 0000000..bf53d68
--- /dev/null
+++ b/utils/common/src/test/java/brooklyn/util/collections/QuorumChecksTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package brooklyn.util.collections;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import brooklyn.util.collections.QuorumCheck.QuorumChecks;
+
+public class QuorumChecksTest {
+
+    @Test
+    public void testAll() {
+        QuorumCheck q = QuorumChecks.all();
+        Assert.assertTrue(q.isQuorate(2, 2));
+        Assert.assertFalse(q.isQuorate(1, 2));
+        Assert.assertTrue(q.isQuorate(0, 0));
+    }
+    
+    @Test
+    public void testAlwaysTrue() {
+        QuorumCheck q = QuorumChecks.alwaysTrue();
+        Assert.assertTrue(q.isQuorate(0, 2));
+        Assert.assertTrue(q.isQuorate(1, 2));
+        Assert.assertTrue(q.isQuorate(0, 0));
+    }
+    
+    @Test
+    public void testAtLeastOne() {
+        QuorumCheck q = QuorumChecks.atLeastOne();
+        Assert.assertTrue(q.isQuorate(2, 2));
+        Assert.assertTrue(q.isQuorate(1, 2));
+        Assert.assertFalse(q.isQuorate(0, 0));
+    }
+    
+    @Test
+    public void testAllAndAtLeastOne() {
+        QuorumCheck q = QuorumChecks.atLeastOne();
+        Assert.assertFalse(q.isQuorate(0, 2));
+        Assert.assertTrue(q.isQuorate(1, 2));
+        Assert.assertFalse(q.isQuorate(0, 0));
+    }
+    
+    @Test
+    public void testAtLeastOneUnlessEmpty() {
+        QuorumCheck q = QuorumChecks.atLeastOneUnlessEmpty();
+        Assert.assertFalse(q.isQuorate(0, 2));
+        Assert.assertTrue(q.isQuorate(1, 2));
+        Assert.assertTrue(q.isQuorate(0, 0));
+    }
+    
+    @Test
+    public void testAtLeastOneUnlessEmptyString() {
+        QuorumCheck q = QuorumChecks.of("atLeastOneUnlessEmpty");
+        Assert.assertFalse(q.isQuorate(0, 2));
+        Assert.assertTrue(q.isQuorate(1, 2));
+        Assert.assertTrue(q.isQuorate(0, 0));
+    }
+    
+    @Test
+    public void testLinearTwoPointsNeedMinTwo() {
+        QuorumCheck q = QuorumChecks.of("[ [0,2], [1,2] ]");
+        Assert.assertTrue(q.isQuorate(2, 2));
+        Assert.assertTrue(q.isQuorate(2, 10));
+        Assert.assertFalse(q.isQuorate(1, 1));
+    }
+    
+    @Test
+    public void testLinearNeedHalfToTenAndTenPercentAtHundred() {
+        QuorumCheck q = QuorumChecks.of("[ [0,0], [10,5], [100,10], [200, 20] ]");
+        Assert.assertTrue(q.isQuorate(2, 2));
+        Assert.assertTrue(q.isQuorate(1, 2));
+        Assert.assertTrue(q.isQuorate(0, 0));
+        Assert.assertFalse(q.isQuorate(1, 10));
+        Assert.assertTrue(q.isQuorate(6, 10));
+        Assert.assertFalse(q.isQuorate(7, 50));
+        Assert.assertTrue(q.isQuorate(8, 50));
+        Assert.assertFalse(q.isQuorate(9, 100));
+        Assert.assertTrue(q.isQuorate(11, 100));
+        Assert.assertFalse(q.isQuorate(19, 200));
+        Assert.assertTrue(q.isQuorate(21, 200));
+        Assert.assertFalse(q.isQuorate(29, 300));
+        Assert.assertTrue(q.isQuorate(31, 300));
+    }
+    
+    
+    
+    
+}