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/10/01 20:03:01 UTC

[01/15] git commit: use `BrooklynFeatureEnablement` to set whether standby or hot-standby should be the default. failure going to hot_standby sets not to failure state, not standby state. rename `EntityReadOnlyInternal` to `EntityTransientCopyInternal`,

Repository: incubator-brooklyn
Updated Branches:
  refs/heads/master 8a1fd77ea -> ff11b4fc0


use `BrooklynFeatureEnablement` to set whether standby or hot-standby should be the default.
failure going to hot_standby sets not to failure state, not standby state.
rename `EntityReadOnlyInternal` to `EntityTransientCopyInternal`, and remove setDisplayName, as per code review.
better tests (working) for propagating initial items, and test (failing) for update/removal of items being rebinded.


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

Branch: refs/heads/master
Commit: a1d83eee5ec738bcd7fc8098a6b54451ee8d4d4d
Parents: 3a8a33a
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Tue Sep 30 10:31:18 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:15 2014 +0100

----------------------------------------------------------------------
 .../brooklyn/management/ManagementContext.java  |  10 +-
 .../entity/basic/EntityReadOnlyInternal.java    | 102 -------------
 .../basic/EntityTransientCopyInternal.java      | 106 ++++++++++++++
 .../entity/proxying/EntityProxyImpl.java        |   4 +-
 .../internal/BrooklynFeatureEnablement.java     |   7 +
 .../ha/HighAvailabilityManagerImpl.java         |  25 ++--
 .../internal/AbstractManagementContext.java     |  16 +++
 .../NonDeploymentManagementContext.java         |  14 ++
 .../HighAvailabilityManagerSplitBrainTest.java  |   8 +-
 .../ha/HighAvailabilityManagerTestFixture.java  |   2 +-
 .../brooklyn/management/ha/HotStandbyTest.java  | 142 +++++++++++++++++--
 11 files changed, 306 insertions(+), 130 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/api/src/main/java/brooklyn/management/ManagementContext.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ManagementContext.java b/api/src/main/java/brooklyn/management/ManagementContext.java
index 29cb20e..6e3a3b1 100644
--- a/api/src/main/java/brooklyn/management/ManagementContext.java
+++ b/api/src/main/java/brooklyn/management/ManagementContext.java
@@ -21,6 +21,7 @@ package brooklyn.management;
 import java.net.URI;
 import java.util.Collection;
 
+import brooklyn.basic.BrooklynObject;
 import brooklyn.catalog.BrooklynCatalog;
 import brooklyn.config.StringConfigMap;
 import brooklyn.entity.Application;
@@ -211,5 +212,12 @@ public interface ManagementContext {
 
     /** Active entitlements checker instance. */
     EntitlementManager getEntitlementManager();
-    
+ 
+
+    /** as {@link #lookup(String, Class)} but not constraining the return type */
+    public BrooklynObject lookup(String id);
+    /** finds an entity with the given ID known at this management context */
+    // TODO in future support policies etc
+    public <T> T lookup(String id, Class<T> type); 
+
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java b/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java
deleted file mode 100644
index 6261a4d..0000000
--- a/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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.entity.basic;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-
-import brooklyn.basic.BrooklynObjectInternal;
-import brooklyn.config.ConfigKey;
-import brooklyn.config.ConfigKey.HasConfigKey;
-import brooklyn.entity.Application;
-import brooklyn.entity.Effector;
-import brooklyn.entity.Entity;
-import brooklyn.entity.EntityType;
-import brooklyn.entity.Group;
-import brooklyn.entity.basic.EntityInternal.FeedSupport;
-import brooklyn.entity.rebind.RebindSupport;
-import brooklyn.entity.rebind.Rebindable;
-import brooklyn.event.AttributeSensor;
-import brooklyn.location.Location;
-import brooklyn.management.ManagementContext;
-import brooklyn.management.internal.EntityManagementSupport;
-import brooklyn.mementos.EntityMemento;
-import brooklyn.policy.Enricher;
-import brooklyn.policy.Policy;
-import brooklyn.util.config.ConfigBag;
-import brooklyn.util.guava.Maybe;
-
-import com.google.common.annotations.Beta;
-
-/** 
- * Selected methods from {@link EntityInternal} and parents which are permitted
- * in read-only mode.
- */
-@Beta
-public interface EntityReadOnlyInternal {
-
-    // from Entity
-    
-    String getId();
-    long getCreationTime();
-    String getDisplayName();
-    @Nullable String getIconUrl();
-    EntityType getEntityType();
-    Application getApplication();
-    String getApplicationId();
-    Entity getParent();
-    Collection<Entity> getChildren();
-    Collection<Policy> getPolicies();
-    Collection<Enricher> getEnrichers();
-    Collection<Group> getGroups();
-    Collection<Location> getLocations();
-    <T> T getAttribute(AttributeSensor<T> sensor);
-    <T> T getConfig(ConfigKey<T> key);
-    <T> T getConfig(HasConfigKey<T> key);
-    Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean includeInherited);
-    Maybe<Object> getConfigRaw(HasConfigKey<?> key, boolean includeInherited);
-    @Deprecated Set<Object> getTags();
-    @Deprecated boolean containsTag(@Nonnull Object tag);
-
-    
-    // from entity local
-    
-    void setDisplayName(String displayName);
-    @Deprecated <T> T getConfig(ConfigKey<T> key, T defaultValue);
-    @Deprecated <T> T getConfig(HasConfigKey<T> key, T defaultValue);
-    
-    // from EntityInternal:
-    
-    EntityConfigMap getConfigMap();
-    Map<ConfigKey<?>,Object> getAllConfig();
-    // for rebind mainly
-    ConfigBag getAllConfigBag();
-    ConfigBag getLocalConfigBag();
-    Map<AttributeSensor, Object> getAllAttributes();
-    EntityManagementSupport getManagementSupport();
-    ManagementContext getManagementContext();
-    Effector<?> getEffector(String effectorName);
-    FeedSupport getFeedSupport();
-    RebindSupport<EntityMemento> getRebindSupport();
-    
-}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/core/src/main/java/brooklyn/entity/basic/EntityTransientCopyInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/EntityTransientCopyInternal.java b/core/src/main/java/brooklyn/entity/basic/EntityTransientCopyInternal.java
new file mode 100644
index 0000000..0f69fe1
--- /dev/null
+++ b/core/src/main/java/brooklyn/entity/basic/EntityTransientCopyInternal.java
@@ -0,0 +1,106 @@
+/*
+ * 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.entity.basic;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import brooklyn.config.ConfigKey;
+import brooklyn.config.ConfigKey.HasConfigKey;
+import brooklyn.entity.Application;
+import brooklyn.entity.Effector;
+import brooklyn.entity.Entity;
+import brooklyn.entity.EntityType;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.EntityInternal.FeedSupport;
+import brooklyn.entity.proxying.EntityProxyImpl;
+import brooklyn.entity.rebind.RebindSupport;
+import brooklyn.event.AttributeSensor;
+import brooklyn.location.Location;
+import brooklyn.management.ManagementContext;
+import brooklyn.management.internal.EntityManagementSupport;
+import brooklyn.mementos.EntityMemento;
+import brooklyn.policy.Enricher;
+import brooklyn.policy.Policy;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.guava.Maybe;
+
+import com.google.common.annotations.Beta;
+
+/** 
+ * Selected methods from {@link EntityInternal} and parents which are permitted
+ * for entities being loaded in read-only mode, enforced by {@link EntityProxyImpl}.
+ * <p>
+ * Some of these methods do expose write capabilities, but such modifications are likely
+ * to be temporary, discarded on next rebind. Callers must take care with any such invocations.
+ * (The primary intent of this interface is to catch and prevent *most* such invocations!)
+ */
+@Beta
+public interface EntityTransientCopyInternal {
+
+    // from Entity
+    
+    String getId();
+    long getCreationTime();
+    String getDisplayName();
+    @Nullable String getIconUrl();
+    EntityType getEntityType();
+    Application getApplication();
+    String getApplicationId();
+    Entity getParent();
+    Collection<Entity> getChildren();
+    Collection<Policy> getPolicies();
+    Collection<Enricher> getEnrichers();
+    Collection<Group> getGroups();
+    Collection<Location> getLocations();
+    <T> T getAttribute(AttributeSensor<T> sensor);
+    <T> T getConfig(ConfigKey<T> key);
+    <T> T getConfig(HasConfigKey<T> key);
+    Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean includeInherited);
+    Maybe<Object> getConfigRaw(HasConfigKey<?> key, boolean includeInherited);
+    @Deprecated Set<Object> getTags();
+    @Deprecated boolean containsTag(@Nonnull Object tag);
+
+    
+    // from entity local
+    
+    @Deprecated <T> T getConfig(ConfigKey<T> key, T defaultValue);
+    @Deprecated <T> T getConfig(HasConfigKey<T> key, T defaultValue);
+
+    
+    // from EntityInternal:
+    
+    EntityConfigMap getConfigMap();
+    Map<ConfigKey<?>,Object> getAllConfig();
+    // for rebind mainly:
+    ConfigBag getAllConfigBag();
+    ConfigBag getLocalConfigBag();
+    @SuppressWarnings("rawtypes")
+    Map<AttributeSensor, Object> getAllAttributes();
+    EntityManagementSupport getManagementSupport();
+    ManagementContext getManagementContext();
+    Effector<?> getEffector(String effectorName);
+    FeedSupport getFeedSupport();
+    RebindSupport<EntityMemento> getRebindSupport();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
index 7cc5793..45364ba 100644
--- a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
+++ b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
@@ -33,7 +33,7 @@ import brooklyn.entity.Entity;
 import brooklyn.entity.basic.AbstractEntity;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
-import brooklyn.entity.basic.EntityReadOnlyInternal;
+import brooklyn.entity.basic.EntityTransientCopyInternal;
 import brooklyn.management.ManagementContext;
 import brooklyn.management.internal.EffectorUtils;
 import brooklyn.management.internal.EntityManagerInternal;
@@ -81,7 +81,7 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
 
     private static final Set<MethodSignature> ENTITY_PERMITTED_READ_ONLY_METHODS = Sets.newLinkedHashSet();
     static {
-        for (Method m : EntityReadOnlyInternal.class.getMethods()) {
+        for (Method m : EntityTransientCopyInternal.class.getMethods()) {
             ENTITY_PERMITTED_READ_ONLY_METHODS.add(new MethodSignature(m));
         }
         if (!ENTITY_NON_EFFECTOR_METHODS.containsAll(ENTITY_PERMITTED_READ_ONLY_METHODS)) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java b/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
index d5ec2ba..eb98144 100644
--- a/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
+++ b/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
@@ -23,6 +23,8 @@ import java.util.Map;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import brooklyn.management.ha.HighAvailabilityMode;
+
 import com.google.common.collect.Maps;
 
 /**
@@ -44,6 +46,10 @@ public class BrooklynFeatureEnablement {
     public static final String FEATURE_FEED_PERSISTENCE_PROPERTY = "brooklyn.experimental.feature.feedPersistence";
 
     public static final String FEATURE_CATALOG_PERSISTENCE_PROPERTY = "brooklyn.experimental.feature.catalogPersistence";
+    
+    /** whether the default standby mode is {@link HighAvailabilityMode#HOT_STANDBY} or falling back to the traditional
+     * {@link HighAvailabilityMode#STANDBY} */
+    public static final String FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY = "brooklyn.experimental.feature.defaultStandbyIsHot";
 
     private static final Map<String, Boolean> FEATURE_ENABLEMENTS = Maps.newLinkedHashMap();
 
@@ -58,6 +64,7 @@ public class BrooklynFeatureEnablement {
         setDefault(FEATURE_ENRICHER_PERSISTENCE_PROPERTY, true);
         setDefault(FEATURE_FEED_PERSISTENCE_PROPERTY, true);
         setDefault(FEATURE_CATALOG_PERSISTENCE_PROPERTY, true);
+        setDefault(FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY, false);
     }
     
     static {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/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 52fffdc..0595db5 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -40,6 +40,7 @@ import brooklyn.entity.rebind.RebindManager;
 import brooklyn.entity.rebind.plane.dto.BasicManagementNodeSyncRecord;
 import brooklyn.entity.rebind.plane.dto.ManagementPlaneSyncRecordImpl;
 import brooklyn.entity.rebind.plane.dto.ManagementPlaneSyncRecordImpl.Builder;
+import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.management.Task;
 import brooklyn.management.ha.BasicMasterChooser.AlphabeticMasterChooser;
 import brooklyn.management.ha.ManagementPlaneSyncRecordPersister.Delta;
@@ -204,8 +205,9 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     @Override
     public void start(HighAvailabilityMode startMode) {
         ownNodeId = managementContext.getManagementNodeId();
-        nodeState = ManagementNodeState.STANDBY;
         nodeStateTransitionComplete = true;
+        // always start in standby; it may get promoted to master or hot_standby in this method
+        nodeState = ManagementNodeState.STANDBY;
         running = true;
         
         // TODO Small race in that we first check, and then we'll do checkMaster() on first poll,
@@ -225,8 +227,10 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
                 ManagementNodeSyncRecord masterNodeDetails = getManagementPlaneSyncState().getManagementNodes().get(masterNodeId);
                 LOG.info("Management node "+ownNodeId+" started as HA " + nodeState + " autodetected, master is "+masterNodeId +
                     (masterNodeDetails==null || masterNodeDetails.getUri()==null ? " (no url)" : " at "+masterNodeDetails.getUri()));
-            } else {
+            } else if (nodeState == ManagementNodeState.MASTER) {
                 LOG.info("Management node "+ownNodeId+" starting as HA MASTER autodetected");
+            } else {
+                throw new IllegalStateException("Management node "+ownNodeId+" started as auto in unexpected mode "+nodeState);
             }
             break;
         case MASTER:
@@ -250,9 +254,15 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
             throw new IllegalStateException("Unexpected high availability start-mode "+startMode+" for "+this);
         }
         
-        if (nodeState==ManagementNodeState.STANDBY && startMode!=HighAvailabilityMode.STANDBY) {
-            // if standby not explicitly requested, we need promote to hot standby
-            // TODO may want a flag which enables/disables hot standby; for now, always enabled unless told to start in standby
+        if (startMode==HighAvailabilityMode.AUTO) {
+            if (BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY)) {
+                startMode = HighAvailabilityMode.HOT_STANDBY;
+            } else {
+                startMode = HighAvailabilityMode.STANDBY;
+            }
+        }
+        if (nodeState==ManagementNodeState.STANDBY && startMode==HighAvailabilityMode.HOT_STANDBY) {
+            // if it should be hot standby, then we need to promote
             nodeStateTransitionComplete = false;
             // inform the world that we are transitioning (not eligible for promotion while going in to hot standby)
             publishHealth();
@@ -624,9 +634,8 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
             return true;
         } catch (Exception e) {
             Exceptions.propagateIfFatal(e);
-            // TODO maybe move to failure state?  and record problems somewhere else for investigation subsequently.
-            nodeState = ManagementNodeState.STANDBY;
-            LOG.warn("Unable to promote "+ownNodeId+" to hot standby: "+e, e);
+            LOG.warn("Unable to promote "+ownNodeId+" to hot standby, switching to FAILED: "+e, e);
+            demoteToFailed();
             return false;
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java b/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
index b91e99b..4c6d7d9 100644
--- a/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
@@ -33,6 +33,7 @@ import javax.annotation.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import brooklyn.basic.BrooklynObject;
 import brooklyn.catalog.BrooklynCatalog;
 import brooklyn.catalog.CatalogLoadMode;
 import brooklyn.catalog.internal.BasicBrooklynCatalog;
@@ -405,4 +406,19 @@ public abstract class AbstractManagementContext implements ManagementContextInte
     public Maybe<URI> getManagementNodeUri() {
         return uri;
     }
+    
+    
+    public BrooklynObject lookup(String id) {
+        return lookup(id, BrooklynObject.class);
+    }
+    
+    @SuppressWarnings("unchecked")
+    public <T> T lookup(String id, Class<T> type) {
+        Object result;
+        result = getEntityManager().getEntity(id);
+        if (result!=null && type.isInstance(result)) return (T)result;
+        // TODO policies, etc
+        return null;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index 1738a9e..dbe50d9 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -34,6 +34,7 @@ import java.util.concurrent.TimeoutException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import brooklyn.basic.BrooklynObject;
 import brooklyn.catalog.BrooklynCatalog;
 import brooklyn.config.BrooklynProperties;
 import brooklyn.config.StringConfigMap;
@@ -417,6 +418,18 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
         initialManagementContext.removePropertiesReloadListener(listener);
     }
 
+    @Override
+    public BrooklynObject lookup(String id) {
+        checkInitialManagementContextReal();
+        return initialManagementContext.lookup(id);
+    }
+
+    @Override
+    public <T> T lookup(String id, Class<T> type) {
+        checkInitialManagementContextReal();
+        return initialManagementContext.lookup(id, type);
+    }
+
     /**
      * For when the initial management context is not "real"; the changeListener is a no-op, but everything else forbidden.
      * 
@@ -551,4 +564,5 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
             throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
         }
     }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/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 f7399d4..da1fd0a 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
@@ -187,6 +187,7 @@ public class HighAvailabilityManagerSplitBrainTest {
         HaMgmtNode n1 = newNode();
         HaMgmtNode n2 = newNode();
         
+        // first auto should become master
         n1.ha.start(HighAvailabilityMode.AUTO);
         ManagementPlaneSyncRecord memento1 = n1.ha.getManagementPlaneSyncState();
         
@@ -196,7 +197,8 @@ public class HighAvailabilityManagerSplitBrainTest {
         assertEquals(memento1.getManagementNodes().get(n1.ownNodeId).getRemoteTimestamp(), time0);
         assertEquals(memento1.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.MASTER);
 
-        n2.ha.start(HighAvailabilityMode.AUTO);
+        // second - make explicit hot; that's a strictly more complex case than cold standby, so provides pretty good coverage
+        n2.ha.start(HighAvailabilityMode.HOT_STANDBY);
         ManagementPlaneSyncRecord memento2 = n2.ha.getManagementPlaneSyncState();
         
         log.info(n2+" HA: "+memento2);
@@ -339,9 +341,9 @@ public class HighAvailabilityManagerSplitBrainTest {
                         timer.stop();
                     }
                     assertEquals(Collections.frequency(counts, ManagementNodeState.MASTER), 1);
-                    assertEquals(Collections.frequency(counts, ManagementNodeState.HOT_STANDBY), nodes.size()-1);
+                    assertEquals(Collections.frequency(counts, ManagementNodeState.HOT_STANDBY)+Collections.frequency(counts, ManagementNodeState.STANDBY), nodes.size()-1);
                     assertEquals(Collections.frequency(savedCounts, ManagementNodeState.MASTER), 1);
-                    assertEquals(Collections.frequency(savedCounts, ManagementNodeState.HOT_STANDBY), nodes.size()-1);
+                    assertEquals(Collections.frequency(savedCounts, ManagementNodeState.HOT_STANDBY)+Collections.frequency(savedCounts, ManagementNodeState.STANDBY), nodes.size()-1);
                 }});
         } catch (Throwable t) {
             log.warn("Failed to stabilize (rethrowing): "+t, t);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/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 c646f69..6fde3fa 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
@@ -216,7 +216,7 @@ public abstract class HighAvailabilityManagerTestFixture {
                 .setMaster("node1")
                 .build());
         
-        manager.start(HighAvailabilityMode.AUTO);
+        manager.start(HighAvailabilityMode.HOT_STANDBY);
         
         ManagementPlaneSyncRecord state = manager.getManagementPlaneSyncState();
         assertEquals(state.getManagementNodes().get("node1").getStatus(), ManagementNodeState.MASTER);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/a1d83eee/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 cfb6056..03eec67 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -32,7 +32,9 @@ import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.entity.Application;
+import brooklyn.entity.Entity;
 import brooklyn.entity.basic.Entities;
+import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.rebind.PersistenceExceptionHandlerImpl;
 import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
 import brooklyn.entity.rebind.persister.InMemoryObjectStore;
@@ -41,12 +43,16 @@ import brooklyn.entity.rebind.persister.PersistMode;
 import brooklyn.entity.rebind.persister.PersistenceObjectStore;
 import brooklyn.location.Location;
 import brooklyn.management.internal.ManagementContextInternal;
+import brooklyn.test.EntityTestUtils;
 import brooklyn.test.entity.LocalManagementContextForTests;
 import brooklyn.test.entity.TestApplication;
 import brooklyn.test.entity.TestEntity;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
+
+import com.google.common.collect.Iterables;
 
 @Test
 public class HotStandbyTest {
@@ -127,36 +133,146 @@ public class HotStandbyTest {
         return new InMemoryObjectStore(sharedBackingStore, sharedBackingStoreDates);
     }
 
-    @Test
-    public void testHotStandby() throws Exception {
+    private HaMgmtNode createMaster() throws Exception {
         HaMgmtNode n1 = newNode();
         n1.ha.start(HighAvailabilityMode.AUTO);
         assertEquals(n1.ha.getNodeState(), ManagementNodeState.MASTER);
-        
+        return n1;
+    }
+    
+    private HaMgmtNode createHotStandby() throws Exception {
+        HaMgmtNode n2 = newNode();
+        n2.ha.start(HighAvailabilityMode.HOT_STANDBY);
+        assertEquals(n2.ha.getNodeState(), ManagementNodeState.HOT_STANDBY);
+        return n2;
+    }
+
+    private TestApplication createFirstAppAndPersist(HaMgmtNode n1) throws Exception {        
         TestApplication app = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        app.setDisplayName("First App");
         app.start(MutableList.<Location>of());
+        app.setConfig(TestEntity.CONF_NAME, "first-app");
         app.setAttribute(TestEntity.SEQUENCE, 3);
         
         n1.mgmt.getRebindManager().forcePersistNow();
-
-        HaMgmtNode n2 = newNode();
-        n2.ha.start(HighAvailabilityMode.AUTO);
-        assertEquals(n2.ha.getNodeState(), ManagementNodeState.HOT_STANDBY);
+        return app;
+    }
+    
+    private Application waitForRebindSequenceNumber(HaMgmtNode master, HaMgmtNode hotStandby, Application app, int expectedSensorSequenceValue) {
+        Time.sleep(Duration.FIVE_SECONDS);
+        Application appRO = hotStandby.mgmt.lookup(app.getId(), Application.class);
+        // TODO drop the sleep and the new lookup, when the implementation can do in-place replace
+        
+        EntityTestUtils.assertAttributeEqualsEventually(appRO, TestEntity.SEQUENCE, expectedSensorSequenceValue);
+        return appRO;
+    }
+    
+    @Test
+    public void testHotStandbySeesChangedNameConfigAndSensorValueButDoesntAllowChange() throws Exception {
+        HaMgmtNode n1 = createMaster();
+        TestApplication app = createFirstAppAndPersist(n1);
+        HaMgmtNode n2 = createHotStandby();
 
         assertEquals(n2.mgmt.getApplications().size(), 1);
-        Application app2 = n2.mgmt.getApplications().iterator().next();
-        assertEquals(app2.getAttribute(TestEntity.SEQUENCE), (Integer)3);
+        Application appRO = n2.mgmt.lookup(app.getId(), Application.class);
+        Assert.assertNotNull(appRO);
+        Assert.assertTrue(appRO instanceof TestApplication);
+        assertEquals(appRO.getDisplayName(), "First App");
+        assertEquals(appRO.getConfig(TestEntity.CONF_NAME), "first-app");
+        assertEquals(appRO.getAttribute(TestEntity.SEQUENCE), (Integer)3);
 
         try {
-            ((TestApplication)app2).setAttribute(TestEntity.SEQUENCE, 4);
+            ((TestApplication)appRO).setAttribute(TestEntity.SEQUENCE, 4);
             Assert.fail("Should not have allowed sensor to be set");
         } catch (Exception e) {
             Assert.assertTrue(e.toString().toLowerCase().contains("read-only"), "Error message did not contain expected text: "+e);
         }
     }
-    
+
+    public void testHotStandbySeesChangedNameConfigAndSensorValue() throws Exception {
+        HaMgmtNode n1 = createMaster();
+        TestApplication app = createFirstAppAndPersist(n1);
+        HaMgmtNode n2 = createHotStandby();
+
+        assertEquals(n2.mgmt.getApplications().size(), 1);
+        Application appRO = n2.mgmt.lookup(app.getId(), Application.class);
+        Assert.assertNotNull(appRO);
+        assertEquals(appRO.getChildren().size(), 0);
+        
+        // test changes
+
+        app.setDisplayName("First App Renamed");
+        app.setConfig(TestEntity.CONF_NAME, "first-app-renamed");
+        app.setAttribute(TestEntity.SEQUENCE, 4);
+
+        appRO = waitForRebindSequenceNumber(n1, n2, app, 4);
+        assertEquals(n2.mgmt.getEntityManager().getEntities().size(), 1);
+        assertEquals(appRO.getDisplayName(), "First App Renamed");
+        assertEquals(appRO.getConfig(TestEntity.CONF_NAME), "first-app-renamed");
+        
+        // and change again for good measure!
+
+        app.setDisplayName("First App");
+        app.setConfig(TestEntity.CONF_NAME, "first-app-restored");
+        app.setAttribute(TestEntity.SEQUENCE, 5);
+        
+        appRO = waitForRebindSequenceNumber(n1, n2, app, 4);
+        assertEquals(n2.mgmt.getEntityManager().getEntities().size(), 1);
+        assertEquals(appRO.getDisplayName(), "First App");
+        assertEquals(appRO.getConfig(TestEntity.CONF_NAME), "first-app-restored");
+    }
+
+
     @Test(groups="Integration", invocationCount=50)
-    public void testHotStandbyManyTimes() throws Exception {
-        testHotStandby();
+    public void testHotStandbySeesAdditionAndRemovalManyTimes() throws Exception {
+        testHotStandbySeesStructuralChangesIncludingRemoval();
     }
+    
+    @Test(groups="Integration") // due to time (could speed up by forcing persist and rebind)
+    public void testHotStandbySeesStructuralChangesIncludingRemoval() throws Exception {
+        HaMgmtNode n1 = createMaster();
+        TestApplication app = createFirstAppAndPersist(n1);
+        HaMgmtNode n2 = createHotStandby();
+
+        assertEquals(n2.mgmt.getApplications().size(), 1);
+        Application appRO = n2.mgmt.lookup(app.getId(), Application.class);
+        Assert.assertNotNull(appRO);
+        assertEquals(appRO.getChildren().size(), 0);
+        
+        // test additions - new child, new app
+        
+        TestEntity child = app.addChild(EntitySpec.create(TestEntity.class).configure(TestEntity.CONF_NAME, "first-child"));
+        TestApplication app2 = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        
+        app.setAttribute(TestEntity.SEQUENCE, 4);
+        appRO = waitForRebindSequenceNumber(n1, n2, app, 4);
+        
+        assertEquals(appRO.getChildren().size(), 1);
+        Entity childRO = Iterables.getOnlyElement(appRO.getChildren());
+        assertEquals(childRO.getId(), child.getId());
+        assertEquals(childRO.getConfig(TestEntity.CONF_NAME), "first-child");
+        
+        assertEquals(n2.mgmt.getApplications().size(), 2);
+        Application app2RO = n2.mgmt.lookup(app2.getId(), Application.class);
+        Assert.assertNotNull(app2RO);
+        assertEquals(app2RO.getConfig(TestEntity.CONF_NAME), "second-app");
+        
+        assertEquals(n2.mgmt.getEntityManager().getEntities().size(), 3);
+        
+        // now test removals
+        
+        Entities.unmanage(child);
+        Entities.unmanage(app2);
+        
+        app.setAttribute(TestEntity.SEQUENCE, 5);
+        appRO = waitForRebindSequenceNumber(n1, n2, app, 5);
+        
+        EntityTestUtils.assertAttributeEqualsEventually(appRO, TestEntity.SEQUENCE, 5);
+        assertEquals(n2.mgmt.getEntityManager().getEntities().size(), 1);
+        assertEquals(appRO.getChildren().size(), 0);
+        assertEquals(n2.mgmt.getApplications().size(), 1);
+        Assert.assertNull(n2.mgmt.lookup(app2.getId(), Application.class));
+        Assert.assertNull(n2.mgmt.lookup(child.getId(), Application.class));
+    }
+    
 }


[15/15] git commit: This closes #205

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


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

Branch: refs/heads/master
Commit: ff11b4fc0ffd1f508985de565adbfe0835d06cf4
Parents: 1f2a378 6971809
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Oct 1 18:33:40 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 18:33:40 2014 +0100

----------------------------------------------------------------------
 usage/jsgui/src/main/webapp/assets/tpl/help/page.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------



[05/15] git commit: hot standby / read-only rebind is working. more to do, but this is the guts of it.

Posted by he...@apache.org.
hot standby / read-only rebind is working.  more to do, but this is the guts of it.


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

Branch: refs/heads/master
Commit: 3a8a33a8346736f7b44dc71e4355168c7fdb06fc
Parents: ea4b812
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Mon Sep 29 16:07:08 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:15 2014 +0100

----------------------------------------------------------------------
 .../brooklyn/entity/rebind/RebindContext.java   |   4 +
 .../brooklyn/entity/rebind/RebindSupport.java   |   2 +-
 .../brooklyn/enricher/basic/Transformer.java    |   1 +
 .../brooklyn/entity/basic/AbstractEntity.java   |   9 +-
 .../java/brooklyn/entity/basic/Entities.java    |  13 +-
 .../brooklyn/entity/basic/EntityInternal.java   |   6 +-
 .../entity/basic/EntityReadOnlyInternal.java    | 102 +++++++++++++
 .../entity/basic/ServiceStateLogic.java         |   6 +-
 .../entity/proxying/EntityProxyImpl.java        |  43 +++++-
 .../entity/proxying/InternalEntityFactory.java  |  30 +++-
 .../AbstractBrooklynObjectRebindSupport.java    |   3 +-
 .../entity/rebind/RebindContextImpl.java        |  12 ++
 .../rebind/RebindExceptionHandlerImpl.java      |   5 +
 .../entity/rebind/RebindManagerImpl.java        |  38 +++--
 .../BrooklynMementoPersisterToObjectStore.java  |   8 +-
 .../brooklyn/event/basic/BasicSensorEvent.java  |  20 ++-
 .../internal/EntityManagementSupport.java       |  23 +++
 .../internal/EntityManagerInternal.java         |  11 ++
 .../management/internal/LocalEntityManager.java |  77 ++++++----
 .../internal/NonDeploymentEntityManager.java    |  11 ++
 .../NonDeploymentManagementContext.java         |  11 +-
 .../brooklyn/management/ha/HotStandbyTest.java  |  20 ++-
 .../brooklyn/management/ha/WarmStandbyTest.java | 149 +++++++++++++++++++
 .../entity/proxy/nginx/UrlMappingImpl.java      |   7 +-
 .../brooklyn/entity/proxy/UrlMappingTest.java   |   8 +
 25 files changed, 553 insertions(+), 66 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 e3f7cf9..1ddd178 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindContext.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindContext.java
@@ -18,6 +18,7 @@
  */
 package brooklyn.entity.rebind;
 
+import brooklyn.basic.BrooklynObject;
 import brooklyn.catalog.CatalogItem;
 import brooklyn.entity.Entity;
 import brooklyn.entity.Feed;
@@ -51,4 +52,7 @@ public interface RebindContext {
     Class<?> loadClass(String typeName) throws ClassNotFoundException;
     
     RebindExceptionHandler getExceptionHandler();
+    
+    boolean isReadOnly(BrooklynObject item);
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/api/src/main/java/brooklyn/entity/rebind/RebindSupport.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindSupport.java b/api/src/main/java/brooklyn/entity/rebind/RebindSupport.java
index 435fbc6..878e93a 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindSupport.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindSupport.java
@@ -45,7 +45,7 @@ public interface RebindSupport<T extends Memento> {
      * Implementations should be very careful to not invoke or inspect these other entities/locations,
      * as they may also be being reconstructed at this time.
      * 
-     * Called before rebind.
+     * Called during rebind, after creation and before the call to start management.
      */
     void reconstruct(RebindContext rebindContext, T memento);
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/enricher/basic/Transformer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/enricher/basic/Transformer.java b/core/src/main/java/brooklyn/enricher/basic/Transformer.java
index 71770d8..d8244c3 100644
--- a/core/src/main/java/brooklyn/enricher/basic/Transformer.java
+++ b/core/src/main/java/brooklyn/enricher/basic/Transformer.java
@@ -72,6 +72,7 @@ public class Transformer<T,U> extends AbstractEnricher implements SensorEventLis
         if (transformationFromEvent != null) {
             transformation = transformationFromEvent;
         } else {
+            // TODO new named class
             transformation = new Function<SensorEvent<T>, U>() {
                 @Override public U apply(SensorEvent<T> input) {
                     return transformationFromValue.apply(input.getValue());

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 92ae2c2..3c3c65f 100644
--- a/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
+++ b/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
@@ -676,7 +676,10 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
     }
 
     @Override
-    public synchronized ManagementContext getManagementContext() {
+    public ManagementContext getManagementContext() {
+        // NB Sept 2014 - removed synch keyword above due to deadlock;
+        // it also synchs in ManagementSupport..getManagementContext();
+        // no apparent reason why it was here also.  @aledsage can you review?
         return getManagementSupport().getManagementContext();
     }
 
@@ -1423,7 +1426,7 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
     @Override
     protected void finalize() throws Throwable {
         super.finalize();
-        if (!getManagementSupport().wasDeployed())
-            LOG.warn("Entity "+this+" was never deployed -- explicit call to manage(Entity) required.");
+        if (!getManagementSupport().wasDeployed() && !Boolean.TRUE.equals(getManagementSupport().isReadOnly()))
+            LOG.warn("Entity "+this+" ("+Integer.toHexString(System.identityHashCode(this))+") was never deployed -- explicit call to manage(Entity) required.");
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 9a75ab2..aa669db 100644
--- a/core/src/main/java/brooklyn/entity/basic/Entities.java
+++ b/core/src/main/java/brooklyn/entity/basic/Entities.java
@@ -67,6 +67,7 @@ import brooklyn.management.Task;
 import brooklyn.management.TaskAdaptable;
 import brooklyn.management.TaskFactory;
 import brooklyn.management.internal.EffectorUtils;
+import brooklyn.management.internal.EntityManagerInternal;
 import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.management.internal.NonDeploymentManagementContext;
@@ -657,8 +658,10 @@ public class Entities {
      */
     public static void destroy(Entity e) {
         if (isManaged(e)) {
-            if (e instanceof Startable) Entities.invokeEffector((EntityLocal)e, e, Startable.STOP).getUnchecked();
-            if (e instanceof EntityInternal) ((EntityInternal)e).destroy();
+            if (!isReadOnly(e)) {
+                if (e instanceof Startable) Entities.invokeEffector((EntityLocal)e, e, Startable.STOP).getUnchecked();
+                if (e instanceof EntityInternal) ((EntityInternal)e).destroy();
+            }
             unmanage(e);
             log.debug("destroyed and unmanaged "+e+"; mgmt now "+
                     (e.getApplicationId()==null ? "(no app)" : e.getApplication().getManagementContext())+" - managed? "+isManaged(e));
@@ -747,6 +750,12 @@ public class Entities {
         return ((EntityInternal)e).getManagementSupport().isNoLongerManaged();
     }
 
+    /** as {@link EntityManagerInternal#isReadOnly(Entity)} */
+    @Beta
+    public static Boolean isReadOnly(Entity e) {
+        return ((EntityInternal)e).getManagementSupport().isReadOnly();
+    }
+
     /**
      * Brings this entity under management only if its ancestor is managed.
      * <p>

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/entity/basic/EntityInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/EntityInternal.java b/core/src/main/java/brooklyn/entity/basic/EntityInternal.java
index ab1fd59..43413e0 100644
--- a/core/src/main/java/brooklyn/entity/basic/EntityInternal.java
+++ b/core/src/main/java/brooklyn/entity/basic/EntityInternal.java
@@ -80,13 +80,13 @@ public interface EntityInternal extends BrooklynObjectInternal, EntityLocal, Reb
     ConfigBag getLocalConfigBag();
 
     @Beta
-    public Map<AttributeSensor, Object> getAllAttributes();
+    Map<AttributeSensor, Object> getAllAttributes();
 
     @Beta
-    public void removeAttribute(AttributeSensor<?> attribute);
+    void removeAttribute(AttributeSensor<?> attribute);
     
     @Beta
-    public void refreshInheritedConfig();
+    void refreshInheritedConfig();
 
     /**
      * Must be called before the entity is started.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java b/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java
new file mode 100644
index 0000000..6261a4d
--- /dev/null
+++ b/core/src/main/java/brooklyn/entity/basic/EntityReadOnlyInternal.java
@@ -0,0 +1,102 @@
+/*
+ * 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.entity.basic;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import brooklyn.basic.BrooklynObjectInternal;
+import brooklyn.config.ConfigKey;
+import brooklyn.config.ConfigKey.HasConfigKey;
+import brooklyn.entity.Application;
+import brooklyn.entity.Effector;
+import brooklyn.entity.Entity;
+import brooklyn.entity.EntityType;
+import brooklyn.entity.Group;
+import brooklyn.entity.basic.EntityInternal.FeedSupport;
+import brooklyn.entity.rebind.RebindSupport;
+import brooklyn.entity.rebind.Rebindable;
+import brooklyn.event.AttributeSensor;
+import brooklyn.location.Location;
+import brooklyn.management.ManagementContext;
+import brooklyn.management.internal.EntityManagementSupport;
+import brooklyn.mementos.EntityMemento;
+import brooklyn.policy.Enricher;
+import brooklyn.policy.Policy;
+import brooklyn.util.config.ConfigBag;
+import brooklyn.util.guava.Maybe;
+
+import com.google.common.annotations.Beta;
+
+/** 
+ * Selected methods from {@link EntityInternal} and parents which are permitted
+ * in read-only mode.
+ */
+@Beta
+public interface EntityReadOnlyInternal {
+
+    // from Entity
+    
+    String getId();
+    long getCreationTime();
+    String getDisplayName();
+    @Nullable String getIconUrl();
+    EntityType getEntityType();
+    Application getApplication();
+    String getApplicationId();
+    Entity getParent();
+    Collection<Entity> getChildren();
+    Collection<Policy> getPolicies();
+    Collection<Enricher> getEnrichers();
+    Collection<Group> getGroups();
+    Collection<Location> getLocations();
+    <T> T getAttribute(AttributeSensor<T> sensor);
+    <T> T getConfig(ConfigKey<T> key);
+    <T> T getConfig(HasConfigKey<T> key);
+    Maybe<Object> getConfigRaw(ConfigKey<?> key, boolean includeInherited);
+    Maybe<Object> getConfigRaw(HasConfigKey<?> key, boolean includeInherited);
+    @Deprecated Set<Object> getTags();
+    @Deprecated boolean containsTag(@Nonnull Object tag);
+
+    
+    // from entity local
+    
+    void setDisplayName(String displayName);
+    @Deprecated <T> T getConfig(ConfigKey<T> key, T defaultValue);
+    @Deprecated <T> T getConfig(HasConfigKey<T> key, T defaultValue);
+    
+    // from EntityInternal:
+    
+    EntityConfigMap getConfigMap();
+    Map<ConfigKey<?>,Object> getAllConfig();
+    // for rebind mainly
+    ConfigBag getAllConfigBag();
+    ConfigBag getLocalConfigBag();
+    Map<AttributeSensor, Object> getAllAttributes();
+    EntityManagementSupport getManagementSupport();
+    ManagementContext getManagementContext();
+    Effector<?> getEffector(String effectorName);
+    FeedSupport getFeedSupport();
+    RebindSupport<EntityMemento> getRebindSupport();
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java b/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java
index 3510121..2ba57b4 100644
--- a/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java
+++ b/core/src/main/java/brooklyn/entity/basic/ServiceStateLogic.java
@@ -90,8 +90,6 @@ public class ServiceStateLogic {
     public static <TKey,TVal> void updateMapSensorEntry(EntityLocal entity, AttributeSensor<Map<TKey,TVal>> sensor, TKey key, TVal v) {
         Map<TKey, TVal> map = entity.getAttribute(sensor);
 
-        // TODO synchronize
-        
         boolean created = (map==null);
         if (created) map = MutableMap.of();
                 
@@ -109,8 +107,10 @@ public class ServiceStateLogic {
             if (changed)
                 map.put(key, (TVal)v);
         }
-        if (changed || created)
+        if (changed || created) {
+            // TODO synchronize; then emit a copy to prevent CME's e.g. UrlMappingTest
             entity.setAttribute(sensor, map);
+        }
     }
     
     public static void setExpectedState(Entity entity, Lifecycle state) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
index e46047b..7cc5793 100644
--- a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
+++ b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
@@ -22,6 +22,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.LinkedHashSet;
 import java.util.Set;
 
 import org.slf4j.Logger;
@@ -32,7 +33,10 @@ import brooklyn.entity.Entity;
 import brooklyn.entity.basic.AbstractEntity;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
+import brooklyn.entity.basic.EntityReadOnlyInternal;
+import brooklyn.management.ManagementContext;
 import brooklyn.management.internal.EffectorUtils;
+import brooklyn.management.internal.EntityManagerInternal;
 
 import com.google.common.base.Objects;
 import com.google.common.collect.Sets;
@@ -53,6 +57,7 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
     private static final Logger LOG = LoggerFactory.getLogger(EntityProxyImpl.class);
 
     private final Entity delegate;
+    private Boolean isMaster;
     
     private static final Set<MethodSignature> OBJECT_METHODS = Sets.newLinkedHashSet();
     static {
@@ -60,7 +65,7 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
             OBJECT_METHODS.add(new MethodSignature(m));
         }
     }
-
+    
     private static final Set<MethodSignature> ENTITY_NON_EFFECTOR_METHODS = Sets.newLinkedHashSet();
     static {
         for (Method m : Entity.class.getMethods()) {
@@ -74,6 +79,19 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
         }
     }
 
+    private static final Set<MethodSignature> ENTITY_PERMITTED_READ_ONLY_METHODS = Sets.newLinkedHashSet();
+    static {
+        for (Method m : EntityReadOnlyInternal.class.getMethods()) {
+            ENTITY_PERMITTED_READ_ONLY_METHODS.add(new MethodSignature(m));
+        }
+        if (!ENTITY_NON_EFFECTOR_METHODS.containsAll(ENTITY_PERMITTED_READ_ONLY_METHODS)) {
+            Set<MethodSignature> extras = new LinkedHashSet<EntityProxyImpl.MethodSignature>(ENTITY_PERMITTED_READ_ONLY_METHODS);
+            extras.removeAll(ENTITY_NON_EFFECTOR_METHODS);
+            throw new IllegalStateException("Entity read-only methods contains items not known as Entity methods: "+
+                extras);
+        }
+    }
+
     public EntityProxyImpl(Entity entity) {
         this.delegate = checkNotNull(entity, "entity");
     }
@@ -83,6 +101,15 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
         return delegate.toString();
     }
     
+    protected boolean isMaster() {
+        if (isMaster!=null) return isMaster;
+        ManagementContext mgmt = ((EntityInternal)delegate).getManagementContext();
+        Boolean isReadOnlyNow = ((EntityManagerInternal)mgmt.getEntityManager()).isReadOnly(delegate);
+        if (isReadOnlyNow==null) return false;
+        isMaster = !isReadOnlyNow;
+        return isMaster;
+    }
+    
     public Object invoke(Object proxy, final Method m, final Object[] args) throws Throwable {
         if (proxy == null) {
             throw new IllegalArgumentException("Static methods not supported via proxy on entity "+delegate);
@@ -93,6 +120,15 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
         Object result;
         if (OBJECT_METHODS.contains(sig)) {
             result = m.invoke(this, args);
+        } else if (!isMaster()) {
+            if (ENTITY_PERMITTED_READ_ONLY_METHODS.contains(sig)) {
+                result = m.invoke(delegate, args);
+            } else if (isMaster==null) {
+                // rebinding or caller manipulating before management; permit all access
+                result = m.invoke(delegate, args);
+            } else {
+                throw new UnsupportedOperationException("Call to '"+sig+"' not permitted on read-only entity "+delegate);
+            }
         } else if (ENTITY_NON_EFFECTOR_METHODS.contains(sig)) {
             result = m.invoke(delegate, args);
         } else {
@@ -139,6 +175,11 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
             MethodSignature o = (MethodSignature) obj;
             return name.equals(o.name) && Arrays.equals(parameterTypes, o.parameterTypes);
         }
+        
+        @Override
+        public String toString() {
+            return name+Arrays.toString(parameterTypes);
+        }
     }
     
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/entity/proxying/InternalEntityFactory.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/proxying/InternalEntityFactory.java b/core/src/main/java/brooklyn/entity/proxying/InternalEntityFactory.java
index 78fa574..c2e21e1 100644
--- a/core/src/main/java/brooklyn/entity/proxying/InternalEntityFactory.java
+++ b/core/src/main/java/brooklyn/entity/proxying/InternalEntityFactory.java
@@ -35,6 +35,7 @@ import brooklyn.entity.Group;
 import brooklyn.entity.basic.AbstractApplication;
 import brooklyn.entity.basic.AbstractEntity;
 import brooklyn.entity.basic.BrooklynTaskTags;
+import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
 import brooklyn.management.internal.LocalEntityManager;
@@ -51,6 +52,7 @@ import brooklyn.util.flags.FlagUtils;
 import brooklyn.util.javalang.AggregateClassLoader;
 import brooklyn.util.task.Tasks;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
@@ -83,6 +85,7 @@ public class InternalEntityFactory extends InternalFactory {
         this.policyFactory = checkNotNull(policyFactory, "policyFactory");
     }
 
+    @VisibleForTesting
     public <T extends Entity> T createEntityProxy(EntitySpec<T> spec, T entity) {
         Set<Class<?>> interfaces = Sets.newLinkedHashSet();
         if (spec.getType().isInterface()) {
@@ -96,9 +99,10 @@ public class InternalEntityFactory extends InternalFactory {
     }
 
     @SuppressWarnings("unchecked")
-    public <T extends Entity> T createEntityProxy(Iterable<Class<?>> interfaces, T entity) {
-        // TODO Don't want the proxy to have to implement EntityLocal, but required by how 
-        // AbstractEntity.parent is used (e.g. parent.getAllConfig)
+    protected <T extends Entity> T createEntityProxy(Iterable<Class<?>> interfaces, T entity) {
+        // We don't especially want the proxy to have to implement EntityLocal, 
+        // but required by how AbstractEntity.parent is used (e.g. parent.getAllConfig).
+        // However within EntityProxyImpl we place add'l guards to prevent read-only access to such methods
         Set<Class<?>> allInterfaces = MutableSet.<Class<?>>builder()
                 .add(EntityProxy.class, Entity.class, EntityLocal.class, EntityInternal.class)
                 .addAll(interfaces)
@@ -149,6 +153,12 @@ public class InternalEntityFactory extends InternalFactory {
         }
     }
 
+    /** creates a new entity instance from a spec, with all children, policies, etc,
+     * fully initialized ({@link AbstractEntity#init()} invoked) and ready for
+     * management -- commonly the caller will next call 
+     * {@link Entities#manage(Entity)} (if it's in a managed application)
+     * or {@link Entities#startManagement(brooklyn.entity.Application, brooklyn.management.ManagementContext)}
+     * (if it's an application) */
     public <T extends Entity> T createEntity(EntitySpec<T> spec) {
         /* Order is important here. Changed Jul 2014 when supporting children in spec.
          * (Previously was much simpler, and parent was set right after running initializers; and there were no children.)
@@ -317,9 +327,16 @@ public class InternalEntityFactory extends InternalFactory {
     }
     
     /**
-     * Constructs an entity (if new-style, calls no-arg constructor; if old-style, uses spec to pass in config).
-     * Sets the entity's proxy. If {@link EntitySpec#id(String)} was set then uses that to override the entity's id, 
+     * Constructs an entity, i.e. instantiate the actual class given a spec,
+     * and sets the entity's proxy. Used by this factory to {@link #createEntity(EntitySpec)}
+     * and also used during rebind.
+     * <p> 
+     * If {@link EntitySpec#id(String)} was set then uses that to override the entity's id, 
      * but that behaviour is deprecated.
+     * <p>
+     * The new-style no-arg constructor is preferred, and   
+     * configuration from the {@link EntitySpec} is <b>not</b> normally applied,
+     * although for old-style entities flags from the spec are passed to the constructor.
      */
     public <T extends Entity> T constructEntity(Class<? extends T> clazz, EntitySpec<T> spec) {
         @SuppressWarnings("deprecation")
@@ -331,7 +348,10 @@ public class InternalEntityFactory extends InternalFactory {
     /**
      * Constructs a new-style entity (fails if no no-arg constructor).
      * Sets the entity's id and proxy.
+     * <p>
+     * As {@link #constructEntity(Class, EntitySpec)} but when no spec is used.
      */
+    // TODO would it be cleaner to have callers just create a spec? and deprecate this?
     public <T extends Entity> T constructEntity(Class<T> clazz, Iterable<Class<?>> interfaces, String entityId) {
         if (!isNewStyle(clazz)) {
             throw new IllegalStateException("Cannot construct old-style entity "+clazz);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 ffe92db..64d7cee 100644
--- a/core/src/main/java/brooklyn/entity/rebind/AbstractBrooklynObjectRebindSupport.java
+++ b/core/src/main/java/brooklyn/entity/rebind/AbstractBrooklynObjectRebindSupport.java
@@ -53,7 +53,8 @@ public abstract class AbstractBrooklynObjectRebindSupport<T extends Memento> imp
         addCustoms(rebindContext, memento);
         
         doReconstruct(rebindContext, memento);
-        instance.rebind();
+        if (!rebindContext.isReadOnly(instance))
+            instance.rebind();
     }
 
     protected abstract void addConfig(RebindContext rebindContext, T memento);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 97269c2..bcac4a7 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindContextImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindContextImpl.java
@@ -23,6 +23,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import java.util.Collection;
 import java.util.Map;
 
+import brooklyn.basic.BrooklynObject;
 import brooklyn.catalog.CatalogItem;
 import brooklyn.entity.Entity;
 import brooklyn.entity.Feed;
@@ -44,6 +45,8 @@ public class RebindContextImpl implements RebindContext {
     private final ClassLoader classLoader;
     private final RebindExceptionHandler exceptionHandler;
     
+    private boolean allAreReadOnly = false;
+    
     public RebindContextImpl(RebindExceptionHandler exceptionHandler, ClassLoader classLoader) {
         this.exceptionHandler = checkNotNull(exceptionHandler, "exceptionHandler");
         this.classLoader = checkNotNull(classLoader, "classLoader");
@@ -152,4 +155,13 @@ public class RebindContextImpl implements RebindContext {
     protected Collection<CatalogItem<?, ?>> getCatalogItems() {
         return catalogItems.values();
     }
+
+    public void setAllReadOnly() {
+        allAreReadOnly = true;
+    }
+    
+    public boolean isReadOnly(BrooklynObject item) {
+        return allAreReadOnly;
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 de37f42..954d95b 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
@@ -332,6 +332,11 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
         if (rebindFailureMode == RebindManager.RebindFailureMode.FAIL_FAST) {
             throw new IllegalStateException("Rebind: aborting due to "+errmsg, e);
         } else {
+            if (Thread.currentThread().isInterrupted()) {
+                if (LOG.isDebugEnabled())
+                    LOG.debug("Rebind: while interrupted, received "+errmsg+"/"+e+"; throwing interruption", e);
+                throw Exceptions.propagate(new InterruptedException("Detected interruptiong while not sleeping, due to secondary error rebinding: "+errmsg+"/"+e));
+            }
             LOG.warn("Rebind: continuing after "+errmsg, e);
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 47f3770..cddb43f 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -45,7 +45,6 @@ import brooklyn.entity.Feed;
 import brooklyn.entity.basic.AbstractApplication;
 import brooklyn.entity.basic.AbstractEntity;
 import brooklyn.entity.basic.ConfigKeys;
-import brooklyn.entity.basic.Entities;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.proxying.InternalEntityFactory;
 import brooklyn.entity.proxying.InternalFactory;
@@ -61,6 +60,7 @@ import brooklyn.management.ExecutionContext;
 import brooklyn.management.Task;
 import brooklyn.management.ha.HighAvailabilityManagerImpl;
 import brooklyn.management.ha.ManagementNodeState;
+import brooklyn.management.internal.EntityManagerInternal;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.mementos.BrooklynMemento;
 import brooklyn.mementos.BrooklynMementoManifest;
@@ -85,6 +85,7 @@ import brooklyn.util.javalang.Reflections;
 import brooklyn.util.task.BasicExecutionContext;
 import brooklyn.util.task.BasicTask;
 import brooklyn.util.task.ScheduledTask;
+import brooklyn.util.task.Tasks;
 import brooklyn.util.text.Strings;
 import brooklyn.util.time.Duration;
 
@@ -308,6 +309,10 @@ public class RebindManagerImpl implements RebindManager {
         if (readOnlyTask!=null) {
             readOnlyTask.cancel(true);
             readOnlyTask.blockUntilEnded();
+            boolean reallyEnded = Tasks.blockUntilInternalTasksEnded(readOnlyTask, Duration.TEN_SECONDS);
+            if (!reallyEnded) {
+                LOG.warn("Rebind (read-only) tasks took too long to die after interrupt (ignoring): "+readOnlyTask);
+            }
             readOnlyTask = null;
         }
         LOG.debug("Stopped read-only rebinding, mgmt "+managementContext.getManagementNodeId());
@@ -440,11 +445,9 @@ public class RebindManagerImpl implements RebindManager {
             throw exceptionHandler.onFailed(e);
         }
     }
-
+    
     protected List<Application> rebindImpl(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler, ManagementNodeState mode) throws IOException {
         checkNotNull(classLoader, "classLoader");
-        boolean isReadOnly = mode==ManagementNodeState.HOT_STANDBY;
-        if (!isReadOnly) Preconditions.checkState(mode==ManagementNodeState.MASTER, "Must be either master or read only to rebind (mode "+mode+")");
 
         RebindTracker.setRebinding();
         try {
@@ -452,6 +455,12 @@ public class RebindManagerImpl implements RebindManager {
             
             Reflections reflections = new Reflections(classLoader);
             RebindContextImpl rebindContext = new RebindContextImpl(exceptionHandler, classLoader);
+            if (mode==ManagementNodeState.HOT_STANDBY) {
+                rebindContext.setAllReadOnly();
+            } else {
+                Preconditions.checkState(mode==ManagementNodeState.MASTER, "Must be either master or read only to rebind (mode "+mode+")");
+            }
+            
             LookupContext realLookupContext = new RebindContextLookupContext(managementContext, rebindContext, exceptionHandler);
             
             // Mutli-phase deserialization.
@@ -477,9 +486,15 @@ public class RebindManagerImpl implements RebindManager {
             
             boolean isEmpty = mementoManifest.isEmpty();
             if (!isEmpty) {
-                LOG.info("Rebinding from "+getPersister().getBackingStoreDescription()+"...");
+                if (mode==ManagementNodeState.HOT_STANDBY)
+                    LOG.debug("Rebinding (read-only) from "+getPersister().getBackingStoreDescription()+"...");
+                else
+                    LOG.info("Rebinding from "+getPersister().getBackingStoreDescription()+"...");
             } else {
-                LOG.info("Rebind check: no existing state; will persist new items to "+getPersister().getBackingStoreDescription());
+                if (mode==ManagementNodeState.HOT_STANDBY)
+                    LOG.debug("Rebind check (read-only): no existing state, reading from "+getPersister().getBackingStoreDescription());
+                else
+                    LOG.info("Rebind check: no existing state; will persist new items to "+getPersister().getBackingStoreDescription());
             }
 
             
@@ -511,6 +526,9 @@ public class RebindManagerImpl implements RebindManager {
                 
                 try {
                     Entity entity = newEntity(entityId, entityType, reflections);
+                    if (rebindContext.isReadOnly(entity)) {
+                        ((EntityInternal)entity).getManagementSupport().setReadOnly();
+                    }
                     rebindContext.registerEntity(entityId, entity);
                 } catch (Exception e) {
                     exceptionHandler.onCreateFailed(BrooklynObjectType.ENTITY, entityId, entityType, e);
@@ -774,8 +792,10 @@ public class RebindManagerImpl implements RebindManager {
                     exceptionHandler.onNotFound(BrooklynObjectType.ENTITY, appId);
                 } else {
                     try {
-                        // TODO if read only we must do something special here!
-                        Entities.startManagement((Application)entity, managementContext);
+                        if (rebindContext.isReadOnly(entity)) { 
+                            ((EntityManagerInternal)managementContext.getEntityManager()).setReadOnly(entity);
+                        }
+                        managementContext.getEntityManager().manage(entity);
                     } catch (Exception e) {
                         exceptionHandler.onManageFailed(BrooklynObjectType.ENTITY, entity, e);
                     }
@@ -816,7 +836,7 @@ public class RebindManagerImpl implements RebindManager {
 
             exceptionHandler.onDone();
 
-            if (!isEmpty) {
+            if (!isEmpty && mode!=ManagementNodeState.HOT_STANDBY) {
                 LOG.info("Rebind complete: {} app{}, {} entit{}, {} location{}, {} polic{}, {} enricher{}, {} feed{}, {} catalog item{}", new Object[]{
                     apps.size(), Strings.s(apps),
                     rebindContext.getEntities().size(), Strings.ies(rebindContext.getEntities()),

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
index e02de72..f3a2882 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
@@ -67,6 +67,7 @@ import brooklyn.util.xstream.XmlUtil;
 import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -612,12 +613,17 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
     public void waitForWritesCompleted(Duration timeout) throws InterruptedException, TimeoutException {
         boolean locked = lock.readLock().tryLock(timeout.toMillisecondsRoundingUp(), TimeUnit.MILLISECONDS);
         if (locked) {
+            ImmutableSet<StoreObjectAccessorWithLock> wc;
+            synchronized (writers) {
+                wc = ImmutableSet.copyOf(writers.values());
+            }
             lock.readLock().unlock();
             
             // Belt-and-braces: the lock above should be enough to ensure no outstanding writes, because
             // each writer is now synchronous.
-            for (StoreObjectAccessorWithLock writer : writers.values())
+            for (StoreObjectAccessorWithLock writer : wc) {
                 writer.waitForCurrentWrites(timeout);
+            }
         } else {
             throw new TimeoutException("Timeout waiting for writes to "+objectStore);
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/event/basic/BasicSensorEvent.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/event/basic/BasicSensorEvent.java b/core/src/main/java/brooklyn/event/basic/BasicSensorEvent.java
index 33134f4..3f09ffe 100644
--- a/core/src/main/java/brooklyn/event/basic/BasicSensorEvent.java
+++ b/core/src/main/java/brooklyn/event/basic/BasicSensorEvent.java
@@ -18,6 +18,11 @@
  */
 package brooklyn.event.basic;
 
+import java.util.ConcurrentModificationException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import brooklyn.entity.Entity;
 import brooklyn.event.Sensor;
 import brooklyn.event.SensorEvent;
@@ -28,6 +33,9 @@ import com.google.common.base.Objects;
  * A {@link SensorEvent} containing data from a {@link Sensor} generated by an {@link Entity}.
  */
 public class BasicSensorEvent<T> implements SensorEvent<T> {
+    
+    private static final Logger log = LoggerFactory.getLogger(BasicSensorEvent.class);
+    
     private final Sensor<T> sensor;
     private final Entity source;
     private final T value;
@@ -77,6 +85,16 @@ public class BasicSensorEvent<T> implements SensorEvent<T> {
     
     @Override
     public String toString() {
-        return source+"."+sensor+"="+value+" @ "+timestamp;
+        try {
+            return source+"."+sensor+"="+value+" @ "+timestamp;
+        } catch (ConcurrentModificationException e) {
+            // TODO occasional CME observed on shutdown, wrt map, e.g. in UrlMappingTest
+            // transformations should set a copy of the map; see e.g. in ServiceStateLogic.updateMapSensor
+            String result = getClass()+":"+source+"."+sensor+"@"+timestamp;
+            log.warn("Error creating string for " + result + " (ignoring): " + e);
+            if (log.isDebugEnabled())
+                log.debug("Trace for error creating string for " + result + " (ignoring): " + e, e);
+            return result;
+        }
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java b/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
index 0b31289..1e2779a 100644
--- a/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
+++ b/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
@@ -89,6 +89,7 @@ public class EntityManagementSupport {
     protected final AtomicBoolean managementContextUsable = new AtomicBoolean(false);
     protected final AtomicBoolean currentlyDeployed = new AtomicBoolean(false);
     protected final AtomicBoolean everDeployed = new AtomicBoolean(false);
+    protected Boolean readOnly = null;
     protected final AtomicBoolean managementFailed = new AtomicBoolean(false);
     
     private volatile EntityChangeListener entityChangeListener = EntityChangeListener.NOOP;
@@ -101,7 +102,28 @@ public class EntityManagementSupport {
     public boolean isNoLongerManaged() {
         return wasDeployed() && !isDeployed();
     }
+    /** whether entity has ever been deployed (managed) */
     public boolean wasDeployed() { return everDeployed.get(); }
+    
+    @Beta
+    public void setReadOnly() {
+        if (isDeployed())
+            throw new IllegalStateException("Cannot set read only after deployment");
+        readOnly = true;
+    }
+
+    /** as {@link EntityManagerInternal#isReadOnly(Entity)} */
+    @Beta
+    public Boolean isReadOnly() {
+        if (readOnly==null) {
+            // readOnly is set in two places, by rebindManagerImpl when the entity is created,
+            // and again when the entity manager is told we should be read only;
+            // TODO twice setting read-only is redundant; possibly doesn't even need to be on managementContext?
+            // however we must check here to discover canonically when it IS fully managed and it is NOT read only
+            readOnly = ((EntityManagerInternal)managementContext.getEntityManager()).isReadOnly(entity);
+        }
+        return readOnly;
+    }
 
     /**
      * Whether the entity's management lifecycle is complete (i.e. both "onManagementStarting" and "onManagementStarted" have
@@ -421,4 +443,5 @@ public class EntityManagementSupport {
             getManagementContext().getRebindManager().getChangeListener().onChanged(entity);
         }
     }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java b/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
index fc7485e..0dc7c13 100644
--- a/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
+++ b/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
@@ -27,4 +27,15 @@ public interface EntityManagerInternal extends EntityManager {
     /** gets all entities currently known to the application, including entities that are not yet managed */
     Iterable<Entity> getAllEntitiesInApplication(Application application);
 
+    /**
+     * Indicates that the given entity should be treated as read only
+     * when it is subsequently managed.
+     * @throws IllegalStateException if the entity is already managed.
+     */
+    void setReadOnly(Entity e);
+
+    /** Whether the entity and its adjuncts should be treated as read-only;
+     * may be null when initializing if RO status is unknown. */
+    Boolean isReadOnly(Entity item);
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
index 9f9e5e3..69fe77b 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
@@ -84,7 +84,9 @@ public class LocalEntityManager implements EntityManagerInternal {
     
     /** Real managed entities */
     protected final Map<String,Entity> entitiesById = Maps.newLinkedHashMap();
-    
+
+    protected final Set<String> readOnlyEntityIds = Sets.newLinkedHashSet();
+
     /** Proxies of the managed entities */
     protected final ObservableList entities = new ObservableList();
     
@@ -220,6 +222,16 @@ public class LocalEntityManager implements EntityManagerInternal {
         preRegisteredEntitiesById.put(entity.getId(), entity);
     }
 
+    public void setReadOnly(Entity e) {
+        readOnlyEntityIds.add(e.getId());
+        ((EntityInternal)e).getManagementSupport().setReadOnly();
+    }
+    
+    @Override
+    public Boolean isReadOnly(Entity item) {
+        return readOnlyEntityIds.contains(item.getId());
+    }
+    
     // TODO synchronization issues here. We guard with isManaged(), but if another thread executing 
     // concurrently then the managed'ness could be set after our check but before we do 
     // onManagementStarting etc. However, we can't just synchronize because we're calling alien code 
@@ -265,24 +277,28 @@ public class LocalEntityManager implements EntityManagerInternal {
     }
     
     @Override
-    public void unmanage(Entity e) {
+    public void unmanage(final Entity e) {
         if (shouldSkipUnmanagement(e)) return;
-        // Need to store all child entities as onManagementStopping removes a child from the parent entity
-        final List<EntityInternal> allEntities =  Lists.newArrayList();        
-        final ManagementTransitionInfo info = new ManagementTransitionInfo(managementContext, ManagementTransitionMode.NORMAL);
-        recursively(e, new Predicate<EntityInternal>() { public boolean apply(EntityInternal it) {
-            if (shouldSkipUnmanagement(it)) return false;
-            allEntities.add(it);
-            it.getManagementSupport().onManagementStopping(info); 
-            return true;
-        } });
-        
-        for (EntityInternal it : allEntities) {
-            if (shouldSkipUnmanagement(it)) continue;
-            unmanageNonRecursive(it);            
-            it.getManagementSupport().onManagementStopped(info);
-            managementContext.getRebindManager().getChangeListener().onUnmanaged(it);
-            if (managementContext.gc != null) managementContext.gc.onUnmanaged(it);
+        if (isReadOnly(e)) {
+            unmanageNonRecursive(e);
+        } else {
+            // Need to store all child entities as onManagementStopping removes a child from the parent entity
+            final List<EntityInternal> allEntities =  Lists.newArrayList();        
+            final ManagementTransitionInfo info = new ManagementTransitionInfo(managementContext, ManagementTransitionMode.NORMAL);
+            recursively(e, new Predicate<EntityInternal>() { public boolean apply(EntityInternal it) {
+                if (shouldSkipUnmanagement(it)) return false;
+                allEntities.add(it);
+                it.getManagementSupport().onManagementStopping(info);
+                return true;
+            } });
+
+            for (EntityInternal it : allEntities) {
+                if (shouldSkipUnmanagement(it)) continue;
+                unmanageNonRecursive(it);
+                it.getManagementSupport().onManagementStopped(info);
+                managementContext.getRebindManager().getChangeListener().onUnmanaged(it);
+                if (managementContext.gc != null) managementContext.gc.onUnmanaged(it);
+            }
         }
     }
     
@@ -299,6 +315,8 @@ public class LocalEntityManager implements EntityManagerInternal {
             return;
         } else if (isPreManaged(entity)) {
             return;
+        } else if (Boolean.TRUE.equals(((EntityInternal)entity).getManagementSupport().isReadOnly())) {
+            return;
         } else {
             Entity rootUnmanaged = entity;
             while (true) {
@@ -415,16 +433,20 @@ public class LocalEntityManager implements EntityManagerInternal {
          * from its groups?
          */
         
-        Collection<Group> groups = e.getGroups();
-        e.clearParent();
-        for (Group group : groups) {
-            group.removeMember(e);
-        }
-        if (e instanceof Group) {
-            Collection<Entity> members = ((Group)e).getMembers();
-            for (Entity member : members) {
-                member.removeGroup((Group)e);
+        if (!isReadOnly(e)) {
+            Collection<Group> groups = e.getGroups();
+            e.clearParent();
+            for (Group group : groups) {
+                group.removeMember(e);
             }
+            if (e instanceof Group) {
+                Collection<Entity> members = ((Group)e).getMembers();
+                for (Entity member : members) {
+                    member.removeGroup((Group)e);
+                }
+            }
+        } else {
+            log.debug("No relations being updated on unmanage of read only {}", e);
         }
 
         synchronized (this) {
@@ -436,6 +458,7 @@ public class LocalEntityManager implements EntityManagerInternal {
 
             entities.remove(proxyE);
             entityProxiesById.remove(e.getId());
+            readOnlyEntityIds.remove(e.getId());
             Object old = entitiesById.remove(e.getId());
 
             entityTypes.remove(e.getId());

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
index 67dbe97..f6cc2ef 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
@@ -155,4 +155,15 @@ public class NonDeploymentEntityManager implements EntityManagerInternal {
             throw new IllegalStateException("Non-deployment context "+this+" (with no initial management context supplied) is not valid for this operation.");
         }
     }
+    
+    @Override
+    public void setReadOnly(Entity e) {
+        throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation: cannot manage "+e);
+    }
+
+    @Override
+    public Boolean isReadOnly(Entity item) {
+        // NB: must return null here, otherwise callers may cache this value
+        return null;
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index 21f1968..1738a9e 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -31,6 +31,9 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import brooklyn.catalog.BrooklynCatalog;
 import brooklyn.config.BrooklynProperties;
 import brooklyn.config.StringConfigMap;
@@ -74,6 +77,8 @@ import com.google.common.base.Objects;
 
 public class NonDeploymentManagementContext implements ManagementContextInternal {
 
+    private static final Logger log = LoggerFactory.getLogger(NonDeploymentManagementContext.class);
+    
     public enum NonDeploymentManagementContextMode {
         PRE_MANAGEMENT,
         MANAGEMENT_REBINDING,
@@ -374,12 +379,14 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
 
     @Override
     public void prePreManage(Entity entity) {
-        // no-op
+        // should throw?  but in 0.7.0-SNAPSHOT it was no-op
+        log.warn("Ignoring call to prePreManage("+entity+") on "+this);
     }
 
     @Override
     public void prePreManage(Location location) {
-        // no-op
+        // should throw?  but in 0.7.0-SNAPSHOT it was no-op
+        log.warn("Ignoring call to prePreManage("+location+") on "+this);
     }
 
     private boolean isInitialManagementContextReal() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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 cc9f065..cfb6056 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -26,10 +26,12 @@ import java.util.Map;
 
 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.Application;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.rebind.PersistenceExceptionHandlerImpl;
 import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
@@ -86,6 +88,8 @@ public class HotStandbyTest {
         
         public void tearDown() throws Exception {
             if (ha != null) ha.stop();
+            if (mgmt!=null) mgmt.getRebindManager().stop();
+            
             if (mgmt != null) Entities.destroyAll(mgmt);
             if (objectStore != null) objectStore.deleteCompletely();
         }
@@ -123,8 +127,6 @@ public class HotStandbyTest {
         return new InMemoryObjectStore(sharedBackingStore, sharedBackingStoreDates);
     }
 
-    
-    
     @Test
     public void testHotStandby() throws Exception {
         HaMgmtNode n1 = newNode();
@@ -142,7 +144,19 @@ public class HotStandbyTest {
         assertEquals(n2.ha.getNodeState(), ManagementNodeState.HOT_STANDBY);
 
         assertEquals(n2.mgmt.getApplications().size(), 1);
+        Application app2 = n2.mgmt.getApplications().iterator().next();
+        assertEquals(app2.getAttribute(TestEntity.SEQUENCE), (Integer)3);
+
+        try {
+            ((TestApplication)app2).setAttribute(TestEntity.SEQUENCE, 4);
+            Assert.fail("Should not have allowed sensor to be set");
+        } catch (Exception e) {
+            Assert.assertTrue(e.toString().toLowerCase().contains("read-only"), "Error message did not contain expected text: "+e);
+        }
     }
     
-
+    @Test(groups="Integration", invocationCount=50)
+    public void testHotStandbyManyTimes() throws Exception {
+        testHotStandby();
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/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
new file mode 100644
index 0000000..d955d50
--- /dev/null
+++ b/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.management.ha;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.rebind.PersistenceExceptionHandlerImpl;
+import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
+import brooklyn.entity.rebind.persister.InMemoryObjectStore;
+import brooklyn.entity.rebind.persister.ListeningObjectStore;
+import brooklyn.entity.rebind.persister.PersistMode;
+import brooklyn.entity.rebind.persister.PersistenceObjectStore;
+import brooklyn.location.Location;
+import brooklyn.management.internal.ManagementContextInternal;
+import brooklyn.test.entity.LocalManagementContextForTests;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+@Test
+public class WarmStandbyTest {
+
+    private static final Logger log = LoggerFactory.getLogger(WarmStandbyTest.class);
+    
+    private List<HaMgmtNode> nodes = new MutableList<WarmStandbyTest.HaMgmtNode>();
+    Map<String,String> sharedBackingStore = MutableMap.of();
+    Map<String,Date> sharedBackingStoreDates = MutableMap.of();
+    private ClassLoader classLoader = getClass().getClassLoader();
+    
+    public class HaMgmtNode {
+        
+        private ManagementContextInternal mgmt;
+        private String ownNodeId;
+        private String nodeName;
+        private ListeningObjectStore objectStore;
+        private ManagementPlaneSyncRecordPersister persister;
+        private HighAvailabilityManagerImpl ha;
+
+        @BeforeMethod(alwaysRun=true)
+        public void setUp() throws Exception {
+            nodeName = "node "+nodes.size();
+            mgmt = newLocalManagementContext();
+            ownNodeId = mgmt.getManagementNodeId();
+            objectStore = new ListeningObjectStore(newPersistenceObjectStore());
+            objectStore.injectManagementContext(mgmt);
+            objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED);
+            persister = new ManagementPlaneSyncRecordPersisterToObjectStore(mgmt, objectStore, classLoader);
+            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
+            BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
+            mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
+            ha = ((HighAvailabilityManagerImpl)mgmt.getHighAvailabilityManager())
+                .setPollPeriod(Duration.PRACTICALLY_FOREVER)
+                .setHeartbeatTimeout(Duration.THIRTY_SECONDS)
+                .setPersister(persister);
+            log.info("Created "+nodeName+" "+ownNodeId);
+        }
+        
+        public void tearDown() throws Exception {
+            if (ha != null) ha.stop();
+            if (mgmt != null) Entities.destroyAll(mgmt);
+            if (objectStore != null) objectStore.deleteCompletely();
+        }
+        
+        @Override
+        public String toString() {
+            return nodeName+" "+ownNodeId;
+        }
+    }
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        nodes.clear();
+        sharedBackingStore.clear();
+    }
+    
+    public HaMgmtNode newNode() throws Exception {
+        HaMgmtNode node = new HaMgmtNode();
+        node.setUp();
+        nodes.add(node);
+        return node;
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        for (HaMgmtNode n: nodes)
+            n.tearDown();
+    }
+
+    protected ManagementContextInternal newLocalManagementContext() {
+        return new LocalManagementContextForTests();
+    }
+
+    protected PersistenceObjectStore newPersistenceObjectStore() {
+        return new InMemoryObjectStore(sharedBackingStore, sharedBackingStoreDates);
+    }
+
+    // TODO refactor above -- routines above this line are shared among HotStandbyTest and SplitBrainTest
+    
+    @Test
+    public void testWarmStandby() throws Exception {
+        HaMgmtNode n1 = newNode();
+        n1.ha.start(HighAvailabilityMode.AUTO);
+        assertEquals(n1.ha.getNodeState(), ManagementNodeState.MASTER);
+        
+        TestApplication app = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        app.start(MutableList.<Location>of());
+        
+        n1.mgmt.getRebindManager().forcePersistNow();
+
+        HaMgmtNode n2 = newNode();
+        n2.ha.start(HighAvailabilityMode.STANDBY);
+        assertEquals(n2.ha.getNodeState(), ManagementNodeState.STANDBY);
+
+        assertEquals(n2.mgmt.getApplications().size(), 0);
+    }
+    
+    // TODO support forcible demotion, and check that a master forcibly demoted 
+    // to warm standby clears its apps, policies, and locations  
+    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/software/webapp/src/main/java/brooklyn/entity/proxy/nginx/UrlMappingImpl.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/main/java/brooklyn/entity/proxy/nginx/UrlMappingImpl.java b/software/webapp/src/main/java/brooklyn/entity/proxy/nginx/UrlMappingImpl.java
index 473a8d5..66b88c0 100644
--- a/software/webapp/src/main/java/brooklyn/entity/proxy/nginx/UrlMappingImpl.java
+++ b/software/webapp/src/main/java/brooklyn/entity/proxy/nginx/UrlMappingImpl.java
@@ -199,10 +199,9 @@ public class UrlMappingImpl extends AbstractGroupImpl implements UrlMapping {
                 }});
             subscriptionHandle2 = subscribe(t, Changeable.MEMBER_REMOVED, new SensorEventListener<Entity>() {
                 @Override public void onEvent(SensorEvent<Entity> event) {
-                    boolean changed = removeMember(event.getValue());
-                    if (changed) {
-                        recomputeAddresses();
-                    }
+                    removeMember(event.getValue());
+                    // recompute, irrespective of change, because framework may have already invoked the removeMember call
+                    recomputeAddresses();
                 }});
             setMembers(t.getChildren(), EntityPredicates.attributeEqualTo(Startable.SERVICE_UP, true));
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/3a8a33a8/software/webapp/src/test/java/brooklyn/entity/proxy/UrlMappingTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/brooklyn/entity/proxy/UrlMappingTest.java b/software/webapp/src/test/java/brooklyn/entity/proxy/UrlMappingTest.java
index 1d680f1..97a7409 100644
--- a/software/webapp/src/test/java/brooklyn/entity/proxy/UrlMappingTest.java
+++ b/software/webapp/src/test/java/brooklyn/entity/proxy/UrlMappingTest.java
@@ -48,6 +48,8 @@ import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
 import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.test.TestUtils;
 import brooklyn.test.entity.TestApplication;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicates;
@@ -110,6 +112,7 @@ public class UrlMappingTest {
     @Test(groups = "Integration")
     public void testTargetMappingsRemovesUnmanagedMember() {
         Iterable<StubAppServer> members = Iterables.filter(cluster.getChildren(), StubAppServer.class);
+        assertEquals(Iterables.size(members), 2);
         StubAppServer target1 = Iterables.get(members, 0);
         StubAppServer target2 = Iterables.get(members, 1);
         
@@ -122,6 +125,11 @@ public class UrlMappingTest {
         assertExpectedTargetsEventually(ImmutableSet.of(target2));
     }
     
+    @Test(groups = "Integration", invocationCount=50)
+    public void testTargetMappingsRemovesUnmanagedMemberManyTimes() {
+        testTargetMappingsRemovesUnmanagedMember();
+    }
+    
     @Test(groups = "Integration")
     public void testTargetMappingsRemovesDownMember() {
         Iterable<StubAppServer> members = Iterables.filter(cluster.getChildren(), StubAppServer.class);


[14/15] git commit: This closes #197

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


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

Branch: refs/heads/master
Commit: 1f2a378bd4a0d74759d79cfd87f27c2b1738b5e8
Parents: 8a1fd77 6ba1639
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Oct 1 18:31:36 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 18:31:36 2014 +0100

----------------------------------------------------------------------
 api/src/main/java/brooklyn/entity/Entity.java   |  10 +-
 .../brooklyn/entity/rebind/RebindContext.java   |   4 +
 .../entity/rebind/RebindExceptionHandler.java   |  19 +-
 .../brooklyn/entity/rebind/RebindManager.java   |  54 +-
 .../brooklyn/entity/rebind/RebindSupport.java   |   2 +-
 .../brooklyn/management/ExecutionContext.java   |   8 +
 .../brooklyn/management/LocationManager.java    |   3 -
 .../brooklyn/management/ManagementContext.java  |  15 +
 api/src/main/java/brooklyn/management/Task.java |   8 +
 .../management/ha/HighAvailabilityManager.java  |  16 +-
 .../management/ha/HighAvailabilityMode.java     |  12 +-
 .../management/ha/ManagementNodeState.java      |  14 +
 .../management/ha/ManagementNodeSyncRecord.java |   2 +
 .../ha/ManagementPlaneSyncRecordPersister.java  |   1 +
 .../mementos/BrooklynMementoPersister.java      |   3 +
 .../brooklyn/enricher/basic/Transformer.java    |   1 +
 .../brooklyn/entity/basic/AbstractEntity.java   | 115 ++--
 .../entity/basic/AbstractGroupImpl.java         |  25 +-
 .../brooklyn/entity/basic/BrooklynTaskTags.java |   4 +
 .../java/brooklyn/entity/basic/Entities.java    |  22 +-
 .../brooklyn/entity/basic/EntityInternal.java   |   6 +-
 .../basic/EntityTransientCopyInternal.java      | 106 ++++
 .../entity/basic/ServiceStateLogic.java         |   6 +-
 .../entity/proxying/EntityProxyImpl.java        | 119 +++-
 .../entity/proxying/InternalEntityFactory.java  |  30 +-
 .../AbstractBrooklynObjectRebindSupport.java    |   3 +-
 .../entity/rebind/BasicEntityRebindSupport.java |   8 +-
 .../rebind/PeriodicDeltaChangeListener.java     |  54 +-
 .../entity/rebind/RebindContextImpl.java        |  12 +
 .../rebind/RebindExceptionHandlerImpl.java      |  37 +-
 .../entity/rebind/RebindManagerImpl.java        | 324 +++++++++--
 .../AbstractBrooklynMementoPersister.java       |   9 +-
 .../BrooklynMementoPersisterToMultiFile.java    |   8 +
 .../BrooklynMementoPersisterToObjectStore.java  |  70 ++-
 .../persister/RetryingMementoSerializer.java    |   4 +
 .../dto/BasicManagementNodeSyncRecord.java      |  22 +-
 .../brooklyn/event/basic/BasicSensorEvent.java  |  20 +-
 .../internal/BrooklynFeatureEnablement.java     |  18 +
 .../location/basic/AbstractLocation.java        |  67 +--
 .../location/basic/LocationInternal.java        |   1 -
 .../management/ha/BasicMasterChooser.java       | 158 +++++-
 .../ha/HighAvailabilityManagerImpl.java         | 241 ++++++--
 .../ha/ManagementPlaneSyncRecordDeltaImpl.java  |  11 +-
 ...ntPlaneSyncRecordPersisterToObjectStore.java |  15 +-
 .../internal/AbstractManagementContext.java     |  24 +-
 .../internal/BrooklynGarbageCollector.java      |   1 +
 .../internal/EntityManagementSupport.java       |  68 ++-
 .../internal/EntityManagerInternal.java         |  15 +
 .../management/internal/LocalAccessManager.java |   1 -
 .../management/internal/LocalEntityManager.java | 246 ++++++--
 .../internal/LocalLocationManager.java          | 171 ++++--
 .../internal/LocalManagementContext.java        |   4 +-
 .../internal/LocationManagerInternal.java       |  40 ++
 .../internal/ManagementTransitionInfo.java      |  30 +-
 .../internal/NonDeploymentEntityManager.java    |  39 ++
 .../internal/NonDeploymentLocationManager.java  |  51 +-
 .../NonDeploymentManagementContext.java         |  67 ++-
 .../util/task/AbstractExecutionContext.java     |   7 +-
 .../util/task/BasicExecutionContext.java        |   7 +-
 .../main/java/brooklyn/util/task/BasicTask.java |   2 +-
 .../java/brooklyn/util/task/ScheduledTask.java  |  18 +
 .../src/main/java/brooklyn/util/task/Tasks.java |  51 ++
 .../entity/rebind/RebindCatalogEntityTest.java  |   5 +-
 .../entity/rebind/RebindEntityTest.java         |   7 +-
 .../entity/rebind/RebindFailuresTest.java       |  33 +-
 .../entity/rebind/RebindTestFixture.java        |   4 +-
 .../brooklyn/entity/rebind/RebindTestUtils.java |  22 +-
 .../rebind/RecordingRebindExceptionHandler.java |   5 -
 ...BrooklynMementoPersisterToMultiFileTest.java |   2 +-
 .../transformer/CompoundTransformerTest.java    |   1 +
 .../ha/HighAvailabilityManagerInMemoryTest.java |   4 +
 .../HighAvailabilityManagerSplitBrainTest.java  |  51 +-
 .../ha/HighAvailabilityManagerTestFixture.java  |  14 +-
 .../brooklyn/management/ha/HotStandbyTest.java  | 564 +++++++++++++++++++
 .../management/ha/MasterChooserTest.java        |  71 ++-
 .../brooklyn/management/ha/WarmStandbyTest.java | 150 +++++
 .../entity/TestApplicationNoEnrichersImpl.java  |  15 -
 .../util/task/ScheduledExecutionTest.java       | 159 +++++-
 .../entity/proxy/nginx/UrlMappingImpl.java      |   7 +-
 .../brooklyn/entity/proxy/UrlMappingTest.java   |   8 +
 .../AbstractWebAppFixtureIntegrationTest.java   |   5 +-
 usage/cli/src/main/java/brooklyn/cli/Main.java  |  12 +-
 .../brooklyn/launcher/BrooklynLauncher.java     |   7 +-
 .../BrooklynLauncherHighAvailabilityTest.java   |  25 +-
 ...lynLauncherRebindToCloudObjectStoreTest.java |   4 -
 .../brooklyn/util/exceptions/Exceptions.java    |  30 +-
 .../util/text/NaturalOrderComparator.java       |   3 +-
 .../main/java/brooklyn/util/time/Duration.java  |  12 +
 88 files changed, 3152 insertions(+), 597 deletions(-)
----------------------------------------------------------------------



[03/15] git commit: add a hot_standby mode, now the default when running HA (although it doesn't do anything, it is a coherent state and the MasterChooser prefers it)

Posted by he...@apache.org.
add a hot_standby mode, now the default when running HA (although it doesn't do anything, it is a coherent state and the MasterChooser prefers it)


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

Branch: refs/heads/master
Commit: c2b05ff274fbba81e9a2fc2544b2b6587d55afae
Parents: 3ef6e25
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Fri Sep 26 13:41:46 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:15 2014 +0100

----------------------------------------------------------------------
 .../management/ha/HighAvailabilityManager.java  |   3 +-
 .../management/ha/HighAvailabilityMode.java     |  12 +-
 .../management/ha/ManagementNodeState.java      |  14 +++
 .../management/ha/BasicMasterChooser.java       | 120 ++++++++++++++-----
 .../ha/HighAvailabilityManagerImpl.java         |  77 ++++++++----
 .../ha/HighAvailabilityManagerInMemoryTest.java |   4 +
 .../HighAvailabilityManagerSplitBrainTest.java  |  34 +++---
 .../ha/HighAvailabilityManagerTestFixture.java  |  10 +-
 .../management/ha/MasterChooserTest.java        |  37 +++++-
 usage/cli/src/main/java/brooklyn/cli/Main.java  |  12 +-
 .../brooklyn/launcher/BrooklynLauncher.java     |   1 +
 .../BrooklynLauncherHighAvailabilityTest.java   |  18 +++
 12 files changed, 260 insertions(+), 82 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java b/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
index 937d25b..d9888f9 100644
--- a/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
+++ b/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
@@ -70,7 +70,8 @@ public interface HighAvailabilityManager {
      * Starts the monitoring of other nodes (and thus potential promotion of this node from standby to master).
      * <p>
      * When this method returns, the status of this node will be set,
-     * either {@link ManagementNodeState#MASTER} if appropriate or {@link ManagementNodeState#STANDBY}.
+     * either {@link ManagementNodeState#MASTER} if appropriate 
+     * or {@link ManagementNodeState#STANDBY} / {@link ManagementNodeState#HOT_STANDBY}.
      *
      * @param startMode mode to start with
      * @throws IllegalStateException if current state of the management-plane doesn't match that desired by {@code startMode} 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/api/src/main/java/brooklyn/management/ha/HighAvailabilityMode.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ha/HighAvailabilityMode.java b/api/src/main/java/brooklyn/management/ha/HighAvailabilityMode.java
index 9a154ca..29f8f7d 100644
--- a/api/src/main/java/brooklyn/management/ha/HighAvailabilityMode.java
+++ b/api/src/main/java/brooklyn/management/ha/HighAvailabilityMode.java
@@ -30,12 +30,22 @@ public enum HighAvailabilityMode {
     AUTO,
     
     /**
-     * Means node must be standby; if there is not already a master then fail fast on startup. 
+     * Means node must be lukewarm standby; if there is not already a master then fail fast on startup.
+     * See {@link ManagementNodeState#STANDBY}. 
      */
     STANDBY,
     
     /**
+     * Means node must be hot standby; if there is not already a master then fail fast on startup.
+     * See {@link ManagementNodeState#HOT_STANDBY}. 
+     */
+    HOT_STANDBY,
+    
+    /**
      * Means node must be master; if there is already a master then fail fast on startup.
+     * See {@link ManagementNodeState#MASTER}.
      */
+    // TODO when multi-master supported we will of course not fail fast on startup when there is already a master;
+    // instead the responsibility for master entities will be divided among masters
     MASTER;
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java b/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
index 00a90a4..d3b0508 100644
--- a/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
+++ b/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
@@ -19,9 +19,23 @@
 package brooklyn.management.ha;
 
 public enum ManagementNodeState {
+    /** @deprecated since 0.7.0 synonym for maintenance (plus, it should have been UK english!) */
     UNINITIALISED,
+    /** node is either coming online, or is in some kind of recovery/transitioning mode */
+    INITIALIZING,
+    
+    /** node is in "lukewarm standby" mode, where it is available to be promoted to master,
+     * but does not have entities loaded and will require some effort to be promoted */
     STANDBY,
+    /** node is acting as read-only proxy */
+    HOT_STANDBY,
+    /** node is running as primary/master, able to manage entities and create new ones */
+    // the semantics are intended to support multi-master here; we could have multiple master nodes,
+    // but we need to look up who is master for any given entity
     MASTER,
+
+    /** node has failed and requires maintenance attention */
     FAILED,
+    /** node has gone away; maintenance not possible */
     TERMINATED;
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java b/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
index 2f82016..78c110a 100644
--- a/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
+++ b/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
@@ -18,18 +18,19 @@
  */
 package brooklyn.management.ha;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import brooklyn.entity.trait.Identifiable;
+import brooklyn.util.collections.MutableList;
 import brooklyn.util.time.Duration;
 
 import com.google.common.annotations.Beta;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
+import com.google.common.annotations.VisibleForTesting;
 
 /**
  * @since 0.7.0
@@ -41,33 +42,69 @@ public abstract class BasicMasterChooser implements MasterChooser {
 
     private static final Logger LOG = LoggerFactory.getLogger(BasicMasterChooser.class);
 
-    // advantage of this over taking most recent is that nodes will agree on this
-    // (where heartbeat timeout is reasonably large)
+    protected static class ScoredRecord<T extends Comparable<T>> implements Identifiable, Comparable<ScoredRecord<T>> {
+        String id;
+        ManagementNodeSyncRecord record;
+        T score;
+        
+        @Override
+        public String getId() {
+            return id;
+        }
+
+        @Override
+        public int compareTo(ScoredRecord<T> o) {
+            return score.compareTo(o.score);
+        }
+    }
+    
+    public ManagementNodeSyncRecord choose(ManagementPlaneSyncRecord memento, Duration heartbeatTimeout, String ownNodeId) {
+        if (LOG.isDebugEnabled()) LOG.debug("Choosing new master from "+memento.getManagementNodes());
+        ManagementNodeSyncRecord me = memento.getManagementNodes().get(ownNodeId);
+        if (me==null) {
+            LOG.warn("Management node details not known when choosing new master: "+memento+" / "+ownNodeId);
+            return null;
+        }
+        Long nowIsh = me.getRemoteTimestamp();
+        if (nowIsh==null) {
+            LOG.warn("Management node for self missing timestamp when choosing new master: "+memento);
+            return null;
+        }
+        
+        List<ScoredRecord<?>> contenders = filterHealthy(memento, heartbeatTimeout, nowIsh);
+        
+        if (!contenders.isEmpty()) {
+            return pick(contenders);
+        } else {
+            LOG.info("No valid management node found for choosing new master: contender="+memento.getManagementNodes());
+            return null;
+        }        
+    }
+
+    /** pick the best contender; argument guaranteed to be non-null and non-empty,
+     * filtered for health reasons */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    protected ManagementNodeSyncRecord pick(List<ScoredRecord<?>> contenders) {
+        ScoredRecord min = null;
+        for (ScoredRecord x: contenders) {
+            if (min==null || x.score.compareTo(min.score)<0) min = x;
+        }
+        return min.record;
+    }
+
     public static class AlphabeticMasterChooser extends BasicMasterChooser {
+        final boolean preferHot;
+        public AlphabeticMasterChooser(boolean preferHot) { this.preferHot = preferHot; }
+        public AlphabeticMasterChooser() { this.preferHot = true; }
         @Override
-        public ManagementNodeSyncRecord choose(ManagementPlaneSyncRecord memento, Duration heartbeatTimeout, String ownNodeId) {
-            if (LOG.isDebugEnabled()) LOG.debug("Choosing new master from "+memento.getManagementNodes());
-            ManagementNodeSyncRecord me = memento.getManagementNodes().get(ownNodeId);
-            if (me==null) {
-                LOG.warn("Management node details not known when choosing new master: "+memento+" / "+ownNodeId);
-                return null;
-            }
-            Long nowIsh = me.getRemoteTimestamp();
-            if (nowIsh==null) {
-                LOG.warn("Management node for self missing timestamp when choosing new master: "+memento);
-                return null;
-            }
-            
-            Map<String, ManagementNodeSyncRecord> contenders = filterHealthy(memento, heartbeatTimeout, nowIsh);
-            
-            if (contenders.size() > 0) {
-                List<String> contenderIds = Lists.newArrayList(contenders.keySet());
-                Collections.sort(contenderIds);
-                return contenders.get(contenderIds.get(0));
-            } else {
-                LOG.info("No valid management node found for choosing new master: contender="+memento.getManagementNodes());
-                return null;
-            }
+        protected String score(ManagementNodeSyncRecord contender) {
+            if (!preferHot)
+                return contender.getNodeId();
+            // simple prefix with the rating
+            String state = (contender.getStatus()==ManagementNodeState.MASTER ? "1" :
+                contender.getStatus()==ManagementNodeState.HOT_STANDBY ? "2" :
+                contender.getStatus()==ManagementNodeState.STANDBY ? "3" : "9");
+            return state + ":" + contender.getNodeId();
         }
     }
     
@@ -75,11 +112,11 @@ public abstract class BasicMasterChooser implements MasterChooser {
      * Filters the {@link ManagementPlaneSyncRecord#getManagementNodes()} to only those in an appropriate state, 
      * and with heartbeats that have not timed out.
      */
-    protected Map<String, ManagementNodeSyncRecord> filterHealthy(ManagementPlaneSyncRecord memento, Duration heartbeatTimeout, long nowIsh) {
+    protected List<ScoredRecord<?>> filterHealthy(ManagementPlaneSyncRecord memento, Duration heartbeatTimeout, long nowIsh) {
         long oldestAcceptableTimestamp = nowIsh - heartbeatTimeout.toMilliseconds();
-        Map<String, ManagementNodeSyncRecord> contenders = Maps.newLinkedHashMap();
+        List<ScoredRecord<?>> contenders = MutableList.of();
         for (ManagementNodeSyncRecord contender : memento.getManagementNodes().values()) {
-            boolean statusOk = (contender.getStatus() == ManagementNodeState.STANDBY || contender.getStatus() == ManagementNodeState.MASTER);
+            boolean statusOk = (contender.getStatus() == ManagementNodeState.STANDBY || contender.getStatus() == ManagementNodeState.HOT_STANDBY || contender.getStatus() == ManagementNodeState.MASTER);
             Long remoteTimestamp = contender.getRemoteTimestamp();
             boolean heartbeatOk;
             if (remoteTimestamp==null) {
@@ -92,10 +129,29 @@ public abstract class BasicMasterChooser implements MasterChooser {
                 heartbeatOk = remoteTimestamp >= oldestAcceptableTimestamp;
             }
             if (statusOk && heartbeatOk) {
-                contenders.put(contender.getNodeId(), contender);
+                contenders.add(newScoredRecord(contender));
             }
             if (LOG.isTraceEnabled()) LOG.trace("Filtering choices of new master: contender="+contender+"; statusOk="+statusOk+"; heartbeatOk="+heartbeatOk);
         }
         return contenders;
     }
+
+    @VisibleForTesting
+    protected List<ScoredRecord<?>> sort(List<ScoredRecord<?>> input) {
+        ArrayList<ScoredRecord<?>> copy = new ArrayList<ScoredRecord<?>>(input);
+        Collections.sort(copy);
+        return copy;
+    }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    protected ScoredRecord<?> newScoredRecord(ManagementNodeSyncRecord contender) {
+        ScoredRecord r = new ScoredRecord();
+        r.id = contender.getNodeId();
+        r.record = contender;
+        r.score = score(contender);
+        return r;
+    }
+
+    protected abstract Comparable<?> score(ManagementNodeSyncRecord contender);
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/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 fdfa879..2d7bf9f 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -123,7 +123,8 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     private volatile Task<?> pollingTask;
     private volatile boolean disabled;
     private volatile boolean running;
-    private volatile ManagementNodeState nodeState = ManagementNodeState.UNINITIALISED;
+    private volatile ManagementNodeState nodeState = ManagementNodeState.INITIALIZING;
+    private volatile boolean nodeStateTransitionComplete = false;
 
     public HighAvailabilityManagerImpl(ManagementContextInternal managementContext) {
         this.managementContext = managementContext;
@@ -202,6 +203,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     public void start(HighAvailabilityMode startMode) {
         ownNodeId = managementContext.getManagementNodeId();
         nodeState = ManagementNodeState.STANDBY;
+        nodeStateTransitionComplete = true;
         running = true;
         
         // TODO Small race in that we first check, and then we'll do checkMaster() on first poll,
@@ -212,13 +214,13 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         case AUTO:
             // don't care; let's start and see if we promote ourselves
             publishAndCheck(true);
-            if (nodeState == ManagementNodeState.STANDBY) {
+            if (nodeState == ManagementNodeState.STANDBY || nodeState == ManagementNodeState.HOT_STANDBY) {
                 String masterNodeId = getManagementPlaneSyncState().getMasterNodeId();
                 ManagementNodeSyncRecord masterNodeDetails = getManagementPlaneSyncState().getManagementNodes().get(masterNodeId);
-                LOG.info("Management node "+ownNodeId+" started as HA STANDBY autodetected, master is "+masterNodeId+
+                LOG.info("Management node "+ownNodeId+" started as HA " + nodeState + " autodetected, master is "+masterNodeId +
                     (masterNodeDetails==null || masterNodeDetails.getUri()==null ? " (no url)" : " at "+masterNodeDetails.getUri()));
             } else {
-                LOG.info("Management node "+ownNodeId+" started as HA MASTER autodetected");
+                LOG.info("Management node "+ownNodeId+" starting as HA MASTER autodetected");
             }
             break;
         case MASTER:
@@ -230,9 +232,10 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
             }
             break;
         case STANDBY:
+        case HOT_STANDBY:
             if (existingMaster != null) {
                 publishAndCheck(true);
-                LOG.info("Management node "+ownNodeId+" started as HA STANDBY explicitly, status "+nodeState);
+                LOG.info("Management node "+ownNodeId+" started as "+startMode+" explicitly, status "+nodeState);
             } else {
                 throw new IllegalStateException("No existing master; cannot start as standby");
             }
@@ -241,6 +244,17 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
             throw new IllegalStateException("Unexpected high availability start-mode "+startMode+" for "+this);
         }
         
+        if (nodeState==ManagementNodeState.STANDBY && startMode!=HighAvailabilityMode.STANDBY) {
+            // if standby not explicitly requested, we need promote to hot standby
+            // TODO may want a flag which enables/disables hot standby; for now, always enabled unless told to start in standby
+            nodeStateTransitionComplete = false;
+            // inform the world that we are transitioning (not eligible for promotion while going in to hot standby)
+            publishHealth();
+            attemptHotStandby();
+            publishHealth();
+        }
+        
+        nodeStateTransitionComplete = true;
         registerPollTask();
     }
 
@@ -263,8 +277,23 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         }
     }
     
+    /** returns the node state this node is trying to be in */
+    public ManagementNodeState getTransitionTargetNodeState() {
+        return nodeState;
+    }
+    
+    @SuppressWarnings("deprecation")
     @Override
     public ManagementNodeState getNodeState() {
+        if (nodeState==ManagementNodeState.FAILED) return nodeState;
+        // if target is master then we claim already being master, to prevent other nodes from taking it
+        // (we may fail subsequently of course)
+        if (nodeState==ManagementNodeState.MASTER) return nodeState;
+        
+        // for backwards compatibility; remove in 0.8.0
+        if (nodeState==ManagementNodeState.UNINITIALISED) return ManagementNodeState.INITIALIZING;
+        
+        if (!nodeStateTransitionComplete) return ManagementNodeState.INITIALIZING;
         return nodeState;
     }
 
@@ -366,12 +395,6 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         if (LOG.isTraceEnabled()) LOG.trace("Published management-node health: {}", memento);
     }
     
-    protected ManagementNodeState toNodeStateForPersistence(ManagementNodeState nodeState) {
-        // uninitialized is set as null - TODO confirm that's necessary; nicer if we don't need this method at all
-        if (nodeState == ManagementNodeState.UNINITIALISED) return null;
-        return nodeState;
-    }
-    
     protected boolean isHeartbeatOk(ManagementNodeSyncRecord masterNode, ManagementNodeSyncRecord meNode) {
         if (masterNode==null) return false;
         if (meNode==null) {
@@ -455,7 +478,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         
         if (demotingSelfInFavourOfOtherMaster) {
             LOG.debug("Master-change for this node only, demoting "+ownNodeRecord.toVerboseString()+" in favour of official master "+newMasterNodeRecord.toVerboseString());
-            demoteToStandby();
+            demoteToStandby(true);
             return;
         }
         
@@ -520,22 +543,29 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     
     protected void demoteToFailed() {
         nodeState = ManagementNodeState.FAILED;
-        onDemotion();
+        onDemotionStopTasks();
+        nodeStateTransitionComplete = true;
         publishDemotionFromMaster(true);
     }
 
-    protected void demoteToStandby() {
+    protected void demoteToStandby(boolean hot) {
         if (!running) {
             LOG.warn("Ignoring demote-from-master request, as HighAvailabilityManager is no longer running");
             return;
         }
 
+        nodeStateTransitionComplete = false;
         nodeState = ManagementNodeState.STANDBY;
-        onDemotion();
+        onDemotionStopTasks();
+        if (hot) {
+            publishDemotionFromMaster(false);
+            attemptHotStandby();
+        }
+        nodeStateTransitionComplete = true;
         publishDemotionFromMaster(false);
     }
     
-    protected void onDemotion() {
+    protected void onDemotionStopTasks() {
         managementContext.getRebindManager().stop();
         for (Application app: managementContext.getApplications())
             Entities.unmanage(app);
@@ -569,6 +599,13 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
             LOG.info("Cancelled "+tasks+" tasks on demotion");
     }
 
+    /** starts hot standby, in foreground; the caller is responsible for publishing health afterwards.
+     * @return whether hot standby was possible (if not, errors should be stored elsewhere) */
+    protected boolean attemptHotStandby() {
+        nodeState = ManagementNodeState.HOT_STANDBY;
+        return true;
+    }
+    
     /**
      * @param reportCleanedState - if true, the record for this mgmt node will be replaced with the
      * actual current status known in this JVM (may be more recent than what is persisted);
@@ -580,7 +617,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
             // if HA is disabled, then we are the only node - no persistence; just load a memento to describe this node
             Builder builder = ManagementPlaneSyncRecordImpl.builder()
                 .node(createManagementNodeSyncRecord(true));
-            if (getNodeState() == ManagementNodeState.MASTER) {
+            if (getTransitionTargetNodeState() == ManagementNodeState.MASTER) {
                 builder.masterNodeId(ownNodeId);
             }
             return builder.build();
@@ -610,7 +647,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
                         .masterNodeId(result.getMasterNodeId())
                         .nodes(allNodes);
                     builder.node(me);
-                    if (getNodeState() == ManagementNodeState.MASTER) {
+                    if (getTransitionTargetNodeState() == ManagementNodeState.MASTER) {
                         builder.masterNodeId(ownNodeId);
                     }
                     result = builder.build();
@@ -631,7 +668,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         brooklyn.entity.rebind.plane.dto.BasicManagementNodeSyncRecord.Builder builder = BasicManagementNodeSyncRecord.builder()
                 .brooklynVersion(BrooklynVersion.get())
                 .nodeId(ownNodeId)
-                .status(toNodeStateForPersistence(getNodeState()))
+                .status(getNodeState())
                 .localTimestamp(timestamp)
                 .uri(managementContext.getManagementNodeUri().orNull());
         if (useLocalTimestampAsRemoteTimestamp)
@@ -664,7 +701,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         @Override
         public ManagementNodeSyncRecord apply(@Nullable ManagementNodeSyncRecord input) {
             if (input == null) return null;
-            if (!(input.getStatus() == ManagementNodeState.STANDBY || input.getStatus() == ManagementNodeState.MASTER)) return input;
+            if (!(input.getStatus() == ManagementNodeState.STANDBY || input.getStatus() == ManagementNodeState.HOT_STANDBY || input.getStatus() == ManagementNodeState.MASTER)) return input;
             if (isHeartbeatOk(input, referenceNode)) return input;
             return BasicManagementNodeSyncRecord.builder()
                     .from(input)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerInMemoryTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerInMemoryTest.java b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerInMemoryTest.java
index 9166202..dfabd0b 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerInMemoryTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerInMemoryTest.java
@@ -29,5 +29,9 @@ public class HighAvailabilityManagerInMemoryTest extends HighAvailabilityManager
     protected PersistenceObjectStore newPersistenceObjectStore() {
         return new InMemoryObjectStore();
     }
+    
+    public void testGetManagementPlaneStatus() throws Exception {
+        super.testGetManagementPlaneStatus();
+    }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/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 f83816e..dbdf3d3 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
@@ -20,6 +20,7 @@ package brooklyn.management.ha;
 
 import static org.testng.Assert.assertEquals;
 
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -201,7 +202,7 @@ public class HighAvailabilityManagerSplitBrainTest {
         log.info(n2+" HA: "+memento2);
         assertEquals(memento2.getMasterNodeId(), n1.ownNodeId);
         assertEquals(memento2.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.MASTER);
-        assertEquals(memento2.getManagementNodes().get(n2.ownNodeId).getStatus(), ManagementNodeState.STANDBY);
+        assertEquals(memento2.getManagementNodes().get(n2.ownNodeId).getStatus(), ManagementNodeState.HOT_STANDBY);
         assertEquals(memento2.getManagementNodes().get(n1.ownNodeId).getRemoteTimestamp(), time0);
         assertEquals(memento2.getManagementNodes().get(n2.ownNodeId).getRemoteTimestamp(), time0);
         
@@ -254,7 +255,7 @@ public class HighAvailabilityManagerSplitBrainTest {
         log.info(n1+" HA now: "+memento1b);
         
         // n1 comes back and demotes himself 
-        assertEquals(memento1b.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.STANDBY);
+        assertEquals(memento1b.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.HOT_STANDBY);
         assertEquals(memento1b.getManagementNodes().get(n2.ownNodeId).getStatus(), ManagementNodeState.MASTER);
         assertEquals(memento1b.getMasterNodeId(), n2.ownNodeId);
         assertEquals(memento1b.getManagementNodes().get(n1.ownNodeId).getRemoteTimestamp(), time2);
@@ -263,7 +264,7 @@ public class HighAvailabilityManagerSplitBrainTest {
         // n2 now sees itself as master, with n1 in standby again
         ManagementPlaneSyncRecord memento2c = n2.ha.getManagementPlaneSyncState();
         log.info(n2+" HA now: "+memento2c);
-        assertEquals(memento2c.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.STANDBY);
+        assertEquals(memento2c.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.HOT_STANDBY);
         assertEquals(memento2c.getManagementNodes().get(n2.ownNodeId).getStatus(), ManagementNodeState.MASTER);
         assertEquals(memento2c.getMasterNodeId(), n2.ownNodeId);
         assertEquals(memento2c.getManagementNodes().get(n1.ownNodeId).getRemoteTimestamp(), time2);
@@ -314,28 +315,33 @@ public class HighAvailabilityManagerSplitBrainTest {
             Asserts.succeedsEventually(new Runnable() {
                 @Override public void run() {
                     ManagementPlaneSyncRecord memento = nodes.get(0).ha.getManagementPlaneSyncState();
-                    int masters=0, standbys=0, savedMasters=0, savedStandbys=0;
+                    List<ManagementNodeState> counts = MutableList.of(), savedCounts = MutableList.of();
                     for (HaMgmtNode n: nodes) {
-                        if (n.ha.getNodeState()==ManagementNodeState.MASTER) masters++;
-                        if (n.ha.getNodeState()==ManagementNodeState.STANDBY) standbys++;
+                        counts.add(n.ha.getNodeState());
                         ManagementNodeSyncRecord m = memento.getManagementNodes().get(n.ownNodeId);
                         if (m!=null) {
-                            if (m.getStatus()==ManagementNodeState.MASTER) savedMasters++;
-                            if (m.getStatus()==ManagementNodeState.STANDBY) savedStandbys++;
+                            savedCounts.add(m.getStatus());
                         }
                     }
-                    log.info("while starting "+nodes.size()+" nodes: "+masters+" M + "+standbys+" zzz; "
+                    log.info("while starting "+nodes.size()+" nodes: "
+                        +Collections.frequency(counts, ManagementNodeState.MASTER)+" M + "
+                        +Collections.frequency(counts, ManagementNodeState.HOT_STANDBY)+" hot + "
+                        +Collections.frequency(counts, ManagementNodeState.STANDBY)+" warm + "
+                        +Collections.frequency(counts, ManagementNodeState.INITIALIZING)+" init; "
                         + memento.getManagementNodes().size()+" saved, "
-                        + memento.getMasterNodeId()+" master, "+savedMasters+" M + "+savedStandbys+" zzz");
+                        +Collections.frequency(savedCounts, ManagementNodeState.MASTER)+" M + "
+                        +Collections.frequency(savedCounts, ManagementNodeState.HOT_STANDBY)+" hot + "
+                        +Collections.frequency(savedCounts, ManagementNodeState.STANDBY)+" warm + "
+                        +Collections.frequency(savedCounts, ManagementNodeState.INITIALIZING)+" init");
 
                     if (timer.isRunning() && Duration.of(timer).compareTo(Duration.TEN_SECONDS)>0) {
                         log.warn("we seem to have a problem stabilizing");  //handy place to set a suspend-VM breakpoint!
                         timer.stop();
                     }
-                    assertEquals(masters, 1);
-                    assertEquals(standbys, nodes.size()-1);
-                    assertEquals(savedMasters, 1);
-                    assertEquals(savedStandbys, nodes.size()-1);
+                    assertEquals(Collections.frequency(counts, ManagementNodeState.MASTER), 1);
+                    assertEquals(Collections.frequency(counts, ManagementNodeState.HOT_STANDBY), nodes.size()-1);
+                    assertEquals(Collections.frequency(savedCounts, ManagementNodeState.MASTER), 1);
+                    assertEquals(Collections.frequency(savedCounts, ManagementNodeState.HOT_STANDBY), nodes.size()-1);
                 }});
         } catch (Throwable t) {
             log.warn("Failed to stabilize (rethrowing): "+t, t);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/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 e0288e3..a657636 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
@@ -99,7 +99,7 @@ public abstract class HighAvailabilityManagerTestFixture {
                 .setRemoteTicker(getRemoteTicker())
                 .setPersister(persister);
         persister.delta(ManagementPlaneSyncRecordDeltaImpl.builder()
-            .node(newManagerMemento(ownNodeId, ManagementNodeState.STANDBY))
+            .node(newManagerMemento(ownNodeId, ManagementNodeState.HOT_STANDBY))
             .build());
 
     }
@@ -142,7 +142,7 @@ public abstract class HighAvailabilityManagerTestFixture {
     // next poll fixes it.
     public void testPromotes() throws Exception {
         persister.delta(ManagementPlaneSyncRecordDeltaImpl.builder()
-                .node(newManagerMemento(ownNodeId, ManagementNodeState.STANDBY))
+                .node(newManagerMemento(ownNodeId, ManagementNodeState.HOT_STANDBY))
                 .node(newManagerMemento("node1", ManagementNodeState.MASTER))
                 .setMaster("node1")
                 .build());
@@ -160,7 +160,7 @@ public abstract class HighAvailabilityManagerTestFixture {
     @Test(groups="Integration") // because one second wait in succeedsContinually
     public void testDoesNotPromoteIfMasterTimeoutNotExpired() throws Exception {
         persister.delta(ManagementPlaneSyncRecordDeltaImpl.builder()
-                .node(newManagerMemento(ownNodeId, ManagementNodeState.STANDBY))
+                .node(newManagerMemento(ownNodeId, ManagementNodeState.HOT_STANDBY))
                 .node(newManagerMemento("node1", ManagementNodeState.MASTER))
                 .setMaster("node1")
                 .build());
@@ -211,7 +211,7 @@ public abstract class HighAvailabilityManagerTestFixture {
     @Test
     public void testGetManagementPlaneSyncStateInfersTimedOutNodeAsFailed() throws Exception {
         persister.delta(ManagementPlaneSyncRecordDeltaImpl.builder()
-                .node(newManagerMemento(ownNodeId, ManagementNodeState.STANDBY))
+                .node(newManagerMemento(ownNodeId, ManagementNodeState.HOT_STANDBY))
                 .node(newManagerMemento("node1", ManagementNodeState.MASTER))
                 .setMaster("node1")
                 .build());
@@ -220,7 +220,7 @@ public abstract class HighAvailabilityManagerTestFixture {
         
         ManagementPlaneSyncRecord state = manager.getManagementPlaneSyncState();
         assertEquals(state.getManagementNodes().get("node1").getStatus(), ManagementNodeState.MASTER);
-        assertEquals(state.getManagementNodes().get(ownNodeId).getStatus(), ManagementNodeState.STANDBY);
+        assertEquals(state.getManagementNodes().get(ownNodeId).getStatus(), ManagementNodeState.HOT_STANDBY);
         
         // Simulate passage of time; ticker used by this HA-manager so it will "correctly" publish
         // its own heartbeat with the new time; but node1's record is now out-of-date.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java b/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
index 23acc5c..6f8ba48 100644
--- a/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
+++ b/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
@@ -21,15 +21,20 @@ package brooklyn.management.ha;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 
+import java.util.List;
+
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
 import brooklyn.BrooklynVersion;
+import brooklyn.entity.basic.EntityFunctions;
 import brooklyn.entity.rebind.plane.dto.BasicManagementNodeSyncRecord;
 import brooklyn.management.ha.BasicMasterChooser.AlphabeticMasterChooser;
+import brooklyn.management.ha.BasicMasterChooser.ScoredRecord;
 import brooklyn.util.time.Duration;
 
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 
 public class MasterChooserTest {
 
@@ -67,18 +72,42 @@ public class MasterChooserTest {
         memento.addNode(newManagerMemento("node2", ManagementNodeState.STANDBY, now - 20*1000));
         memento.addNode(newManagerMemento("node3", ManagementNodeState.STANDBY, now));
         Duration heartbeatTimeout = Duration.THIRTY_SECONDS;
-        assertEquals(chooser.filterHealthy(memento, heartbeatTimeout, now).keySet(), ImmutableSet.of("node2", "node3"));
+        assertEquals(getIds(chooser.sort(chooser.filterHealthy(memento, heartbeatTimeout, now))), ImmutableList.of("node2", "node3"));
+    }
+    
+    protected static List<String> getIds(List<ScoredRecord<?>> filterHealthy) {
+        return ImmutableList.copyOf(Iterables.transform(filterHealthy, EntityFunctions.id()));
+    }
+
+    @Test
+    public void testFiltersOutByStatusNotPreferringMaster() throws Exception {
+        assertEquals(doTestFiltersOutByStatus(false, false), ImmutableList.of("node4", "node5"));
+    }
+    @Test
+    public void testFiltersOutByStatusPreferringMaster() throws Exception {
+        assertEquals(doTestFiltersOutByStatus(true, false), ImmutableList.of("node5", "node4"));
     }
     
     @Test
-    public void testFiltersOutByStatus() throws Exception {
+    public void testFiltersOutByStatusNotPreferringHot() throws Exception {
+        assertEquals(doTestFiltersOutByStatus(false, true), ImmutableList.of("node4", "node5", "node6"));
+    }
+    @Test
+    public void testFiltersOutByStatusPreferringHot() throws Exception {
+        assertEquals(doTestFiltersOutByStatus(true, true), ImmutableList.of("node5", "node6", "node4"));
+    }
+    
+    protected List<String> doTestFiltersOutByStatus(boolean preferHot, boolean includeHot) throws Exception {
+        chooser = new AlphabeticMasterChooser(preferHot);
         memento.addNode(newManagerMemento("node1", ManagementNodeState.FAILED, now));
         memento.addNode(newManagerMemento("node2", ManagementNodeState.TERMINATED, now));
         memento.addNode(newManagerMemento("node3", null, now));
         memento.addNode(newManagerMemento("node4",  ManagementNodeState.STANDBY, now));
         memento.addNode(newManagerMemento("node5", ManagementNodeState.MASTER, now));
+        if (includeHot)
+            memento.addNode(newManagerMemento("node6",  ManagementNodeState.HOT_STANDBY, now));
         Duration heartbeatTimeout = Duration.THIRTY_SECONDS;
-        assertEquals(chooser.filterHealthy(memento, heartbeatTimeout, now).keySet(), ImmutableSet.of("node4", "node5"));
+        return getIds(chooser.sort(chooser.filterHealthy(memento, heartbeatTimeout, now)));
     }
     
     private ManagementNodeSyncRecord newManagerMemento(String nodeId, ManagementNodeState status, long timestamp) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/usage/cli/src/main/java/brooklyn/cli/Main.java
----------------------------------------------------------------------
diff --git a/usage/cli/src/main/java/brooklyn/cli/Main.java b/usage/cli/src/main/java/brooklyn/cli/Main.java
index 093f307..681e7a0 100644
--- a/usage/cli/src/main/java/brooklyn/cli/Main.java
+++ b/usage/cli/src/main/java/brooklyn/cli/Main.java
@@ -298,16 +298,18 @@ public class Main extends AbstractMain {
         protected final static String HA_OPTION_AUTO = "auto";
         protected final static String HA_OPTION_MASTER = "master";
         protected final static String HA_OPTION_STANDBY = "standby";
-        static { Enums.checkAllEnumeratedIgnoreCase(HighAvailabilityMode.class, HA_OPTION_AUTO, HA_OPTION_DISABLED, HA_OPTION_MASTER, HA_OPTION_STANDBY); }
+        protected final static String HA_OPTION_HOT_STANDBY = "hot_standby";
+        static { Enums.checkAllEnumeratedIgnoreCase(HighAvailabilityMode.class, HA_OPTION_AUTO, HA_OPTION_DISABLED, HA_OPTION_MASTER, HA_OPTION_STANDBY, HA_OPTION_HOT_STANDBY); }
         
-        @Option(name = { HA_OPTION }, allowedValues = { HA_OPTION_DISABLED, HA_OPTION_AUTO, HA_OPTION_MASTER, HA_OPTION_STANDBY },
+        @Option(name = { HA_OPTION }, allowedValues = { HA_OPTION_DISABLED, HA_OPTION_AUTO, HA_OPTION_MASTER, HA_OPTION_STANDBY, HA_OPTION_HOT_STANDBY },
                 title = "high availability mode",
                 description =
                         "The high availability mode. Possible values are: \n"+
                         "disabled: management node works in isolation - will not cooperate with any other standby/master nodes in management plane; \n"+
                         "auto: will look for other management nodes, and will allocate itself as standby or master based on other nodes' states; \n"+
                         "master: will startup as master - if there is already a master then fails immediately; \n"+
-                        "standby: will start up as standby - if there is not already a master then fails immediately")
+                        "standby: will start up as lukewarm standby - if there is not already a master then fails immediately; \n"+
+                        "hot_standby: will start up as hot standby - if there is not already a master then fails immediately")
         public String highAvailability = HA_OPTION_AUTO;
 
         @VisibleForTesting
@@ -456,8 +458,8 @@ public class Main extends AbstractMain {
                     if (highAvailabilityMode.get() == HighAvailabilityMode.AUTO)
                         return HighAvailabilityMode.DISABLED;
                     throw new FatalConfigurationRuntimeException("Cannot specify highAvailability when persistence is disabled");
-                } else if (persistMode == PersistMode.CLEAN && highAvailabilityMode.get() == HighAvailabilityMode.STANDBY) {
-                    throw new FatalConfigurationRuntimeException("Cannot specify highAvailability STANDBY when persistence is CLEAN");
+                } else if (persistMode == PersistMode.CLEAN && (highAvailabilityMode.get() == HighAvailabilityMode.STANDBY || highAvailabilityMode.get() == HighAvailabilityMode.HOT_STANDBY)) {
+                    throw new FatalConfigurationRuntimeException("Cannot specify highAvailability "+highAvailabilityMode.get()+" when persistence is CLEAN");
                 }
             }
             return highAvailabilityMode.get();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
----------------------------------------------------------------------
diff --git a/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java b/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
index dd5db0e..47d80a4 100644
--- a/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
+++ b/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
@@ -758,6 +758,7 @@ public class BrooklynLauncher {
                 case AUTO:
                 case MASTER:
                 case STANDBY:
+                case HOT_STANDBY:
                     startMode = highAvailabilityMode;
                     break;
                 case DISABLED:

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c2b05ff2/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
----------------------------------------------------------------------
diff --git a/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java b/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
index 949638f..419124d 100644
--- a/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
+++ b/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
@@ -216,6 +216,24 @@ public class BrooklynLauncherHighAvailabilityTest {
         }
     }
     
+    @Test
+    public void testHighAvailabilityHotStandbyModeFailsIfNoExistingMaster() throws Exception {
+        try {
+            primary = BrooklynLauncher.newInstance();
+            primary.webconsole(false)
+                    .brooklynProperties(LocalManagementContextForTests.setEmptyCatalogAsDefault(BrooklynProperties.Factory.newEmpty()))
+                    .highAvailabilityMode(HighAvailabilityMode.HOT_STANDBY)
+                    .persistMode(PersistMode.AUTO)
+                    .persistenceDir(persistenceDir)
+                    .persistPeriod(Duration.millis(10))
+                    .application(EntitySpec.create(TestApplication.class))
+                    .start();
+            fail();
+        } catch (IllegalStateException e) {
+            // success
+        }
+    }
+    
     private void assertOnlyApp(ManagementContext managementContext, Class<? extends Application> expectedType) {
         assertEquals(managementContext.getApplications().size(), 1, "apps="+managementContext.getApplications());
         assertNotNull(Iterables.find(managementContext.getApplications(), Predicates.instanceOf(TestApplication.class), null), "apps="+managementContext.getApplications());


[10/15] git commit: Ensure BrooklynObject management, remanagement, and unmanagement, by replacing Location instances, refreshing the catalog, and swapping out the delegate but keeping the entity proxy.

Posted by he...@apache.org.
Ensure BrooklynObject management, remanagement, and unmanagement, by replacing Location instances, refreshing the catalog, and swapping out the delegate but keeping the entity proxy.


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

Branch: refs/heads/master
Commit: 4c866cb8767b28752a1e871bd2cd123f7e5a776a
Parents: a1d83ee
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Tue Sep 30 23:03:22 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:16 2014 +0100

----------------------------------------------------------------------
 api/src/main/java/brooklyn/entity/Entity.java   |   3 +-
 .../brooklyn/entity/rebind/RebindManager.java   |  14 +-
 .../brooklyn/management/LocationManager.java    |   3 -
 .../brooklyn/entity/basic/AbstractEntity.java   | 106 ++++++-----
 .../entity/basic/AbstractGroupImpl.java         |  25 +--
 .../entity/proxying/EntityProxyImpl.java        |  70 ++++---
 .../entity/rebind/BasicEntityRebindSupport.java |   8 +-
 .../rebind/RebindExceptionHandlerImpl.java      |   2 +-
 .../entity/rebind/RebindManagerImpl.java        |  82 ++++++--
 .../internal/BrooklynFeatureEnablement.java     |  11 ++
 .../location/basic/AbstractLocation.java        |  67 +++----
 .../location/basic/LocationInternal.java        |   1 -
 .../internal/EntityManagementSupport.java       |  65 ++++---
 .../internal/EntityManagerInternal.java         |  22 ++-
 .../management/internal/LocalAccessManager.java |   1 -
 .../management/internal/LocalEntityManager.java | 190 +++++++++++++++----
 .../internal/LocalLocationManager.java          | 165 ++++++++++++----
 .../internal/LocalManagementContext.java        |   4 +-
 .../internal/LocationManagerInternal.java       |  40 ++++
 .../internal/ManagementTransitionInfo.java      |  30 ++-
 .../internal/NonDeploymentEntityManager.java    |  48 ++++-
 .../internal/NonDeploymentLocationManager.java  |  51 ++++-
 .../NonDeploymentManagementContext.java         |   2 +-
 .../brooklyn/management/ha/HotStandbyTest.java  |  50 +++--
 24 files changed, 771 insertions(+), 289 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/api/src/main/java/brooklyn/entity/Entity.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/Entity.java b/api/src/main/java/brooklyn/entity/Entity.java
index 9ff131e..8e32dcd 100644
--- a/api/src/main/java/brooklyn/entity/Entity.java
+++ b/api/src/main/java/brooklyn/entity/Entity.java
@@ -127,7 +127,8 @@ public interface Entity extends BrooklynObject {
     
     /** 
      * Creates an {@link Entity} from the given spec and adds it, setting this entity as the parent,
-     * returning the added child. */
+     * returning the added child. The added child is not yet managed, even if the parent is managed,
+     * so if adding post-management an explicit call to manage the child will be needed. */
     <T extends Entity> T addChild(EntitySpec<T> spec);
     
     /** 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
index 6f15599..9e62fa4 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
@@ -29,6 +29,7 @@ import brooklyn.mementos.BrooklynMementoPersister;
 import brooklyn.mementos.BrooklynMementoRawData;
 import brooklyn.util.time.Duration;
 
+import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
 
 /**
@@ -58,13 +59,18 @@ public interface RebindManager {
     public BrooklynMementoPersister getPersister();
 
     /** @deprecated since 0.7; use {@link #rebind(ClassLoader, RebindExceptionHandler, ManagementNodeState)} */ @Deprecated
-    public List<Application> rebind() throws IOException;
+    public List<Application> rebind();
     
     /** @deprecated since 0.7; use {@link #rebind(ClassLoader, RebindExceptionHandler, ManagementNodeState)} */ @Deprecated
-    public List<Application> rebind(ClassLoader classLoader) throws IOException;
+    public List<Application> rebind(ClassLoader classLoader);
     /** @deprecated since 0.7; use {@link #rebind(ClassLoader, RebindExceptionHandler, ManagementNodeState)} */ @Deprecated
-    public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler) throws IOException;
-    public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler, ManagementNodeState mode) throws IOException;
+    public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler);
+    /** causes this management context to rebind, loading data from the given backing store.
+     * use wisely, as this can cause local entities to be completely lost, or will throw in many other situations.
+     * in general it may be invoked for a new node becoming {@link ManagementNodeState#MASTER} 
+     * or periodically for a node in {@link ManagementNodeState#HOT_STANDBY}. */
+    @Beta
+    public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler, ManagementNodeState mode);
 
     public BrooklynMementoRawData retrieveMementoRawData() throws IOException;
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/api/src/main/java/brooklyn/management/LocationManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/LocationManager.java b/api/src/main/java/brooklyn/management/LocationManager.java
index 4708474..c871aab 100644
--- a/api/src/main/java/brooklyn/management/LocationManager.java
+++ b/api/src/main/java/brooklyn/management/LocationManager.java
@@ -21,10 +21,7 @@ package brooklyn.management;
 import java.util.Collection;
 import java.util.Map;
 
-import javax.annotation.Nullable;
-
 import brooklyn.location.Location;
-import brooklyn.location.LocationDefinition;
 import brooklyn.location.LocationSpec;
 
 /**

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/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 3c3c65f..de00c8a 100644
--- a/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
+++ b/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
@@ -53,6 +53,7 @@ import brooklyn.event.basic.AttributeSensorAndConfigKey;
 import brooklyn.event.basic.BasicNotificationSensor;
 import brooklyn.event.feed.AbstractFeed;
 import brooklyn.event.feed.ConfigToAttributes;
+import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.internal.storage.BrooklynStorage;
 import brooklyn.internal.storage.Reference;
 import brooklyn.internal.storage.impl.BasicReference;
@@ -359,8 +360,14 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
                 (o instanceof Entity && Objects.equal(getId(), ((Entity)o).getId()));
     }
     
+    /** internal use only */ @Beta
     public void setProxy(Entity proxy) {
-        if (selfProxy != null) throw new IllegalStateException("Proxy is already set; cannot reset proxy for "+toString());
+        if (selfProxy != null) 
+            throw new IllegalStateException("Proxy is already set; cannot reset proxy for "+toString());
+        resetProxy(proxy);
+    }
+    /** internal use only */ @Beta
+    public void resetProxy(Entity proxy) {
         selfProxy = checkNotNull(proxy, "proxy");
     }
     
@@ -403,53 +410,55 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
         getManagementSupport().setManagementContext(managementContext);
         entityType.setName(getEntityTypeName());
         if (displayNameAutoGenerated) displayName.set(getEntityType().getSimpleName()+":"+Strings.maxlen(getId(), 4));
-        
-        Entity oldParent = parent.get();
-        Set<Group> oldGroups = groups;
-        Set<Entity> oldChildren = children;
-        List<Location> oldLocations = locations.get();
-        EntityConfigMap oldConfig = configsInternal;
-        AttributeMap oldAttribs = attributesInternal;
-        long oldCreationTimeUtc = creationTimeUtc.get();
-        String oldDisplayName = displayName.get();
-        String oldIconUrl = iconUrl.get();
-        
-        parent = managementContext.getStorage().getReference(getId()+"-parent");
-        groups = SetFromLiveMap.create(managementContext.getStorage().<Group,Boolean>getMap(getId()+"-groups"));
-        children = SetFromLiveMap.create(managementContext.getStorage().<Entity,Boolean>getMap(getId()+"-children"));
-        locations = managementContext.getStorage().getNonConcurrentList(getId()+"-locations");
-        creationTimeUtc = managementContext.getStorage().getReference(getId()+"-creationTime");
-        displayName = managementContext.getStorage().getReference(getId()+"-displayName");
-        iconUrl = managementContext.getStorage().getReference(getId()+"-iconUrl");
-        
-        // Only override stored defaults if we have actual values. We might be in setManagementContext
-        // because we are reconstituting an existing entity in a new brooklyn management-node (in which
-        // case believe what is already in the storage), or we might be in the middle of creating a new 
-        // entity. Normally for a new entity (using EntitySpec creation approach), this will get called
-        // before setting the parent etc. However, for backwards compatibility we still support some
-        // things calling the entity's constructor directly.
-        if (oldParent != null) parent.set(oldParent);
-        if (oldGroups.size() > 0) groups.addAll(oldGroups);
-        if (oldChildren.size() > 0) children.addAll(oldChildren);
-        if (oldLocations.size() > 0) locations.set(ImmutableList.copyOf(oldLocations));
-        if (creationTimeUtc.isNull()) creationTimeUtc.set(oldCreationTimeUtc);
-        if (displayName.isNull()) {
-            displayName.set(oldDisplayName);
-        } else {
-            displayNameAutoGenerated = false;
-        }
-        if (iconUrl.isNull()) iconUrl.set(oldIconUrl);
-        
-        configsInternal = new EntityConfigMap(this, managementContext.getStorage().<ConfigKey<?>, Object>getMap(getId()+"-config"));
-        if (oldConfig.getLocalConfig().size() > 0) {
-            configsInternal.setLocalConfig(oldConfig.getLocalConfig());
-        }
-        refreshInheritedConfig();
-        
-        attributesInternal = new AttributeMap(this, managementContext.getStorage().<Collection<String>, Object>getMap(getId()+"-attributes"));
-        if (oldAttribs.asRawMap().size() > 0) {
-            for (Map.Entry<Collection<String>,Object> entry : oldAttribs.asRawMap().entrySet()) {
-                attributesInternal.update(entry.getKey(), entry.getValue());
+
+        if (BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_USE_BROOKLYN_LIVE_OBJECTS_DATAGRID_STORAGE)) {
+            Entity oldParent = parent.get();
+            Set<Group> oldGroups = groups;
+            Set<Entity> oldChildren = children;
+            List<Location> oldLocations = locations.get();
+            EntityConfigMap oldConfig = configsInternal;
+            AttributeMap oldAttribs = attributesInternal;
+            long oldCreationTimeUtc = creationTimeUtc.get();
+            String oldDisplayName = displayName.get();
+            String oldIconUrl = iconUrl.get();
+
+            parent = managementContext.getStorage().getReference(getId()+"-parent");
+            groups = SetFromLiveMap.create(managementContext.getStorage().<Group,Boolean>getMap(getId()+"-groups"));
+            children = SetFromLiveMap.create(managementContext.getStorage().<Entity,Boolean>getMap(getId()+"-children"));
+            locations = managementContext.getStorage().getNonConcurrentList(getId()+"-locations");
+            creationTimeUtc = managementContext.getStorage().getReference(getId()+"-creationTime");
+            displayName = managementContext.getStorage().getReference(getId()+"-displayName");
+            iconUrl = managementContext.getStorage().getReference(getId()+"-iconUrl");
+
+            // Only override stored defaults if we have actual values. We might be in setManagementContext
+            // because we are reconstituting an existing entity in a new brooklyn management-node (in which
+            // case believe what is already in the storage), or we might be in the middle of creating a new 
+            // entity. Normally for a new entity (using EntitySpec creation approach), this will get called
+            // before setting the parent etc. However, for backwards compatibility we still support some
+            // things calling the entity's constructor directly.
+            if (oldParent != null) parent.set(oldParent);
+            if (oldGroups.size() > 0) groups.addAll(oldGroups);
+            if (oldChildren.size() > 0) children.addAll(oldChildren);
+            if (oldLocations.size() > 0) locations.set(ImmutableList.copyOf(oldLocations));
+            if (creationTimeUtc.isNull()) creationTimeUtc.set(oldCreationTimeUtc);
+            if (displayName.isNull()) {
+                displayName.set(oldDisplayName);
+            } else {
+                displayNameAutoGenerated = false;
+            }
+            if (iconUrl.isNull()) iconUrl.set(oldIconUrl);
+
+            configsInternal = new EntityConfigMap(this, managementContext.getStorage().<ConfigKey<?>, Object>getMap(getId()+"-config"));
+            if (oldConfig.getLocalConfig().size() > 0) {
+                configsInternal.setLocalConfig(oldConfig.getLocalConfig());
+            }
+            refreshInheritedConfig();
+
+            attributesInternal = new AttributeMap(this, managementContext.getStorage().<Collection<String>, Object>getMap(getId()+"-attributes"));
+            if (oldAttribs.asRawMap().size() > 0) {
+                for (Map.Entry<Collection<String>,Object> entry : oldAttribs.asRawMap().entrySet()) {
+                    attributesInternal.update(entry.getKey(), entry.getValue());
+                }
             }
         }
     }
@@ -533,7 +542,6 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
             throw new IllegalStateException("loop detected trying to set parent of "+this+" as "+entity+", which is already a descendent");
         
         parent.set(entity);
-        //previously tested entity!=null but that should be guaranteed?
         entity.addChild(getProxyIfAvailable());
         refreshInheritedConfig();
         previouslyOwned = true;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/entity/basic/AbstractGroupImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/AbstractGroupImpl.java b/core/src/main/java/brooklyn/entity/basic/AbstractGroupImpl.java
index a8ace7f..bff6e2c 100644
--- a/core/src/main/java/brooklyn/entity/basic/AbstractGroupImpl.java
+++ b/core/src/main/java/brooklyn/entity/basic/AbstractGroupImpl.java
@@ -30,6 +30,7 @@ import org.slf4j.LoggerFactory;
 import brooklyn.entity.Entity;
 import brooklyn.entity.Group;
 import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.util.collections.SetFromLiveMap;
 
@@ -69,17 +70,19 @@ public abstract class AbstractGroupImpl extends AbstractEntity implements Abstra
     public void setManagementContext(ManagementContextInternal managementContext) {
         super.setManagementContext(managementContext);
 
-        Set<Entity> oldMembers = members;
-
-        members = SetFromLiveMap.create(managementContext.getStorage().<Entity,Boolean>getMap(getId()+"-members"));
-
-        // Only override stored defaults if we have actual values. We might be in setManagementContext
-        // because we are reconstituting an existing entity in a new brooklyn management-node (in which
-        // case believe what is already in the storage), or we might be in the middle of creating a new
-        // entity. Normally for a new entity (using EntitySpec creation approach), this will get called
-        // before setting the parent etc. However, for backwards compatibility we still support some
-        // things calling the entity's constructor directly.
-        if (oldMembers.size() > 0) members.addAll(oldMembers);
+        if (BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_USE_BROOKLYN_LIVE_OBJECTS_DATAGRID_STORAGE)) {
+            Set<Entity> oldMembers = members;
+            
+            members = SetFromLiveMap.create(managementContext.getStorage().<Entity,Boolean>getMap(getId()+"-members"));
+
+            // Only override stored defaults if we have actual values. We might be in setManagementContext
+            // because we are reconstituting an existing entity in a new brooklyn management-node (in which
+            // case believe what is already in the storage), or we might be in the middle of creating a new
+            // entity. Normally for a new entity (using EntitySpec creation approach), this will get called
+            // before setting the parent etc. However, for backwards compatibility we still support some
+            // things calling the entity's constructor directly.
+            if (oldMembers.size() > 0) members.addAll(oldMembers);
+        }
     }
 
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
index 45364ba..094c24c 100644
--- a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
+++ b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
@@ -34,9 +34,11 @@ import brooklyn.entity.basic.AbstractEntity;
 import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityLocal;
 import brooklyn.entity.basic.EntityTransientCopyInternal;
+import brooklyn.entity.rebind.RebindManagerImpl.RebindTracker;
 import brooklyn.management.ManagementContext;
 import brooklyn.management.internal.EffectorUtils;
 import brooklyn.management.internal.EntityManagerInternal;
+import brooklyn.management.internal.ManagementTransitionInfo.ManagementTransitionMode;
 
 import com.google.common.base.Objects;
 import com.google.common.collect.Sets;
@@ -53,10 +55,9 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
     // TODO Currently the proxy references the real entity and invokes methods on it directly.
     // As we work on remoting/distribution, this will be replaced by RPC.
 
-    @SuppressWarnings("unused")
     private static final Logger LOG = LoggerFactory.getLogger(EntityProxyImpl.class);
 
-    private final Entity delegate;
+    private Entity delegate;
     private Boolean isMaster;
     
     private static final Set<MethodSignature> OBJECT_METHODS = Sets.newLinkedHashSet();
@@ -96,6 +97,15 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
         this.delegate = checkNotNull(entity, "entity");
     }
     
+    /** invoked to specify that a different underlying delegate should be used, 
+     * e.g. because we are switching copy impls or switching primary/copy*/
+    // TODO XXX because the delegate has a backpointer to the proxy, there is potentially
+    // a nasty problem wherein an old proxy is kept, with a pointer to an old delegate.  
+    public synchronized void resetDelegate(Entity delegate) {
+        this.delegate = delegate;
+        this.isMaster = null;
+    }
+    
     @Override
     public String toString() {
         return delegate.toString();
@@ -103,11 +113,23 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
     
     protected boolean isMaster() {
         if (isMaster!=null) return isMaster;
+        
         ManagementContext mgmt = ((EntityInternal)delegate).getManagementContext();
-        Boolean isReadOnlyNow = ((EntityManagerInternal)mgmt.getEntityManager()).isReadOnly(delegate);
-        if (isReadOnlyNow==null) return false;
-        isMaster = !isReadOnlyNow;
-        return isMaster;
+        ManagementTransitionMode mode = ((EntityManagerInternal)mgmt.getEntityManager()).getLastManagementTransitionMode(delegate.getId());
+        Boolean ro = ((EntityInternal)delegate).getManagementSupport().isReadOnlyRaw();
+        
+        if (mode==null || ro==null) {
+            // not configured yet
+            return false;
+        }
+        boolean isMasterX = !mode.isReadOnly();
+        if (isMasterX != !ro) {
+            LOG.warn("Inconsistent read-only state for "+delegate+" (possibly rebinding); "
+                + "management thinks "+isMasterX+" but entity thinks "+!ro);
+            return false;
+        }
+        isMaster = isMasterX;
+        return isMasterX;
     }
     
     public Object invoke(Object proxy, final Method m, final Object[] args) throws Throwable {
@@ -119,25 +141,29 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
 
         Object result;
         if (OBJECT_METHODS.contains(sig)) {
-            result = m.invoke(this, args);
-        } else if (!isMaster()) {
-            if (ENTITY_PERMITTED_READ_ONLY_METHODS.contains(sig)) {
-                result = m.invoke(delegate, args);
-            } else if (isMaster==null) {
-                // rebinding or caller manipulating before management; permit all access
-                result = m.invoke(delegate, args);
-            } else {
-                throw new UnsupportedOperationException("Call to '"+sig+"' not permitted on read-only entity "+delegate);
-            }
-        } else if (ENTITY_NON_EFFECTOR_METHODS.contains(sig)) {
+            result = m.invoke(delegate, args);
+        } else if (ENTITY_PERMITTED_READ_ONLY_METHODS.contains(sig)) {
             result = m.invoke(delegate, args);
         } else {
-            Object[] nonNullArgs = (args == null) ? new Object[0] : args;
-            Effector<?> eff = findEffector(m, nonNullArgs);
-            if (eff != null) {
-                result = EffectorUtils.invokeMethodEffector(delegate, eff, nonNullArgs);
+            if (!isMaster()) {
+                if (isMaster==null || RebindTracker.isRebinding()) {
+                    // rebinding or caller manipulating before management; permit all access
+                    // (as of this writing, things seem to work fine without the isRebinding check;
+                    // but including in it may allow us to tighten the methods in EntityTransientCopyInternal) 
+                    result = m.invoke(delegate, args);
+                } else {
+                    throw new UnsupportedOperationException("Call to '"+sig+"' not permitted on read-only entity "+delegate);
+                }
+            } else if (ENTITY_NON_EFFECTOR_METHODS.contains(sig)) {
+                result = m.invoke(delegate, args);
             } else {
-                result = m.invoke(delegate, nonNullArgs);
+                Object[] nonNullArgs = (args == null) ? new Object[0] : args;
+                Effector<?> eff = findEffector(m, nonNullArgs);
+                if (eff != null) {
+                    result = EffectorUtils.invokeMethodEffector(delegate, eff, nonNullArgs);
+                } else {
+                    result = m.invoke(delegate, nonNullArgs);
+                }
             }
         }
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/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 624d3dd..849ef05 100644
--- a/core/src/main/java/brooklyn/entity/rebind/BasicEntityRebindSupport.java
+++ b/core/src/main/java/brooklyn/entity/rebind/BasicEntityRebindSupport.java
@@ -192,11 +192,15 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
         }
     }
     
+    protected Entity proxy(Entity target) {
+        return target instanceof AbstractEntity ? ((AbstractEntity)target).getProxyIfAvailable() : target;
+    }
+    
     protected void addChildren(RebindContext rebindContext, EntityMemento memento) {
         for (String childId : memento.getChildren()) {
             Entity child = rebindContext.getEntity(childId);
             if (child != null) {
-                entity.addChild(child);
+                entity.addChild(proxy(child));
             } else {
                 LOG.warn("Entity not found; discarding child {} of entity {}({})",
                         new Object[] {childId, memento.getType(), memento.getId()});
@@ -207,7 +211,7 @@ public class BasicEntityRebindSupport extends AbstractBrooklynObjectRebindSuppor
     protected void setParent(RebindContext rebindContext, EntityMemento memento) {
         Entity parent = (memento.getParent() != null) ? rebindContext.getEntity(memento.getParent()) : null;
         if (parent != null) {
-            entity.setParent(parent);
+            entity.setParent(proxy(parent));
         } else if (memento.getParent() != null){
             LOG.warn("Entity not found; discarding parent {} of entity {}({}), so entity will be orphaned and unmanaged",
                     new Object[] {memento.getParent(), memento.getType(), memento.getId()});

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/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 954d95b..870a031 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
@@ -356,7 +356,7 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
     }
     
     protected void onDoneImpl(Exception e) {
-        if (e != null) Exceptions.propagateIfFatal(e);
+        Exceptions.propagateIfFatal(e);
         
         List<Exception> allExceptions = Lists.newArrayList();
         

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/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 cddb43f..8c067f1 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -24,7 +24,9 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
@@ -61,7 +63,9 @@ import brooklyn.management.Task;
 import brooklyn.management.ha.HighAvailabilityManagerImpl;
 import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.management.internal.EntityManagerInternal;
+import brooklyn.management.internal.LocationManagerInternal;
 import brooklyn.management.internal.ManagementContextInternal;
+import brooklyn.management.internal.ManagementTransitionInfo.ManagementTransitionMode;
 import brooklyn.mementos.BrooklynMemento;
 import brooklyn.mementos.BrooklynMementoManifest;
 import brooklyn.mementos.BrooklynMementoPersister;
@@ -95,6 +99,7 @@ import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 
 /** Manages the persistence/rebind process.
  * <p>
@@ -132,6 +137,7 @@ public class RebindManagerImpl implements RebindManager {
     
     private volatile boolean readOnlyRunning = false;
     private volatile ScheduledTask readOnlyTask = null;
+    private transient Semaphore rebindActive = new Semaphore(1);
     
     private volatile BrooklynMementoPersister persistenceStoreAccess;
 
@@ -369,22 +375,22 @@ public class RebindManagerImpl implements RebindManager {
     }
     
     @Override
-    public List<Application> rebind() throws IOException {
+    public List<Application> rebind() {
         return rebind(null, null, null);
     }
     
     @Override
-    public List<Application> rebind(final ClassLoader classLoader) throws IOException {
+    public List<Application> rebind(final ClassLoader classLoader) {
         return rebind(classLoader, null, null);
     }
 
     @Override
-    public List<Application> rebind(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler) throws IOException {
+    public List<Application> rebind(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler) {
         return rebind(classLoader, exceptionHandler, null);
     }
     
     @Override
-    public List<Application> rebind(ClassLoader classLoaderO, RebindExceptionHandler exceptionHandlerO, ManagementNodeState modeO) throws IOException {
+    public List<Application> rebind(ClassLoader classLoaderO, RebindExceptionHandler exceptionHandlerO, ManagementNodeState modeO) {
         final ClassLoader classLoader = classLoaderO!=null ? classLoaderO :
             managementContext.getCatalog().getRootClassLoader();
         final RebindExceptionHandler exceptionHandler = exceptionHandlerO!=null ? exceptionHandlerO :
@@ -446,9 +452,12 @@ public class RebindManagerImpl implements RebindManager {
         }
     }
     
-    protected List<Application> rebindImpl(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler, ManagementNodeState mode) throws IOException {
+    protected List<Application> rebindImpl(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler, ManagementNodeState mode) {
         checkNotNull(classLoader, "classLoader");
 
+        try {
+            rebindActive.acquire();
+        } catch (InterruptedException e1) { Exceptions.propagate(e1); }
         RebindTracker.setRebinding();
         try {
             exceptionHandler.onStart();
@@ -526,9 +535,6 @@ public class RebindManagerImpl implements RebindManager {
                 
                 try {
                     Entity entity = newEntity(entityId, entityType, reflections);
-                    if (rebindContext.isReadOnly(entity)) {
-                        ((EntityInternal)entity).getManagementSupport().setReadOnly();
-                    }
                     rebindContext.registerEntity(entityId, entity);
                 } catch (Exception e) {
                     exceptionHandler.onCreateFailed(BrooklynObjectType.ENTITY, entityId, entityType, e);
@@ -770,20 +776,41 @@ public class RebindManagerImpl implements RebindManager {
             //
 
             LOG.debug("RebindManager managing locations");
+            LocationManagerInternal locationManager = (LocationManagerInternal)managementContext.getLocationManager();
+            Set<String> oldLocations = Sets.newLinkedHashSet(locationManager.getLocationIds());
+            for (Location location: rebindContext.getLocations()) {
+                ManagementTransitionMode oldMode = locationManager.getLastManagementTransitionMode(location.getId());
+                locationManager.setManagementTransitionMode(location, computeMode(location, oldMode, rebindContext.isReadOnly(location)) );
+                if (oldMode!=null)
+                    oldLocations.remove(location.getId());
+            }
             for (Location location: rebindContext.getLocations()) {
                 if (location.getParent()==null) {
                     // manage all root locations
-                    // LocationManager.manage perhaps should not be deprecated, as we need to do this I think?
                     try {
-                        managementContext.getLocationManager().manage(location);
+                        ((LocationManagerInternal)managementContext.getLocationManager()).manageRebindedRoot(location);
                     } catch (Exception e) {
                         exceptionHandler.onManageFailed(BrooklynObjectType.LOCATION, location, e);
                     }
                 }
             }
+            // destroy old
+            for (String oldLocationId: oldLocations) {
+               locationManager.unmanage(locationManager.getLocation(oldLocationId), ManagementTransitionMode.REBINDING_DESTROYED); 
+            }
             
             // Manage the top-level apps (causing everything under them to become managed)
             LOG.debug("RebindManager managing entities");
+            EntityManagerInternal entityManager = (EntityManagerInternal)managementContext.getEntityManager();
+            Set<String> oldEntities = Sets.newLinkedHashSet(entityManager.getEntityIds());
+            for (Entity entity: rebindContext.getEntities()) {
+                ((EntityInternal)entity).getManagementSupport().setReadOnly( rebindContext.isReadOnly(entity) );
+                
+                ManagementTransitionMode oldMode = entityManager.getLastManagementTransitionMode(entity.getId());
+                entityManager.setManagementTransitionMode(entity, computeMode(entity, oldMode, rebindContext.isReadOnly(entity)) );
+                if (oldMode!=null)
+                    oldEntities.remove(entity.getId());
+            }
             List<Application> apps = Lists.newArrayList();
             for (String appId : memento.getApplicationIds()) {
                 Entity entity = rebindContext.getEntity(appId);
@@ -792,16 +819,17 @@ public class RebindManagerImpl implements RebindManager {
                     exceptionHandler.onNotFound(BrooklynObjectType.ENTITY, appId);
                 } else {
                     try {
-                        if (rebindContext.isReadOnly(entity)) { 
-                            ((EntityManagerInternal)managementContext.getEntityManager()).setReadOnly(entity);
-                        }
-                        managementContext.getEntityManager().manage(entity);
+                        entityManager.manageRebindedRoot(entity);
                     } catch (Exception e) {
                         exceptionHandler.onManageFailed(BrooklynObjectType.ENTITY, entity, e);
                     }
                     apps.add((Application)entity);
                 }
             }
+            // destroy old
+            for (String oldEntityId: oldEntities) {
+               entityManager.unmanage(entityManager.getEntity(oldEntityId), ManagementTransitionMode.REBINDING_DESTROYED); 
+            }
 
             // Register catalogue items with the management context.
             CatalogLoadMode catalogLoadMode = managementContext.getConfig().getConfig(BrooklynServerConfig.CATALOG_LOAD_MODE);
@@ -851,14 +879,36 @@ public class RebindManagerImpl implements RebindManager {
             // Return the top-level applications
             LOG.debug("RebindManager complete; return apps: {}", memento.getApplicationIds());
             return apps;
-            
-        } catch (RuntimeException e) {
+
+        } catch (Exception e) {
             throw exceptionHandler.onFailed(e);
         } finally {
+            rebindActive.release();
             RebindTracker.reset();
         }
     }
     
+    static ManagementTransitionMode computeMode(BrooklynObject item, ManagementTransitionMode oldMode, boolean isNowReadOnly) {
+        return computeMode(item, oldMode==null ? null : oldMode.wasReadOnly(), isNowReadOnly);
+    }
+
+    static ManagementTransitionMode computeMode(BrooklynObject item, Boolean wasReadOnly, boolean isNowReadOnly) {
+        if (wasReadOnly==null) {
+            // not known
+            if (Boolean.TRUE.equals(isNowReadOnly)) return ManagementTransitionMode.REBINDING_READONLY;
+            else return ManagementTransitionMode.CREATING;
+        } else {
+            if (wasReadOnly && isNowReadOnly)
+                return ManagementTransitionMode.REBINDING_READONLY;
+            else if (wasReadOnly)
+                return ManagementTransitionMode.REBINDING_NO_LONGER_PRIMARY;
+            else if (isNowReadOnly)
+                return ManagementTransitionMode.REBINDING_BECOMING_PRIMARY;
+            else
+                throw new IllegalStateException("Rebinding master not supported: "+item);
+        }
+    }
+
     /**
      * Sorts the map of nodes, so that a node's parent is guaranteed to come before that node
      * (unless the parent is missing).

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java b/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
index eb98144..fc938c4 100644
--- a/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
+++ b/core/src/main/java/brooklyn/internal/BrooklynFeatureEnablement.java
@@ -23,8 +23,10 @@ import java.util.Map;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import brooklyn.internal.storage.BrooklynStorage;
 import brooklyn.management.ha.HighAvailabilityMode;
 
+import com.google.common.annotations.Beta;
 import com.google.common.collect.Maps;
 
 /**
@@ -35,6 +37,7 @@ import com.google.common.collect.Maps;
  * 
  * @author aled
  */
+@Beta
 public class BrooklynFeatureEnablement {
 
     private static final Logger LOG = LoggerFactory.getLogger(BrooklynFeatureEnablement.class);
@@ -50,6 +53,13 @@ public class BrooklynFeatureEnablement {
     /** whether the default standby mode is {@link HighAvailabilityMode#HOT_STANDBY} or falling back to the traditional
      * {@link HighAvailabilityMode#STANDBY} */
     public static final String FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY = "brooklyn.experimental.feature.defaultStandbyIsHot";
+    
+    /** whether to attempt to use {@link BrooklynStorage} (datagrid) as a backing store for data;
+     * note this is <b>not</b> compatible with {@link #FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY} 
+     * which uses a blob/file store and a larger-granularity rebind process than was intended with the datagrid */
+    /* not sure if we still even need this? now the rebind/read-only feature reloads on demand from the persistence store;
+     * the data-grid backing  */
+    public static final String FEATURE_USE_BROOKLYN_LIVE_OBJECTS_DATAGRID_STORAGE = "brooklyn.experimental.feature.useBrooklynLiveObjectsDatagridStorage";
 
     private static final Map<String, Boolean> FEATURE_ENABLEMENTS = Maps.newLinkedHashMap();
 
@@ -65,6 +75,7 @@ public class BrooklynFeatureEnablement {
         setDefault(FEATURE_FEED_PERSISTENCE_PROPERTY, true);
         setDefault(FEATURE_CATALOG_PERSISTENCE_PROPERTY, true);
         setDefault(FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY, false);
+        setDefault(FEATURE_USE_BROOKLYN_LIVE_OBJECTS_DATAGRID_STORAGE, false);
     }
     
     static {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/location/basic/AbstractLocation.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/location/basic/AbstractLocation.java b/core/src/main/java/brooklyn/location/basic/AbstractLocation.java
index 7535564..473ce78 100644
--- a/core/src/main/java/brooklyn/location/basic/AbstractLocation.java
+++ b/core/src/main/java/brooklyn/location/basic/AbstractLocation.java
@@ -39,6 +39,7 @@ import brooklyn.entity.rebind.BasicLocationRebindSupport;
 import brooklyn.entity.rebind.RebindSupport;
 import brooklyn.entity.trait.Configurable;
 import brooklyn.event.basic.BasicConfigKey;
+import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.internal.storage.BrooklynStorage;
 import brooklyn.internal.storage.Reference;
 import brooklyn.internal.storage.impl.BasicReference;
@@ -166,38 +167,40 @@ public abstract class AbstractLocation extends AbstractBrooklynObject implements
         super.setManagementContext(managementContext);
         if (displayNameAutoGenerated && getId() != null) name.set(getClass().getSimpleName()+":"+getId().substring(0, Math.min(getId().length(),4)));
 
-        Location oldParent = parent.get();
-        Set<Location> oldChildren = children;
-        Map<String, Object> oldConfig = configBag.getAllConfig();
-        Long oldCreationTimeUtc = creationTimeUtc.get();
-        String oldDisplayName = name.get();
-        HostGeoInfo oldHostGeoInfo = hostGeoInfo.get();
-        
-        parent = managementContext.getStorage().getReference(getId()+"-parent");
-        children = SetFromLiveMap.create(managementContext.getStorage().<Location,Boolean>getMap(getId()+"-children"));
-        creationTimeUtc = managementContext.getStorage().getReference(getId()+"-creationTime");
-        hostGeoInfo = managementContext.getStorage().getReference(getId()+"-hostGeoInfo");
-        name = managementContext.getStorage().getReference(getId()+"-displayName");
-        
-        // Only override stored defaults if we have actual values. We might be in setManagementContext
-        // because we are reconstituting an existing entity in a new brooklyn management-node (in which
-        // case believe what is already in the storage), or we might be in the middle of creating a new 
-        // entity. Normally for a new entity (using EntitySpec creation approach), this will get called
-        // before setting the parent etc. However, for backwards compatibility we still support some
-        // things calling the entity's constructor directly.
-        if (oldParent != null) parent.set(oldParent);
-        if (oldChildren.size() > 0) children.addAll(oldChildren);
-        if (creationTimeUtc.isNull()) creationTimeUtc.set(oldCreationTimeUtc);
-        if (hostGeoInfo.isNull()) hostGeoInfo.set(oldHostGeoInfo);
-        if (name.isNull()) {
-            name.set(oldDisplayName);
-        } else {
-            displayNameAutoGenerated = false;
-        }
-        
-        configBag = ConfigBag.newLiveInstance(managementContext.getStorage().<String,Object>getMap(getId()+"-config"));
-        if (oldConfig.size() > 0) {
-            configBag.putAll(oldConfig);
+        if (BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_USE_BROOKLYN_LIVE_OBJECTS_DATAGRID_STORAGE)) {
+            Location oldParent = parent.get();
+            Set<Location> oldChildren = children;
+            Map<String, Object> oldConfig = configBag.getAllConfig();
+            Long oldCreationTimeUtc = creationTimeUtc.get();
+            String oldDisplayName = name.get();
+            HostGeoInfo oldHostGeoInfo = hostGeoInfo.get();
+
+            parent = managementContext.getStorage().getReference(getId()+"-parent");
+            children = SetFromLiveMap.create(managementContext.getStorage().<Location,Boolean>getMap(getId()+"-children"));
+            creationTimeUtc = managementContext.getStorage().getReference(getId()+"-creationTime");
+            hostGeoInfo = managementContext.getStorage().getReference(getId()+"-hostGeoInfo");
+            name = managementContext.getStorage().getReference(getId()+"-displayName");
+
+            // Only override stored defaults if we have actual values. We might be in setManagementContext
+            // because we are reconstituting an existing entity in a new brooklyn management-node (in which
+            // case believe what is already in the storage), or we might be in the middle of creating a new 
+            // entity. Normally for a new entity (using EntitySpec creation approach), this will get called
+            // before setting the parent etc. However, for backwards compatibility we still support some
+            // things calling the entity's constructor directly.
+            if (oldParent != null) parent.set(oldParent);
+            if (oldChildren.size() > 0) children.addAll(oldChildren);
+            if (creationTimeUtc.isNull()) creationTimeUtc.set(oldCreationTimeUtc);
+            if (hostGeoInfo.isNull()) hostGeoInfo.set(oldHostGeoInfo);
+            if (name.isNull()) {
+                name.set(oldDisplayName);
+            } else {
+                displayNameAutoGenerated = false;
+            }
+
+            configBag = ConfigBag.newLiveInstance(managementContext.getStorage().<String,Object>getMap(getId()+"-config"));
+            if (oldConfig.size() > 0) {
+                configBag.putAll(oldConfig);
+            }
         }
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/location/basic/LocationInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/location/basic/LocationInternal.java b/core/src/main/java/brooklyn/location/basic/LocationInternal.java
index 8c8f38c..a31b0c8 100644
--- a/core/src/main/java/brooklyn/location/basic/LocationInternal.java
+++ b/core/src/main/java/brooklyn/location/basic/LocationInternal.java
@@ -24,7 +24,6 @@ import brooklyn.basic.BrooklynObjectInternal;
 import brooklyn.config.ConfigKey;
 import brooklyn.entity.basic.ConfigKeys;
 import brooklyn.entity.rebind.RebindSupport;
-import brooklyn.entity.rebind.Rebindable;
 import brooklyn.location.Location;
 import brooklyn.management.ManagementContext;
 import brooklyn.mementos.LocationMemento;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java b/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
index 1e2779a..61ff168 100644
--- a/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
+++ b/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
@@ -48,6 +48,7 @@ import brooklyn.util.exceptions.Exceptions;
 
 import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Stopwatch;
 
 /**
@@ -85,6 +86,7 @@ public class EntityManagementSupport {
     protected transient ExecutionContext executionContext;
     
     // TODO the application
+    // (elaborate or remove ^^^ ? -AH, Sept 2014)
     
     protected final AtomicBoolean managementContextUsable = new AtomicBoolean(false);
     protected final AtomicBoolean currentlyDeployed = new AtomicBoolean(false);
@@ -106,22 +108,24 @@ public class EntityManagementSupport {
     public boolean wasDeployed() { return everDeployed.get(); }
     
     @Beta
-    public void setReadOnly() {
+    public void setReadOnly(boolean isReadOnly) {
         if (isDeployed())
             throw new IllegalStateException("Cannot set read only after deployment");
-        readOnly = true;
+        this.readOnly = isReadOnly;
     }
 
-    /** as {@link EntityManagerInternal#isReadOnly(Entity)} */
+    /** Whether the entity and its adjuncts should be treated as read-only;
+     * may be null briefly when initializing if RO status is unknown. */
     @Beta
-    public Boolean isReadOnly() {
-        if (readOnly==null) {
-            // readOnly is set in two places, by rebindManagerImpl when the entity is created,
-            // and again when the entity manager is told we should be read only;
-            // TODO twice setting read-only is redundant; possibly doesn't even need to be on managementContext?
-            // however we must check here to discover canonically when it IS fully managed and it is NOT read only
-            readOnly = ((EntityManagerInternal)managementContext.getEntityManager()).isReadOnly(entity);
-        }
+    public Boolean isReadOnlyRaw() {
+        return readOnly;
+    }
+
+    /** Whether the entity and its adjuncts should be treated as read-only;
+     * error if initializing and RO status is unknown. */
+    @Beta
+    public boolean isReadOnly() {
+        Preconditions.checkNotNull(readOnly, "Read-only status of %s not yet known", entity);
         return readOnly;
     }
 
@@ -174,8 +178,10 @@ public class EntityManagementSupport {
                 this.managementContext = info.getManagementContext();
                 nonDeploymentManagementContext.setMode(NonDeploymentManagementContext.NonDeploymentManagementContextMode.MANAGEMENT_STARTING);
                 
-                nonDeploymentManagementContext.getSubscriptionManager().setDelegate((AbstractSubscriptionManager) managementContext.getSubscriptionManager());
-                nonDeploymentManagementContext.getSubscriptionManager().startDelegatingForSubscribing();
+                if (!isReadOnly()) {
+                    nonDeploymentManagementContext.getSubscriptionManager().setDelegate((AbstractSubscriptionManager) managementContext.getSubscriptionManager());
+                    nonDeploymentManagementContext.getSubscriptionManager().startDelegatingForSubscribing();
+                }
     
                 managementContextUsable.set(true);
                 currentlyDeployed.set(true);
@@ -196,13 +202,16 @@ public class EntityManagementSupport {
              *  then:  set the management context and the entity is "managed" from the perspective of external viewers (ManagementContext.isManaged(entity) returns true)
              */
             
-            entity.onManagementStarting();
+            if (!isReadOnly()) {
+                entity.onManagementStarting();
+            }
         } catch (Throwable t) {
             managementFailed.set(true);
             throw Exceptions.propagate(t);
         }
     }
 
+    @SuppressWarnings("deprecation")
     public void onManagementStarted(ManagementTransitionInfo info) {
         try {
             synchronized (this) {
@@ -231,14 +240,19 @@ public class EntityManagementSupport {
                  * subsequent sensor events and executions occur directly (no queueing)
                  */
                 
-                nonDeploymentManagementContext.getSubscriptionManager().startDelegatingForPublishing();
-                //TODO more of the above
-            
+                if (!isReadOnly()) {
+                    nonDeploymentManagementContext.getSubscriptionManager().startDelegatingForPublishing();
+                }
+                
+                // TODO more of the above
                 // TODO custom started activities
+                // (elaborate or remove ^^^ ? -AH, Sept 2014)
             }
             
-            entity.onManagementBecomingMaster();
-            entity.onManagementStarted();
+            if (!isReadOnly()) {
+                entity.onManagementBecomingMaster();
+                entity.onManagementStarted();
+            }
             
             synchronized (this) {
                 nonDeploymentManagementContext = null;
@@ -249,6 +263,7 @@ public class EntityManagementSupport {
         }
     }
     
+    @SuppressWarnings("deprecation")
     public void onManagementStopping(ManagementTransitionInfo info) {
         synchronized (this) {
             if (managementContext != info.getManagementContext()) {
@@ -278,15 +293,21 @@ public class EntityManagementSupport {
         }
         // TODO custom stopping activities
         // TODO framework stopping events - no more sensors, executions, etc
+        // (elaborate or remove ^^^ ? -AH, Sept 2014)
         
-        if (entity.getParent()!=null) entity.getParent().removeChild(entity.getProxyIfAvailable());
+        if (!isReadOnly()) {
+            // if we support remote parent of local child, the following call will need to be properly remoted
+            if (entity.getParent()!=null) entity.getParent().removeChild(entity.getProxyIfAvailable());
+        }
         // new subscriptions will be queued / not allowed
         nonDeploymentManagementContext.getSubscriptionManager().stopDelegatingForSubscribing();
         // new publications will be queued / not allowed
         nonDeploymentManagementContext.getSubscriptionManager().stopDelegatingForPublishing();
         
-        entity.onManagementNoLongerMaster();
-        entity.onManagementStopped();
+        if (!isReadOnly()) {
+            entity.onManagementNoLongerMaster();
+            entity.onManagementStopped();
+        }
     }
     
     public void onManagementStopped(ManagementTransitionInfo info) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java b/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
index 0dc7c13..e134b23 100644
--- a/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
+++ b/core/src/main/java/brooklyn/management/internal/EntityManagerInternal.java
@@ -20,22 +20,26 @@ package brooklyn.management.internal;
 
 import brooklyn.entity.Application;
 import brooklyn.entity.Entity;
+import brooklyn.location.Location;
 import brooklyn.management.EntityManager;
+import brooklyn.management.internal.ManagementTransitionInfo.ManagementTransitionMode;
 
 public interface EntityManagerInternal extends EntityManager {
 
     /** gets all entities currently known to the application, including entities that are not yet managed */
     Iterable<Entity> getAllEntitiesInApplication(Application application);
 
-    /**
-     * Indicates that the given entity should be treated as read only
-     * when it is subsequently managed.
-     * @throws IllegalStateException if the entity is already managed.
-     */
-    void setReadOnly(Entity e);
+    public Iterable<String> getEntityIds();
+    
+    ManagementTransitionMode getLastManagementTransitionMode(String itemId);
+    void setManagementTransitionMode(Entity item, ManagementTransitionMode mode);
 
-    /** Whether the entity and its adjuncts should be treated as read-only;
-     * may be null when initializing if RO status is unknown. */
-    Boolean isReadOnly(Entity item);
+    /** 
+     * Begins management for the given rebinded root, recursively; 
+     * if rebinding as a read-only copy, {@link #setReadOnly(Location, boolean)} should be called prior to this.
+     */
+    void manageRebindedRoot(Entity item);
+    
+    void unmanage(final Entity e, final ManagementTransitionMode info);
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/LocalAccessManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalAccessManager.java b/core/src/main/java/brooklyn/management/internal/LocalAccessManager.java
index fb20963..077e56f 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalAccessManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalAccessManager.java
@@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicReference;
 import brooklyn.entity.Entity;
 import brooklyn.location.Location;
 import brooklyn.management.AccessController;
-import brooklyn.management.AccessController.Response;
 
 import com.google.common.annotations.Beta;
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
index 69fe77b..52050a4 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
@@ -39,6 +39,7 @@ import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.EntityPredicates;
 import brooklyn.entity.proxying.BasicEntityTypeRegistry;
 import brooklyn.entity.proxying.EntityProxy;
+import brooklyn.entity.proxying.EntityProxyImpl;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.proxying.EntityTypeRegistry;
 import brooklyn.entity.proxying.InternalEntityFactory;
@@ -54,6 +55,7 @@ import brooklyn.policy.PolicySpec;
 import brooklyn.util.collections.SetFromLiveMap;
 import brooklyn.util.exceptions.Exceptions;
 
+import com.google.api.client.repackaged.com.google.common.base.Preconditions;
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
 import com.google.common.base.Predicates;
@@ -84,8 +86,9 @@ public class LocalEntityManager implements EntityManagerInternal {
     
     /** Real managed entities */
     protected final Map<String,Entity> entitiesById = Maps.newLinkedHashMap();
-
-    protected final Set<String> readOnlyEntityIds = Sets.newLinkedHashSet();
+    
+    /** Management mode for each entity */
+    protected final Map<String,ManagementTransitionMode> entityModesById = Maps.newLinkedHashMap();
 
     /** Proxies of the managed entities */
     protected final ObservableList entities = new ObservableList();
@@ -168,6 +171,11 @@ public class LocalEntityManager implements EntityManagerInternal {
     }
     
     @Override
+    public synchronized Collection<String> getEntityIds() {
+        return ImmutableList.copyOf(entityProxiesById.keySet());
+    }
+    
+    @Override
     public synchronized Collection<Entity> getEntitiesInApplication(Application application) {
         Predicate<Entity> predicate = EntityPredicates.applicationIdEqualTo(application.getId());
         return ImmutableList.copyOf(Iterables.filter(entityProxiesById.values(), predicate));
@@ -221,15 +229,15 @@ public class LocalEntityManager implements EntityManagerInternal {
         }
         preRegisteredEntitiesById.put(entity.getId(), entity);
     }
-
-    public void setReadOnly(Entity e) {
-        readOnlyEntityIds.add(e.getId());
-        ((EntityInternal)e).getManagementSupport().setReadOnly();
+    
+    @Override
+    public ManagementTransitionMode getLastManagementTransitionMode(String itemId) {
+        return entityModesById.get(itemId);
     }
     
     @Override
-    public Boolean isReadOnly(Entity item) {
-        return readOnlyEntityIds.contains(item.getId());
+    public void setManagementTransitionMode(Entity item, ManagementTransitionMode mode) {
+        entityModesById.put(item.getId(), mode);
     }
     
     // TODO synchronization issues here. We guard with isManaged(), but if another thread executing 
@@ -249,27 +257,64 @@ public class LocalEntityManager implements EntityManagerInternal {
                     new Exception("source of duplicate management of "+e));
             return;
         }
+        manageRecursive(e, ManagementTransitionMode.CREATING);
+    }
 
+    @Override
+    public void manageRebindedRoot(Entity item) {
+        ManagementTransitionMode mode = getLastManagementTransitionMode(item.getId());
+        Preconditions.checkNotNull(mode, "Mode not set for rebinding %s", item);
+        manageRecursive(item, mode);
+    }
+    
+    protected void manageRecursive(Entity e, final ManagementTransitionMode initialMode) {
         AccessController.Response access = managementContext.getAccessController().canManageEntity(e);
         if (!access.isAllowed()) {
             throw new IllegalStateException("Access controller forbids management of "+e+": "+access.getMsg());
         }
 
-        final ManagementTransitionInfo info = new ManagementTransitionInfo(managementContext, ManagementTransitionMode.NORMAL);
         final List<EntityInternal> allEntities =  Lists.newArrayList();
         recursively(e, new Predicate<EntityInternal>() { public boolean apply(EntityInternal it) {
+            ManagementTransitionMode mode = getLastManagementTransitionMode(it.getId());
+            if (mode==null) {
+                setManagementTransitionMode(it, mode = initialMode);
+            }
+            if (it.getManagementSupport().isReadOnlyRaw()==null) {
+                if (mode.isReadOnly()) {
+                    // should have been marked by rebinder
+                    log.warn("Read-only entity "+it+" not marked as such on call to manage");
+                }
+                it.getManagementSupport().setReadOnly(mode.isReadOnly());
+            }
+            
             if (it.getManagementSupport().isDeployed()) {
-                return false;
-            } else {
-                allEntities.add(it);
-                preManageNonRecursive(it);
-                it.getManagementSupport().onManagementStarting(info); 
-                return manageNonRecursive(it);
+                if (mode==ManagementTransitionMode.CREATING) {
+                    // silently bail out
+                    return false;
+                } else {
+                    // on rebind, should not have any deployed instances
+                    log.warn("Already deployed "+it+" when managing "+mode+"/"+initialMode+"; ignoring this and all descendants");
+                    return false;
+                }
+            }
+            
+            // check RO status is consistent
+            boolean isNowReadOnly = Boolean.TRUE.equals( ((EntityInternal)it).getManagementSupport().isReadOnly() );
+            if (mode.isReadOnly()!=isNowReadOnly) {
+                throw new IllegalStateException("Read-only status mismatch for "+it+": "+mode+" / RO="+isNowReadOnly);
             }
+
+            allEntities.add(it);
+            preManageNonRecursive(it, mode);
+            it.getManagementSupport().onManagementStarting( new ManagementTransitionInfo(managementContext, mode) ); 
+            return manageNonRecursive(it, mode);
         } });
         
         for (EntityInternal it : allEntities) {
             if (!it.getManagementSupport().isFullyManaged()) {
+                ManagementTransitionMode mode = getLastManagementTransitionMode(it.getId());
+                ManagementTransitionInfo info = new ManagementTransitionInfo(managementContext, mode);
+                
                 it.getManagementSupport().onManagementStarted(info);
                 managementContext.getRebindManager().getChangeListener().onManaged(it);
             }
@@ -278,13 +323,47 @@ public class LocalEntityManager implements EntityManagerInternal {
     
     @Override
     public void unmanage(final Entity e) {
+        unmanage(e, ManagementTransitionMode.DESTROYING);
+    }
+    
+    public void unmanage(final Entity e, final ManagementTransitionMode mode) {
+        unmanage(e, mode, false);
+    }
+    
+    private void unmanage(final Entity e, ManagementTransitionMode mode, boolean hasBeenReplaced) {
         if (shouldSkipUnmanagement(e)) return;
-        if (isReadOnly(e)) {
+        final ManagementTransitionInfo info = new ManagementTransitionInfo(managementContext, mode);
+        
+        if (hasBeenReplaced) {
+            // we are unmanaging an old instance after having replaced it
+            
+            if (mode==ManagementTransitionMode.REBINDING_NO_LONGER_PRIMARY) {
+                // when migrating away, these all need to be called
+                ((EntityInternal)e).getManagementSupport().onManagementStopping(info);
+                ((EntityInternal)e).getManagementSupport().onManagementStopped(info);
+                managementContext.getRebindManager().getChangeListener().onUnmanaged(e);
+                if (managementContext.gc != null) managementContext.gc.onUnmanaged(e);
+            } else {
+                // should be coming *from* read only; nothing needed
+                if (!mode.wasReadOnly())
+                    log.warn("Should not be unmanaging "+e+" in mode "+mode);
+            }
+            
+        } else if (mode==ManagementTransitionMode.REBINDING_DESTROYED) {
+            // we are unmanaging an instance (secondary) for which the primary has been destroyed elsewhere
+            ((EntityInternal)e).getManagementSupport().onManagementStopping(info);
+            ((EntityInternal)e).getManagementSupport().onManagementStopped(info);
             unmanageNonRecursive(e);
-        } else {
+            managementContext.getRebindManager().getChangeListener().onUnmanaged(e);
+            if (managementContext.gc != null) managementContext.gc.onUnmanaged(e);
+            
+        } else if (mode==ManagementTransitionMode.DESTROYING) {
+            // we are unmanaging an instance either because it is being destroyed (primary), 
+            // or due to an explicit call (shutting down all things, read-only and primary);
+            // in either case, should be recursive
+            
             // Need to store all child entities as onManagementStopping removes a child from the parent entity
             final List<EntityInternal> allEntities =  Lists.newArrayList();        
-            final ManagementTransitionInfo info = new ManagementTransitionInfo(managementContext, ManagementTransitionMode.NORMAL);
             recursively(e, new Predicate<EntityInternal>() { public boolean apply(EntityInternal it) {
                 if (shouldSkipUnmanagement(it)) return false;
                 allEntities.add(it);
@@ -299,6 +378,9 @@ public class LocalEntityManager implements EntityManagerInternal {
                 managementContext.getRebindManager().getChangeListener().onUnmanaged(it);
                 if (managementContext.gc != null) managementContext.gc.onUnmanaged(it);
             }
+            
+        } else {
+            log.warn("Invalid mode for unmanage: "+mode+" on "+e+" (ignoring)");
         }
     }
     
@@ -357,14 +439,15 @@ public class LocalEntityManager implements EntityManagerInternal {
      * Note that refs to the given entity are stored in a a weak hashmap so if the subsequent management
      * attempt fails then this reference to the entity will eventually be discarded (if no-one else holds 
      * a reference).
+     * @param isOrWasReadOnly 
      */
-    private synchronized boolean preManageNonRecursive(Entity e) {
+    private synchronized boolean preManageNonRecursive(Entity e, ManagementTransitionMode mode) {
         Entity realE = toRealEntity(e);
         
         Object old = preManagedEntitiesById.put(e.getId(), realE);
         preRegisteredEntitiesById.remove(e.getId());
         
-        if (old!=null) {
+        if (old!=null && mode==ManagementTransitionMode.CREATING) {
             if (old.equals(e)) {
                 log.warn("{} redundant call to pre-start management of entity {}", this, e);
             } else {
@@ -372,7 +455,8 @@ public class LocalEntityManager implements EntityManagerInternal {
             }
             return false;
         } else {
-            if (log.isTraceEnabled()) log.trace("{} pre-start management of entity {}", this, e);
+            if (log.isTraceEnabled()) log.trace("{} pre-start management of entity {}, mode {}", 
+                new Object[] { this, e, mode });
             return true;
         }
     }
@@ -380,35 +464,53 @@ public class LocalEntityManager implements EntityManagerInternal {
     /**
      * Should ensure that the entity is now managed somewhere, and known about in all the lists.
      * Returns true if the entity has now become managed; false if it was already managed (anything else throws exception)
+     * @param isOrWasReadOnly 
      */
-    private synchronized boolean manageNonRecursive(Entity e) {
-        Entity realE = toRealEntity(e);
-        Entity proxyE = toProxyEntityIfAvailable(e);
-        
-        // If we don't already know about the proxy, then use the real thing; presumably it's 
-        // the legacy way of creating the entity so didn't get a preManage() call
-        entityProxiesById.put(e.getId(), proxyE);
+    private synchronized boolean manageNonRecursive(Entity e, ManagementTransitionMode mode) {
+        Entity old = entitiesById.get(e.getId());
         
-        entityTypes.put(e.getId(), realE.getClass().getName());
-        
-        Object old = entitiesById.put(e.getId(), realE);
-        if (old!=null) {
+        if (old!=null && mode==ManagementTransitionMode.CREATING) {
             if (old.equals(e)) {
                 log.warn("{} redundant call to start management of entity {}", this, e);
             } else {
                 throw new IllegalStateException("call to manage entity "+e+" but different entity "+old+" already known under that id at "+this);
             }
             return false;
-        } else {
-            if (log.isDebugEnabled()) log.debug("{} starting management of entity {}", this, e);
-            preManagedEntitiesById.remove(e.getId());
-            if ((e instanceof Application) && (e.getParent()==null)) {
-                applications.add((Application)proxyE);
-                applicationIds.add(e.getId());
+        }
+        
+        if (log.isDebugEnabled()) log.debug("{} starting management of entity {}", this, e);
+        Entity realE = toRealEntity(e);
+        
+        Entity oldProxy = entityProxiesById.get(e.getId());
+        Entity proxyE;
+        if (oldProxy!=null) {
+            if (mode==ManagementTransitionMode.CREATING) {
+                throw new IllegalStateException("call to manage entity "+e+" but already had proxy "+oldProxy+" already known under that id at "+this);
             }
-            entities.add(proxyE);
-            return true;
+            // set the new entity to the old proxy
+            ((AbstractEntity)realE).resetProxy(oldProxy);
+            proxyE = oldProxy;
+            ((EntityProxyImpl)(Proxy.getInvocationHandler(proxyE))).resetDelegate(realE);
+        } else {
+            proxyE = toProxyEntityIfAvailable(e);
+        }
+        entityProxiesById.put(e.getId(), proxyE);
+        entityTypes.put(e.getId(), realE.getClass().getName());
+        entitiesById.put(e.getId(), realE);
+
+        preManagedEntitiesById.remove(e.getId());
+        if ((e instanceof Application) && (e.getParent()==null)) {
+            applications.add((Application)proxyE);
+            applicationIds.add(e.getId());
+        }
+        entities.add(proxyE);
+        
+        if (old!=null && old!=e) {
+            // passing the transition info will ensure the right shutdown steps invoked for old instance
+            unmanage(old, mode, true);
         }
+        
+        return true;
     }
 
     /**
@@ -433,7 +535,7 @@ public class LocalEntityManager implements EntityManagerInternal {
          * from its groups?
          */
         
-        if (!isReadOnly(e)) {
+        if (!getLastManagementTransitionMode(e.getId()).isReadOnly()) {
             Collection<Group> groups = e.getGroups();
             e.clearParent();
             for (Group group : groups) {
@@ -458,7 +560,7 @@ public class LocalEntityManager implements EntityManagerInternal {
 
             entities.remove(proxyE);
             entityProxiesById.remove(e.getId());
-            readOnlyEntityIds.remove(e.getId());
+            entityModesById.remove(e.getId());
             Object old = entitiesById.remove(e.getId());
 
             entityTypes.remove(e.getId());
@@ -509,6 +611,9 @@ public class LocalEntityManager implements EntityManagerInternal {
             Entity result = ((AbstractEntity)e).getProxy();
             return (result == null) ? e : result;
         } else {
+            // If we don't already know about the proxy, then use the real thing; presumably it's 
+            // the legacy way of creating the entity so didn't get a preManage() call
+
             return e;
         }
     }
@@ -545,4 +650,5 @@ public class LocalEntityManager implements EntityManagerInternal {
     private boolean isRunning() {
         return managementContext.isRunning();
     }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java b/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
index 8ce4dbe..47087f9 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
@@ -36,17 +36,18 @@ import brooklyn.location.LocationSpec;
 import brooklyn.location.basic.AbstractLocation;
 import brooklyn.location.basic.LocationInternal;
 import brooklyn.management.AccessController;
-import brooklyn.management.LocationManager;
+import brooklyn.management.internal.ManagementTransitionInfo.ManagementTransitionMode;
 import brooklyn.util.config.ConfigBag;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.exceptions.RuntimeInterruptedException;
 
+import com.google.api.client.repackaged.com.google.common.base.Preconditions;
 import com.google.common.annotations.Beta;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
 
-public class LocalLocationManager implements LocationManager {
+public class LocalLocationManager implements LocationManagerInternal {
 
     @Beta /* expect to remove when API returns LocationSpec or similar */
     public static final ConfigKey<Boolean> CREATE_UNMANAGED = ConfigKeys.newBooleanConfigKey("brooklyn.internal.location.createUnmanaged",
@@ -60,6 +61,9 @@ public class LocalLocationManager implements LocationManager {
     protected final Map<String,Location> locationsById = Maps.newLinkedHashMap();
     private final Map<String, Location> preRegisteredLocationsById = Maps.newLinkedHashMap();
 
+    /** Management mode for each location */
+    protected final Map<String,ManagementTransitionMode> locationModesById = Maps.newLinkedHashMap();
+
     private final BrooklynStorage storage;
     private Map<String, String> locationTypes;
     
@@ -115,6 +119,11 @@ public class LocalLocationManager implements LocationManager {
     }
     
     @Override
+    public Collection<String> getLocationIds() {
+        return ImmutableList.copyOf(locationsById.keySet());
+    }
+
+    @Override
     public synchronized Location getLocation(String id) {
         return locationsById.get(id);
     }
@@ -149,7 +158,18 @@ public class LocalLocationManager implements LocationManager {
         preRegisteredLocationsById.put(loc.getId(), loc);
     }
     
+    @Override
+    public ManagementTransitionMode getLastManagementTransitionMode(String itemId) {
+        return locationModesById.get(itemId);
+    }
+    
+    @Override
+    public void setManagementTransitionMode(Location item, ManagementTransitionMode mode) {
+        locationModesById.put(item.getId(), mode);
+    }
+
     // TODO synchronization issues here: see comment in LocalEntityManager.manage(Entity)
+    /** management on creation */
     @Override
     public Location manage(Location loc) {
         if (isManaged(loc)) {
@@ -158,50 +178,115 @@ public class LocalLocationManager implements LocationManager {
             return loc;
         }
         
-        AccessController.Response access = managementContext.getAccessManager().getAccessController().canManageLocation(loc);
-        if (!access.isAllowed()) {
-            throw new IllegalStateException("Access controller forbids management of "+loc+": "+access.getMsg());
-        }
-        
         Location parent = loc.getParent();
         if (parent != null && !managementContext.getLocationManager().isManaged(parent)) {
-//            throw new IllegalStateException("Can't manage "+e+" because its parent is not yet managed ("+parent+")");
             log.warn("Parent location "+parent+" of "+loc+" is not managed; attempting to manage it (in future this may be disallowed)");
-            manage(parent);
+            return manage(parent);
+        } else {
+            return manageRecursive(loc, ManagementTransitionMode.CREATING);
         }
-        
+    }
+    
+    @Override
+    public void manageRebindedRoot(Location item) {
+        ManagementTransitionMode mode = getLastManagementTransitionMode(item.getId());
+        Preconditions.checkNotNull(mode, "Mode not set for rebinding %s", item);
+        manageRecursive(item, mode);
+    }
+    
+    protected Location manageRecursive(Location loc, final ManagementTransitionMode initialMode) {
+        AccessController.Response access = managementContext.getAccessController().canManageLocation(loc);
+        if (!access.isAllowed()) {
+            throw new IllegalStateException("Access controller forbids management of "+loc+": "+access.getMsg());
+        }
+
         recursively(loc, new Predicate<AbstractLocation>() { public boolean apply(AbstractLocation it) {
+            ManagementTransitionMode mode = getLastManagementTransitionMode(it.getId());
+            if (mode==null) {
+                setManagementTransitionMode(it, mode = initialMode);
+            }
+            
             if (it.isManaged()) {
-                return false;
-            } else {
-                boolean result = manageNonRecursive(it);
-                if (result) {
-                    it.setManagementContext(managementContext);
+                if (mode==ManagementTransitionMode.CREATING) {
+                    // silently bail out
+                    return false;
+                } else {
+                    // on rebind, we just replace, fall through to below
+                }
+            }
+            
+            boolean result = manageNonRecursive(it, null);
+            if (result) {
+                it.setManagementContext(managementContext);
+                if (!mode.isReadOnly()) {
                     it.onManagementStarted();
                     recordLocationEvent(it, Lifecycle.CREATED);
-                    managementContext.getRebindManager().getChangeListener().onManaged(it);
                 }
-                return result;
+                managementContext.getRebindManager().getChangeListener().onManaged(it);
             }
+            return result;
         } });
         return loc;
     }
     
     @Override
-    public void unmanage(Location loc) {
+    public void unmanage(final Location loc) {
+        unmanage(loc, ManagementTransitionMode.DESTROYING);
+    }
+    
+    public void unmanage(final Location loc, final ManagementTransitionMode mode) {
+        unmanage(loc, mode, false);
+    }
+    
+    private void unmanage(final Location loc, ManagementTransitionMode mode, boolean hasBeenReplaced) {
         if (shouldSkipUnmanagement(loc)) return;
-        
-        recursively(loc, new Predicate<AbstractLocation>() { public boolean apply(AbstractLocation it) {
-            if (shouldSkipUnmanagement(it)) return false;
-            boolean result = unmanageNonRecursive(it);
-            if (result) {
-                it.onManagementStopped(); 
-                managementContext.getRebindManager().getChangeListener().onUnmanaged(it);
-                recordLocationEvent(it, Lifecycle.DESTROYED);
-                if (managementContext.gc != null) managementContext.gc.onUnmanaged(it);
+
+        if (hasBeenReplaced) {
+            // we are unmanaging an old instance after having replaced it
+            
+            if (mode==ManagementTransitionMode.REBINDING_NO_LONGER_PRIMARY) {
+                // when migrating away, these all need to be called
+                managementContext.getRebindManager().getChangeListener().onUnmanaged(loc);
+                if (managementContext.gc != null) managementContext.gc.onUnmanaged(loc);
+            } else {
+                // should be coming *from* read only; nothing needed
+                if (!mode.wasReadOnly())
+                    log.warn("Should not be unmanaging "+loc+" in mode "+mode);
             }
-            return result;
-        } });
+
+        } else if (mode==ManagementTransitionMode.REBINDING_DESTROYED) {
+            // we are unmanaging an instance (secondary) for which the primary has been destroyed elsewhere
+            unmanageNonRecursive(loc);
+            managementContext.getRebindManager().getChangeListener().onUnmanaged(loc);
+            if (managementContext.gc != null) managementContext.gc.onUnmanaged(loc);
+            
+        } else if (mode==ManagementTransitionMode.DESTROYING) {
+            // we are unmanaging an instance either because it is being destroyed (primary), 
+            // or due to an explicit call (shutting down all things, read-only and primary);
+            // in either case, should be recursive
+            
+            // Need to store all child entities as onManagementStopping removes a child from the parent entity
+            recursively(loc, new Predicate<AbstractLocation>() { public boolean apply(AbstractLocation it) {
+                if (shouldSkipUnmanagement(it)) return false;
+                boolean result = unmanageNonRecursive(it);
+                if (result) {
+                    ManagementTransitionMode mode = getLastManagementTransitionMode(it.getId());
+                    if (mode==null) {
+                        log.warn("Missing transition mode for "+it+" when unmanaging; assuming primary");
+                        mode = ManagementTransitionMode.DESTROYING;
+                    }
+                    if (!mode.isReadOnly()) it.onManagementStopped();
+                    managementContext.getRebindManager().getChangeListener().onUnmanaged(it);
+                    if (!mode.isReadOnly()) recordLocationEvent(it, Lifecycle.DESTROYED);
+                    if (managementContext.gc != null) managementContext.gc.onUnmanaged(it);
+                }
+                return result;
+            } });
+            
+        } else {
+            log.warn("Invalid mode for unmanage: "+mode+" on "+loc+" (ignoring)");
+        }
+        
     }
     
     /**
@@ -231,33 +316,40 @@ public class LocalLocationManager implements LocationManager {
     /**
      * Should ensure that the location is now managed somewhere, and known about in all the lists.
      * Returns true if the location has now become managed; false if it was already managed (anything else throws exception)
+     * @param rebindPrimary true if rebinding primary, false if rebinding as copy, null if creating (not rebinding)
      */
-    private synchronized boolean manageNonRecursive(Location loc) {
-        Object old = locationsById.put(loc.getId(), loc);
+    private synchronized boolean manageNonRecursive(Location loc, ManagementTransitionMode mode) {
+        Location old = locationsById.put(loc.getId(), loc);
         preRegisteredLocationsById.remove(loc.getId());
 
         locationTypes.put(loc.getId(), loc.getClass().getName());
         
-        if (old!=null) {
+        if (old!=null && mode==ManagementTransitionMode.CREATING) {
             if (old.equals(loc)) {
                 log.warn("{} redundant call to start management of location {}", this, loc);
             } else {
                 throw new IllegalStateException("call to manage location "+loc+" but different location "+old+" already known under that id at "+this);
             }
             return false;
-        } else {
-            return true;
         }
-    }
 
+        if (old!=null && old!=loc) {
+            // passing the transition info will ensure the right shutdown steps invoked for old instance
+            unmanage(old, mode, true);
+        }
+        
+        return true;
+    }
+    
     /**
      * Should ensure that the location is no longer managed anywhere, remove from all lists.
      * Returns true if the location has been removed from management; if it was not previously managed (anything else throws exception) 
      */
-    private synchronized boolean unmanageNonRecursive(AbstractLocation loc) {
+    private synchronized boolean unmanageNonRecursive(Location loc) {
         loc.setParent(null);
         Object old = locationsById.remove(loc.getId());
         locationTypes.remove(loc.getId());
+        locationModesById.remove(loc.getId());
         
         if (old==null) {
             log.warn("{} call to stop management of unknown location (already unmanaged?) {}", this, loc);
@@ -288,4 +380,5 @@ public class LocalLocationManager implements LocationManager {
     private boolean isRunning() {
         return managementContext.isRunning();
     }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java b/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
index bf4a559..96de11d 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
@@ -39,7 +39,6 @@ import brooklyn.config.BrooklynProperties.Factory.Builder;
 import brooklyn.entity.Application;
 import brooklyn.entity.Effector;
 import brooklyn.entity.Entity;
-import brooklyn.entity.basic.BrooklynTaskTags;
 import brooklyn.entity.drivers.downloads.BasicDownloadsManager;
 import brooklyn.entity.effector.Effectors;
 import brooklyn.entity.proxying.InternalEntityFactory;
@@ -55,7 +54,6 @@ import brooklyn.management.SubscriptionManager;
 import brooklyn.management.Task;
 import brooklyn.management.TaskAdaptable;
 import brooklyn.management.ha.OsgiManager;
-import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.guava.Maybe;
 import brooklyn.util.task.BasicExecutionContext;
@@ -135,7 +133,7 @@ public class LocalManagementContext extends AbstractManagementContext {
      * Creates a LocalManagement with default BrooklynProperties.
      */
     public LocalManagementContext() {
-        this(new Builder());
+        this(BrooklynProperties.Factory.builderDefault());
     }
 
     public LocalManagementContext(BrooklynProperties brooklynProperties) {


[13/15] git commit: address the rest of @sjcorbett's comments

Posted by he...@apache.org.
address the rest of @sjcorbett's comments


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

Branch: refs/heads/master
Commit: 6ba1639e3af23f6cae31f2062647b7900b3ccd5e
Parents: 6d58882
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Oct 1 18:28:00 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 18:28:00 2014 +0100

----------------------------------------------------------------------
 .../management/ha/HighAvailabilityManagerImpl.java |  4 +---
 .../management/internal/LocalEntityManager.java    | 17 +++++++++--------
 .../management/internal/LocalLocationManager.java  | 10 +++++-----
 .../internal/NonDeploymentLocationManager.java     |  2 +-
 .../ha/HighAvailabilityManagerSplitBrainTest.java  |  5 +++--
 .../brooklyn/management/ha/HotStandbyTest.java     |  8 +++++---
 .../brooklyn/management/ha/WarmStandbyTest.java    |  1 +
 7 files changed, 25 insertions(+), 22 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6ba1639e/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 200a902..c71d352 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -53,10 +53,10 @@ import brooklyn.util.task.ScheduledTask;
 import brooklyn.util.text.Strings;
 import brooklyn.util.time.Duration;
 
-import com.google.api.client.repackaged.com.google.common.base.Preconditions;
 import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Ticker;
 import com.google.common.collect.Iterables;
 
@@ -718,8 +718,6 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         try {
             Preconditions.checkState(nodeStateTransitionComplete==false, "Must be in transitioning state to go into hot standby");
             nodeState = ManagementNodeState.HOT_STANDBY;
-            
-            // XXX
             managementContext.getRebindManager().startReadOnly();
             
             return true;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6ba1639e/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
index 0eae80b..adc9839 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
@@ -23,6 +23,7 @@ import groovy.util.ObservableList;
 
 import java.lang.reflect.Proxy;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -88,7 +89,7 @@ public class LocalEntityManager implements EntityManagerInternal {
     protected final Map<String,Entity> entitiesById = Maps.newLinkedHashMap();
     
     /** Management mode for each entity */
-    protected final Map<String,ManagementTransitionMode> entityModesById = Maps.newLinkedHashMap();
+    protected final Map<String,ManagementTransitionMode> entityModesById = Collections.synchronizedMap(Maps.<String,ManagementTransitionMode>newLinkedHashMap());
 
     /** Proxies of the managed entities */
     protected final ObservableList entities = new ObservableList();
@@ -282,7 +283,7 @@ public class LocalEntityManager implements EntityManagerInternal {
             if (it.getManagementSupport().isReadOnlyRaw()==null) {
                 if (mode.isReadOnly()) {
                     // should have been marked by rebinder
-                    log.warn("Read-only entity "+it+" not marked as such on call to manage");
+                    log.warn("Read-only entity "+it+" not marked as such on call to manage; marking and continuing");
                 }
                 it.getManagementSupport().setReadOnly(mode.isReadOnly());
             }
@@ -345,8 +346,9 @@ public class LocalEntityManager implements EntityManagerInternal {
                 if (managementContext.gc != null) managementContext.gc.onUnmanaged(e);
             } else {
                 // should be coming *from* read only; nothing needed
-                if (!mode.wasReadOnly())
-                    log.warn("Should not be unmanaging "+e+" in mode "+mode);
+                if (!mode.wasReadOnly()) {
+                    log.warn("Should not be unmanaging "+e+" in mode "+mode+"; ignoring");
+                }
             }
             
         } else if (mode==ManagementTransitionMode.REBINDING_DESTROYED) {
@@ -439,7 +441,6 @@ public class LocalEntityManager implements EntityManagerInternal {
      * Note that refs to the given entity are stored in a a weak hashmap so if the subsequent management
      * attempt fails then this reference to the entity will eventually be discarded (if no-one else holds 
      * a reference).
-     * @param isOrWasReadOnly 
      */
     private synchronized boolean preManageNonRecursive(Entity e, ManagementTransitionMode mode) {
         Entity realE = toRealEntity(e);
@@ -449,7 +450,7 @@ public class LocalEntityManager implements EntityManagerInternal {
         
         if (old!=null && mode==ManagementTransitionMode.CREATING) {
             if (old.equals(e)) {
-                log.warn("{} redundant call to pre-start management of entity {}", this, e);
+                log.warn("{} redundant call to pre-start management of entity {}; ignoring", this, e);
             } else {
                 throw new IllegalStateException("call to pre-manage entity "+e+" but different entity "+old+" already known under that id at "+this);
             }
@@ -471,7 +472,7 @@ public class LocalEntityManager implements EntityManagerInternal {
         
         if (old!=null && mode==ManagementTransitionMode.CREATING) {
             if (old.equals(e)) {
-                log.warn("{} redundant call to start management of entity {}", this, e);
+                log.warn("{} redundant call to start management of entity {}; ignoring", this, e);
             } else {
                 throw new IllegalStateException("call to manage entity "+e+" but different entity "+old+" already known under that id at "+this);
             }
@@ -565,7 +566,7 @@ public class LocalEntityManager implements EntityManagerInternal {
 
             entityTypes.remove(e.getId());
             if (old==null) {
-                log.warn("{} call to stop management of unknown entity (already unmanaged?) {}", this, e);
+                log.warn("{} call to stop management of unknown entity (already unmanaged?) {}; ignoring", this, e);
                 return false;
             } else if (!old.equals(e)) {
                 // shouldn't happen...

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6ba1639e/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java b/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
index 47087f9..b70d4f3 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalLocationManager.java
@@ -41,8 +41,8 @@ import brooklyn.util.config.ConfigBag;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.exceptions.RuntimeInterruptedException;
 
-import com.google.api.client.repackaged.com.google.common.base.Preconditions;
 import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Maps;
@@ -251,7 +251,7 @@ public class LocalLocationManager implements LocationManagerInternal {
             } else {
                 // should be coming *from* read only; nothing needed
                 if (!mode.wasReadOnly())
-                    log.warn("Should not be unmanaging "+loc+" in mode "+mode);
+                    log.warn("Should not be unmanaging "+loc+" in mode "+mode+"; ignoring");
             }
 
         } else if (mode==ManagementTransitionMode.REBINDING_DESTROYED) {
@@ -299,7 +299,7 @@ public class LocalLocationManager implements LocationManagerInternal {
         } catch (RuntimeInterruptedException e) {
             throw e;
         } catch (RuntimeException e) {
-            log.warn("Failed to store location lifecycle event for "+loc, e);
+            log.warn("Failed to store location lifecycle event for "+loc+" (ignoring)", e);
         }
     }
 
@@ -352,11 +352,11 @@ public class LocalLocationManager implements LocationManagerInternal {
         locationModesById.remove(loc.getId());
         
         if (old==null) {
-            log.warn("{} call to stop management of unknown location (already unmanaged?) {}", this, loc);
+            log.warn("{} call to stop management of unknown location (already unmanaged?) {}; ignoring", this, loc);
             return false;
         } else if (!old.equals(loc)) {
             // shouldn't happen...
-            log.error("{} call to stop management of location {} removed different location {}", new Object[] { this, loc, old });
+            log.error("{} call to stop management of location {} removed different location {}; ignoring", new Object[] { this, loc, old });
             return true;
         } else {
             if (log.isDebugEnabled()) log.debug("{} stopped management of location {}", this, loc);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6ba1639e/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
index 3628231..a11d876 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
@@ -76,7 +76,7 @@ public class NonDeploymentLocationManager implements LocationManagerInternal {
         if (isInitialManagementContextReal()) {
             return ((LocationManagerInternal)initialManagementContext.getLocationManager()).getLocationIds();
         } else {
-            return null;
+            return Collections.emptyList();
         }
     }
     

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6ba1639e/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 a0b7feb..48bdfa5 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
@@ -69,7 +69,8 @@ public class HighAvailabilityManagerSplitBrainTest {
     private ClassLoader classLoader = getClass().getClassLoader();
     
     public class HaMgmtNode {
-        
+        // TODO share with HotStandbyTest and WarmStandbyTest and a few others (minor differences but worth it ultimately)
+
         private ManagementContextInternal mgmt;
         private String ownNodeId;
         private String nodeName;
@@ -278,7 +279,7 @@ public class HighAvailabilityManagerSplitBrainTest {
 
         // right number of entities at n2; n1 may or may not depending whether hot standby is default
         assertEquals(n2.mgmt.getApplications().size(), 1);
-//        assertEquals(n1.mgmt.getApplications().size(), 0);
+        assertEquals(n1.mgmt.getApplications().size(), BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY) ? 1 : 0);
     }
     
     @Test(invocationCount=50, groups="Integration")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6ba1639e/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 1a41c19..a34f26b 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -70,7 +70,8 @@ public class HotStandbyTest {
     private ClassLoader classLoader = getClass().getClassLoader();
     
     public class HaMgmtNode {
-        
+        // TODO share with WarmStandbyTest and SplitBrainTest and a few others (minor differences but worth it ultimately)
+
         private ManagementContextInternal mgmt;
         private String ownNodeId;
         private String nodeName;
@@ -204,7 +205,7 @@ public class HotStandbyTest {
     }
     
     @Test
-    public void testHotStandbySeesChangedNameConfigAndSensorValueButDoesntAllowChange() throws Exception {
+    public void testHotStandbySeesInitialCustomNameConfigAndSensorValueButDoesntAllowChange() throws Exception {
         HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
         TestApplication app = createFirstAppAndPersist(n1);
         HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
@@ -223,10 +224,11 @@ public class HotStandbyTest {
         } catch (Exception e) {
             Assert.assertTrue(e.toString().toLowerCase().contains("read-only"), "Error message did not contain expected text: "+e);
         }
+        assertEquals(appRO.getAttribute(TestEntity.SEQUENCE), (Integer)3);
     }
 
     @Test
-    public void testHotStandbySeesChangedNameConfigAndSensorValue() throws Exception {
+    public void testHotStandbySeesChangesToNameConfigAndSensorValue() throws Exception {
         HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
         TestApplication app = createFirstAppAndPersist(n1);
         HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6ba1639e/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 d955d50..8c40947 100644
--- a/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/WarmStandbyTest.java
@@ -56,6 +56,7 @@ public class WarmStandbyTest {
     private ClassLoader classLoader = getClass().getClassLoader();
     
     public class HaMgmtNode {
+        // TODO share with HotStandbyTest and SplitBrainTest and a few others (minor differences but worth it ultimately)
         
         private ManagementContextInternal mgmt;
         private String ownNodeId;


[12/15] git commit: Wait for the actual result of the shutdown call.

Posted by he...@apache.org.
Wait for the actual result of the shutdown call.

Wait indefinitely for the shutdown result, otherwise we get "invalid" success, not aware if there was indeed a shutdown.


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

Branch: refs/heads/master
Commit: 69718095f319fa087215d4f54e0f01d3b1582c17
Parents: c49d7d3
Author: Svetoslav Neykov <sv...@cloudsoftcorp.com>
Authored: Wed Oct 1 19:14:45 2014 +0300
Committer: Svetoslav Neykov <sv...@cloudsoftcorp.com>
Committed: Wed Oct 1 19:14:45 2014 +0300

----------------------------------------------------------------------
 usage/jsgui/src/main/webapp/assets/tpl/help/page.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/69718095/usage/jsgui/src/main/webapp/assets/tpl/help/page.html
----------------------------------------------------------------------
diff --git a/usage/jsgui/src/main/webapp/assets/tpl/help/page.html b/usage/jsgui/src/main/webapp/assets/tpl/help/page.html
index 9f4d21a..7d3ef0a 100644
--- a/usage/jsgui/src/main/webapp/assets/tpl/help/page.html
+++ b/usage/jsgui/src/main/webapp/assets/tpl/help/page.html
@@ -50,7 +50,7 @@ under the License.
             $.ajax({
                 type:"POST",
                 url:"/v1/server/shutdown",
-                data: { stopAppsFirst: stopAppsFirst },
+                data: { stopAppsFirst: stopAppsFirst, shutdownTimeout: 0, requestTimeout: 0 },
                 success:function (data) {
                     $('#help-page').fadeTo(500,0.1);
                 },


[06/15] git commit: improve support for interrupting scheduled tasks and a "wait for interrupted tasks to really complete" method on Tasks; see discussion there, and test case in ScheduledExecutionTest.java

Posted by he...@apache.org.
improve support for interrupting scheduled tasks and a "wait for interrupted tasks to really complete" method on Tasks; see discussion there, and test case in ScheduledExecutionTest.java


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

Branch: refs/heads/master
Commit: ea4b8121688c0eea3a69d6917c8e873359ec5b32
Parents: ea4b4cc
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Mon Sep 29 13:36:18 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:15 2014 +0100

----------------------------------------------------------------------
 api/src/main/java/brooklyn/management/Task.java |   8 +
 .../main/java/brooklyn/util/task/BasicTask.java |   2 +-
 .../java/brooklyn/util/task/ScheduledTask.java  |  18 +++
 .../src/main/java/brooklyn/util/task/Tasks.java |  51 ++++++
 .../util/task/ScheduledExecutionTest.java       | 159 ++++++++++++++++---
 5 files changed, 213 insertions(+), 25 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b8121/api/src/main/java/brooklyn/management/Task.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/Task.java b/api/src/main/java/brooklyn/management/Task.java
index fa3bd40..d6dc401 100644
--- a/api/src/main/java/brooklyn/management/Task.java
+++ b/api/src/main/java/brooklyn/management/Task.java
@@ -22,6 +22,7 @@ import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeoutException;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -117,4 +118,11 @@ public interface Task<T> extends ListenableFuture<T>, TaskAdaptable<T> {
      * (including a {@link TimeoutException} if the duration expires) */
     public T getUnchecked(Duration duration);
 
+    /** As {@link Future#cancel(boolean)}. Note that {@link #isDone()} and {@link #blockUntilEnded(Duration)} return immediately
+     * once a task is cancelled, consistent with the underlying {@link FutureTask} behaviour.  
+     * TODO Fine-grained control over underlying jobs, e.g. to ensure anything represented by this task is actually completed,
+     * is not (yet) publicly exposed. See the convenience method blockUntilInternalTasksEnded in the Tasks set of helpers
+     * for more discussion. */
+    public boolean cancel(boolean mayInterruptIfRunning);
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b8121/core/src/main/java/brooklyn/util/task/BasicTask.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/task/BasicTask.java b/core/src/main/java/brooklyn/util/task/BasicTask.java
index 64c768c..a3847a6 100644
--- a/core/src/main/java/brooklyn/util/task/BasicTask.java
+++ b/core/src/main/java/brooklyn/util/task/BasicTask.java
@@ -276,7 +276,7 @@ public class BasicTask<T> implements TaskInternal<T> {
     }
 
     @Override
-    public synchronized boolean cancel() { return cancel(true); }
+    public final synchronized boolean cancel() { return cancel(true); }
 
     /** doesn't resume it, just means if something was cancelled but not submitted it could now be submitted;
      * probably going to be removed and perhaps some mechanism for running again made available

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b8121/core/src/main/java/brooklyn/util/task/ScheduledTask.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/task/ScheduledTask.java b/core/src/main/java/brooklyn/util/task/ScheduledTask.java
index b6b3d9d..27aae30 100644
--- a/core/src/main/java/brooklyn/util/task/ScheduledTask.java
+++ b/core/src/main/java/brooklyn/util/task/ScheduledTask.java
@@ -32,6 +32,7 @@ import brooklyn.management.Task;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.time.Duration;
 
+import com.google.common.annotations.Beta;
 import com.google.common.base.Throwables;
 
 /**
@@ -162,4 +163,21 @@ public class ScheduledTask extends BasicTask {
         blockUntilFirstScheduleStarted();
 		return (truth(recentRun)) ? recentRun.get() : internalFuture.get();
 	}
+	
+	@Override
+	public synchronized boolean cancel(boolean mayInterrupt) {
+	    boolean result = super.cancel(mayInterrupt);
+	    if (nextRun!=null) {
+	        nextRun.cancel(mayInterrupt);
+	        notifyAll();
+	    }
+	    return result;
+	}
+	
+	/** internal method used to allow callers to wait for underlying tasks to finished in the case of cancellation 
+	 * @param duration */ 
+	@Beta
+	public boolean blockUntilNextRunFinished(Duration timeout) {
+	    return Tasks.blockUntilInternalTasksEnded(nextRun, timeout);
+	}
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b8121/core/src/main/java/brooklyn/util/task/Tasks.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/task/Tasks.java b/core/src/main/java/brooklyn/util/task/Tasks.java
index 2afd539..b4bebee 100644
--- a/core/src/main/java/brooklyn/util/task/Tasks.java
+++ b/core/src/main/java/brooklyn/util/task/Tasks.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
 
 import javax.annotation.Nullable;
 
@@ -36,6 +37,9 @@ import brooklyn.management.TaskAdaptable;
 import brooklyn.management.TaskFactory;
 import brooklyn.management.TaskQueueingContext;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.time.CountdownTimer;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
 
 import com.google.common.annotations.Beta;
 import com.google.common.base.Function;
@@ -371,4 +375,51 @@ public class Tasks {
         if (t!=null) TaskTags.addTagDynamically(t, tag);
     }
     
+    /** 
+     * Workaround for limitation described at {@link Task#cancel(boolean)};
+     * internal method used to allow callers to wait for underlying tasks to finished in the case of cancellation.
+     * <p> 
+     * It is irritating that {@link FutureTask} sync's object clears the runner thread, 
+     * so even if {@link BasicTask#getInternalFuture()} is used, there is no means of determining if the underlying object is done.
+     * The {@link Task#getEndTimeUtc()} seems the only way.
+     *  
+     * @return true if tasks ended; false if timed out
+     **/ 
+    @Beta
+    public static boolean blockUntilInternalTasksEnded(Task<?> t, Duration timeout) {
+        CountdownTimer timer = timeout.countdownTimer();
+        
+        if (t==null)
+            return true;
+        
+        if (t instanceof ScheduledTask) {
+            boolean result = ((ScheduledTask)t).blockUntilNextRunFinished(timer.getDurationRemaining());
+            if (!result) return false;
+        }
+
+        t.blockUntilEnded(timer.getDurationRemaining());
+        
+        // TODO when the below is confirmed, delete the code below
+//        Future<?> f = ((BasicTask<?>)t).getInternalFuture();
+//        if (f==null) return;
+//        try {
+//            f.isDone();
+//        } catch (Exception e) {
+//            Exceptions.propagateIfFatal(e);
+//            // ignore
+//        }
+        while (true) {
+            if (t.getEndTimeUtc()>=0) return true;
+            // above should be sufficient; but just in case, trying the below
+            Thread tt = t.getThread();
+            if (tt==null || !tt.isAlive()) {
+                log.warn("Internal task thread is dead or null ("+tt+") but task not ended: "+t.getEndTimeUtc()+" ("+t+")");
+                return true;
+            }
+            if (timer.isExpired())
+                return false;
+            Time.sleep(Duration.millis(10));
+        }
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b8121/core/src/test/java/brooklyn/util/task/ScheduledExecutionTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/util/task/ScheduledExecutionTest.java b/core/src/test/java/brooklyn/util/task/ScheduledExecutionTest.java
index 654168e..31dd5e0 100644
--- a/core/src/test/java/brooklyn/util/task/ScheduledExecutionTest.java
+++ b/core/src/test/java/brooklyn/util/task/ScheduledExecutionTest.java
@@ -25,17 +25,25 @@ import static org.testng.Assert.fail;
 import java.util.List;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import brooklyn.management.Task;
 import brooklyn.test.Asserts;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.exceptions.RuntimeInterruptedException;
+import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
 
+import com.google.common.base.Stopwatch;
 import com.google.common.collect.Lists;
 
 public class ScheduledExecutionTest {
@@ -99,35 +107,138 @@ public class ScheduledExecutionTest {
 		assertEquals(i.get(), 5);
 	}
 
-	@Test
-	public void testScheduledTaskCancelEnding() throws Exception {
-		int PERIOD = 20;
-		BasicExecutionManager m = new BasicExecutionManager("mycontextid");
-		final AtomicInteger i = new AtomicInteger();
-		ScheduledTask t = new ScheduledTask(MutableMap.of("delay", 2*PERIOD, "period", PERIOD), new Callable<Task<?>>() {
+    @Test
+    public void testScheduledTaskCancelEnding() throws Exception {
+        Duration PERIOD = Duration.millis(20);
+        BasicExecutionManager m = new BasicExecutionManager("mycontextid");
+        final AtomicInteger i = new AtomicInteger();
+        ScheduledTask t = new ScheduledTask(MutableMap.of("delay", PERIOD.times(2), "period", PERIOD), new Callable<Task<?>>() {
             public Task<?> call() throws Exception {
                 return new BasicTask<Integer>(new Callable<Integer>() {
                     public Integer call() {
-            			log.info("task running ("+i+"): "+Tasks.current()+" "+Tasks.current().getStatusDetail(false));
-            			ScheduledTask submitter = (ScheduledTask) ((BasicTask)Tasks.current()).getSubmittedByTask();
-            			i.incrementAndGet();
-            			if (i.get() >= 5) submitter.cancel();
-            			return i.get();
+                        log.info("task running ("+i+"): "+Tasks.current()+" "+Tasks.current().getStatusDetail(false));
+                        ScheduledTask submitter = (ScheduledTask) ((BasicTask)Tasks.current()).getSubmittedByTask();
+                        i.incrementAndGet();
+                        if (i.get() >= 5) submitter.cancel();
+                        return i.get();
                     }});
             }});
-	
-		log.info("submitting {} {}", t, t.getStatusDetail(false));
-		m.submit(t);
-		log.info("submitted {} {}", t, t.getStatusDetail(false));
-		Integer interimResult = (Integer) t.get();
-		log.info("done one ({}) {} {}", new Object[] {interimResult, t, t.getStatusDetail(false)});
-		assertTrue(i.get() > 0);
-		t.blockUntilEnded();
-//		int finalResult = t.get()
-		log.info("ended ({}) {} {}", new Object[] {i, t, t.getStatusDetail(false)});
-//		assertEquals(finalResult, 5)
-		assertEquals(i.get(), 5);
-	}
+    
+        log.info(JavaClassNames.niceClassAndMethod()+" - submitting {} {}", t, t.getStatusDetail(false));
+        m.submit(t);
+        log.info("submitted {} {}", t, t.getStatusDetail(false));
+        Integer interimResult = (Integer) t.get();
+        log.info("done one ({}) {} {}", new Object[] {interimResult, t, t.getStatusDetail(false)});
+        assertTrue(i.get() > 0);
+        t.blockUntilEnded();
+//      int finalResult = t.get()
+        log.info("ended ({}) {} {}", new Object[] {i, t, t.getStatusDetail(false)});
+//      assertEquals(finalResult, 5)
+        assertEquals(i.get(), 5);
+    }
+
+    @Test(groups="Integration")
+    public void testScheduledTaskCancelOuter() throws Exception {
+        final Duration PERIOD = Duration.millis(20);
+        final Duration CYCLE_DELAY = Duration.ONE_SECOND;
+        // this should be enough to start the next cycle, but not so much that the cycle ends;
+        // and enough that when a task is interrupted it terminates within this period
+        final Duration SMALL_FRACTION_OF_CYCLE_DELAY = PERIOD.add(CYCLE_DELAY.multiply(0.1));
+        
+        BasicExecutionManager m = new BasicExecutionManager("mycontextid");
+        final AtomicInteger i = new AtomicInteger();
+        ScheduledTask t = new ScheduledTask(MutableMap.of("delay", PERIOD.times(2), "period", PERIOD), new Callable<Task<?>>() {
+            public Task<?> call() throws Exception {
+                return new BasicTask<Integer>(new Callable<Integer>() {
+                    public Integer call() {
+                        log.info("task running ("+i+"): "+Tasks.current()+" "+Tasks.current().getStatusDetail(false));
+                        Time.sleep(CYCLE_DELAY);
+                        i.incrementAndGet();
+                        return i.get();
+                    }});
+            }});
+    
+        log.info(JavaClassNames.niceClassAndMethod()+" - submitting {} {}", t, t.getStatusDetail(false));
+        m.submit(t);
+        log.info("submitted {} {}", t, t.getStatusDetail(false));
+        Integer interimResult = (Integer) t.get();
+        log.info("done one ({}) {} {}", new Object[] {interimResult, t, t.getStatusDetail(false)});
+        assertEquals(i.get(), 1);
+        
+        Time.sleep(SMALL_FRACTION_OF_CYCLE_DELAY);
+        assertEquals(t.get(), 2);
+        
+        Time.sleep(SMALL_FRACTION_OF_CYCLE_DELAY);
+        Stopwatch timer = Stopwatch.createUnstarted();
+        t.cancel(true);
+        t.blockUntilEnded();
+//      int finalResult = t.get()
+        log.info("blocked until ended ({}) {} {}, in {}", new Object[] {i, t, t.getStatusDetail(false), Duration.of(timer)});
+        try {
+            t.get();
+            Assert.fail("Should have failed getting result of cancelled "+t);
+        } catch (Exception e) {
+            /* expected */
+        }
+        assertEquals(i.get(), 2);
+        log.info("ended ({}) {} {}, in {}", new Object[] {i, t, t.getStatusDetail(false), Duration.of(timer)});
+        Assert.assertTrue(Duration.of(timer).isShorterThan(SMALL_FRACTION_OF_CYCLE_DELAY));
+    }
+
+    @Test(groups="Integration")
+    public void testScheduledTaskCancelInterrupts() throws Exception {
+        final Duration PERIOD = Duration.millis(20);
+        final Duration CYCLE_DELAY = Duration.ONE_SECOND;
+        // this should be enough to start the next cycle, but not so much that the cycle ends;
+        // and enough that when a task is interrupted it terminates within this period
+        final Duration SMALL_FRACTION_OF_CYCLE_DELAY = PERIOD.add(CYCLE_DELAY.multiply(0.1));
+        
+        BasicExecutionManager m = new BasicExecutionManager("mycontextid");
+        final Semaphore interruptedSemaphore = new Semaphore(0);
+        final AtomicInteger i = new AtomicInteger();
+        ScheduledTask t = new ScheduledTask(MutableMap.of("delay", PERIOD.times(2), "period", PERIOD), new Callable<Task<?>>() {
+            public Task<?> call() throws Exception {
+                return new BasicTask<Integer>(new Callable<Integer>() {
+                    public Integer call() {
+                        try {
+                            log.info("task running ("+i+"): "+Tasks.current()+" "+Tasks.current().getStatusDetail(false));
+                            Time.sleep(CYCLE_DELAY);
+                            i.incrementAndGet();
+                            return i.get();
+                        } catch (RuntimeInterruptedException e) {
+                            interruptedSemaphore.release();
+                            throw Exceptions.propagate(e);
+                        }
+                    }});
+            }});
+    
+        log.info(JavaClassNames.niceClassAndMethod()+" - submitting {} {}", t, t.getStatusDetail(false));
+        m.submit(t);
+        log.info("submitted {} {}", t, t.getStatusDetail(false));
+        Integer interimResult = (Integer) t.get();
+        log.info("done one ({}) {} {}", new Object[] {interimResult, t, t.getStatusDetail(false)});
+        assertEquals(i.get(), 1);
+        
+        Time.sleep(SMALL_FRACTION_OF_CYCLE_DELAY);
+        assertEquals(t.get(), 2);
+        
+        Time.sleep(SMALL_FRACTION_OF_CYCLE_DELAY);
+        Stopwatch timer = Stopwatch.createUnstarted();
+        t.cancel(true);
+        t.blockUntilEnded();
+//      int finalResult = t.get()
+        log.info("blocked until ended ({}) {} {}, in {}", new Object[] {i, t, t.getStatusDetail(false), Duration.of(timer)});
+        try {
+            t.get();
+            Assert.fail("Should have failed getting result of cancelled "+t);
+        } catch (Exception e) {
+            /* expected */
+        }
+        assertEquals(i.get(), 2);
+        Assert.assertTrue(interruptedSemaphore.tryAcquire(1, SMALL_FRACTION_OF_CYCLE_DELAY.toMilliseconds(), TimeUnit.MILLISECONDS), "child thread was not interrupted");
+        log.info("ended ({}) {} {}, in {}", new Object[] {i, t, t.getStatusDetail(false), Duration.of(timer)});
+        Assert.assertTrue(Duration.of(timer).isShorterThan(SMALL_FRACTION_OF_CYCLE_DELAY));
+    }
 
     @Test(groups="Integration")
     public void testScheduledTaskTakesLongerThanPeriod() throws Exception {


[04/15] git commit: RebindManager is can run in persistence mode or in read-only mode

Posted by he...@apache.org.
RebindManager is can run in persistence mode or in read-only mode


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

Branch: refs/heads/master
Commit: 5188c137dd0ad62b478ce356820ed847c9bb02b1
Parents: c2b05ff
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Fri Sep 26 22:29:00 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:15 2014 +0100

----------------------------------------------------------------------
 .../brooklyn/entity/rebind/RebindManager.java   |  31 +++-
 .../rebind/PeriodicDeltaChangeListener.java     |  12 +-
 .../rebind/RebindExceptionHandlerImpl.java      |   5 +-
 .../entity/rebind/RebindManagerImpl.java        | 180 +++++++++++++++----
 .../BrooklynMementoPersisterToObjectStore.java  |  38 ++--
 .../ha/HighAvailabilityManagerImpl.java         |  19 +-
 .../NonDeploymentManagementContext.java         |  20 +++
 .../entity/rebind/RebindCatalogEntityTest.java  |   2 +-
 .../entity/rebind/RebindEntityTest.java         |   2 +-
 .../entity/rebind/RebindFailuresTest.java       |   2 +-
 .../brooklyn/entity/rebind/RebindTestUtils.java |   6 +-
 ...BrooklynMementoPersisterToMultiFileTest.java |   2 +-
 .../brooklyn/management/ha/HotStandbyTest.java  | 148 +++++++++++++++
 .../AbstractWebAppFixtureIntegrationTest.java   |   2 +-
 .../brooklyn/launcher/BrooklynLauncher.java     |   2 +-
 .../brooklyn/util/exceptions/Exceptions.java    |  30 ++--
 16 files changed, 419 insertions(+), 82 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
index 5c5d972..8bbdcaa 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
@@ -24,7 +24,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
 import brooklyn.entity.Application;
-import brooklyn.mementos.BrooklynMemento;
+import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.mementos.BrooklynMementoPersister;
 import brooklyn.mementos.BrooklynMementoRawData;
 import brooklyn.util.time.Duration;
@@ -72,15 +72,32 @@ public interface RebindManager {
     public ChangeListener getChangeListener();
 
     /**
-     * Starts the persisting of state (if persister is set; otherwise will start persisting as soon as
-     * persister is set). Until {@link #start()} is called, no data will be persisted but entities can 
-     * rebind.
+     * Starts the background persisting of state
+     * (if persister is set; otherwise will start persisting as soon as persister is set). 
+     * Until this is called, no data will be persisted although entities can be rebinded.
      */
-    public void start();
+    public void startPersistence();
 
-    /** Stops persisting. Waits for any current persistence to complete. */
-    public void stop();
+    /** Stops the background persistence of state. 
+     * Waits for any current persistence to complete. */
+    public void stopPersistence();
 
+    /**
+     * Perform an initial load of state read-only and starts a background process 
+     * reading (mirroring) state periodically.
+     */
+    public void startReadOnly();
+    /** Stops the background reading (mirroring) of state. 
+     * Interrupts any current activity and waits for it to cease. */
+    public void stopReadOnly();
+    
+    /** Starts the appropriate background processes, {@link #startPersistence()} if {@link ManagementNodeState#MASTER},
+     * {@link #startReadOnly()} if {@link ManagementNodeState#HOT_STANDBY} */
+    public void start();
+    /** Stops the appropriate background processes, {@link #stopPersistence()} or {@link #stopReadOnly()},
+     * waiting for activity there to cease (interrupting in the case of {@link #stopReadOnly()}). */
+    public void stop();
+    
     /** @deprecated since 0.7.0; use {@link #waitForPendingComplete(Duration)} */
     @VisibleForTesting
     @Deprecated

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
index a20a158..0d708f4 100644
--- a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
+++ b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
@@ -117,11 +117,17 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
     
     private final Semaphore persistingMutex = new Semaphore(1);
     
+    /** @deprecated since 0.7.0 pass in a {@link Duration} */
+    @Deprecated
     public PeriodicDeltaChangeListener(ExecutionManager executionManager, BrooklynMementoPersister persister, PersistenceExceptionHandler exceptionHandler, long periodMillis) {
+        this(executionManager, persister, exceptionHandler, Duration.of(periodMillis, TimeUnit.MILLISECONDS));
+    }
+    
+    public PeriodicDeltaChangeListener(ExecutionManager executionManager, BrooklynMementoPersister persister, PersistenceExceptionHandler exceptionHandler, Duration period) {
         this.executionManager = executionManager;
         this.persister = persister;
         this.exceptionHandler = exceptionHandler;
-        this.period = Duration.of(periodMillis, TimeUnit.MILLISECONDS);
+        this.period = period;
         
         this.persistPoliciesEnabled = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_POLICY_PERSISTENCE_PROPERTY);
         this.persistEnrichersEnabled = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_ENRICHER_PERSISTENCE_PROPERTY);
@@ -130,6 +136,10 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
     
     @SuppressWarnings("unchecked")
     public void start() {
+        if (running || (scheduledTask!=null && !scheduledTask.isDone())) {
+            LOG.warn("Request to start "+this+" when already running - "+scheduledTask+"; ignoring");
+            return;
+        }
         running = true;
         
         Callable<Task<?>> taskFactory = new Callable<Task<?>>() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/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 ad96961..25523c6 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
@@ -36,7 +36,6 @@ import brooklyn.entity.rebind.RebindManager.RebindFailureMode;
 import brooklyn.location.Location;
 import brooklyn.policy.Enricher;
 import brooklyn.policy.Policy;
-import brooklyn.util.exceptions.CompoundRuntimeException;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.text.Strings;
 
@@ -375,8 +374,8 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
         if (allExceptions.isEmpty()) {
             return; // no errors
         } else {
-            CompoundRuntimeException compoundException = new CompoundRuntimeException("Problem"+(allExceptions.size() == 1 ? "" : "s")+" rebinding", allExceptions);
-            LOG.info("RebindManager failed (throwing): "+compoundException.toString());
+            RuntimeException compoundException = Exceptions.create("Failure rebinding", allExceptions);
+            LOG.debug(compoundException.getMessage()+" (rethrowing)");
             throw compoundException;
         }
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/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 f24612c..70a2ba7 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -58,6 +58,8 @@ import brooklyn.location.Location;
 import brooklyn.location.basic.AbstractLocation;
 import brooklyn.location.basic.LocationInternal;
 import brooklyn.management.Task;
+import brooklyn.management.ha.HighAvailabilityManagerImpl;
+import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.mementos.BrooklynMemento;
 import brooklyn.mementos.BrooklynMementoManifest;
@@ -76,12 +78,16 @@ import brooklyn.policy.Policy;
 import brooklyn.policy.basic.AbstractPolicy;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.exceptions.RuntimeInterruptedException;
 import brooklyn.util.flags.FlagUtils;
 import brooklyn.util.javalang.Reflections;
 import brooklyn.util.task.BasicExecutionContext;
+import brooklyn.util.task.BasicTask;
+import brooklyn.util.task.ScheduledTask;
 import brooklyn.util.text.Strings;
 import brooklyn.util.time.Duration;
 
+import com.google.api.client.repackaged.com.google.common.base.Preconditions;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Optional;
 import com.google.common.collect.ImmutableMap;
@@ -114,16 +120,17 @@ public class RebindManagerImpl implements RebindManager {
     
     public static final Logger LOG = LoggerFactory.getLogger(RebindManagerImpl.class);
 
-    private volatile Duration periodicPersistPeriod = Duration.ONE_SECOND;
-    
-    private volatile boolean running = false;
-    
     private final ManagementContextInternal managementContext;
-
-    private volatile PeriodicDeltaChangeListener realChangeListener;
-    private volatile ChangeListener changeListener;
     
+    private volatile Duration periodicPersistPeriod = Duration.ONE_SECOND;
+    
+    private volatile boolean persistenceRunning = false;
+    private volatile PeriodicDeltaChangeListener persistenceRealChangeListener;
+    private volatile ChangeListener persistencePublicChangeListener;
     private volatile BrooklynMementoPersister persister;
+    
+    private volatile boolean readOnlyRunning = false;
+    private volatile ScheduledTask readOnlyTask = null;
 
     private final boolean persistPoliciesEnabled;
     private final boolean persistEnrichersEnabled;
@@ -160,7 +167,7 @@ public class RebindManagerImpl implements RebindManager {
 
     public RebindManagerImpl(ManagementContextInternal managementContext) {
         this.managementContext = managementContext;
-        this.changeListener = ChangeListener.NOOP;
+        this.persistencePublicChangeListener = ChangeListener.NOOP;
         
         this.persistPoliciesEnabled = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_POLICY_PERSISTENCE_PROPERTY);
         this.persistEnrichersEnabled = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_ENRICHER_PERSISTENCE_PROPERTY);
@@ -205,11 +212,11 @@ public class RebindManagerImpl implements RebindManager {
         }
         this.persister = checkNotNull(val, "persister");
         
-        this.realChangeListener = new PeriodicDeltaChangeListener(managementContext.getExecutionManager(), persister, exceptionHandler, periodicPersistPeriod.toMilliseconds());
-        this.changeListener = new SafeChangeListener(realChangeListener);
+        this.persistenceRealChangeListener = new PeriodicDeltaChangeListener(managementContext.getExecutionManager(), persister, exceptionHandler, periodicPersistPeriod);
+        this.persistencePublicChangeListener = new SafeChangeListener(persistenceRealChangeListener);
         
-        if (running) {
-            realChangeListener.start();
+        if (persistenceRunning) {
+            persistenceRealChangeListener.start();
         }
     }
 
@@ -220,16 +227,113 @@ public class RebindManagerImpl implements RebindManager {
     }
     
     @Override
-    public void start() {
-        running = true;
-        if (realChangeListener != null) realChangeListener.start();
+    public void startPersistence() {
+        if (readOnlyRunning) {
+            throw new IllegalStateException("Cannot start read-only when already running with persistence");
+        }
+        LOG.debug("Starting persistence, mgmt "+managementContext.getManagementNodeId());
+        persistenceRunning = true;
+        if (persistenceRealChangeListener != null) persistenceRealChangeListener.start();
     }
     
     @Override
-    public void stop() {
-        running = false;
-        if (realChangeListener != null) realChangeListener.stop();
+    public void stopPersistence() {
+        LOG.debug("Stopping persistence, mgmt "+managementContext.getManagementNodeId());
+        persistenceRunning = false;
+        if (persistenceRealChangeListener != null) persistenceRealChangeListener.stop();
         if (persister != null) persister.stop(true);
+        LOG.debug("Stopped persistence, mgmt "+managementContext.getManagementNodeId());
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Override
+    public void startReadOnly() {
+        if (persistenceRunning) {
+            throw new IllegalStateException("Cannot start read-only when already running with persistence");
+        }
+        if (readOnlyRunning || readOnlyTask!=null) {
+            LOG.warn("Cannot request read-only mode for "+this+" when already running - "+readOnlyTask+"; ignoring");
+            return;
+        }
+        LOG.debug("Starting read-only rebinding, mgmt "+managementContext.getManagementNodeId());
+        readOnlyRunning = true;
+        
+        try {
+            rebind(null, null, ManagementNodeState.HOT_STANDBY);
+        } catch (Exception e) {
+            Exceptions.propagate(e);
+        }
+        
+        Callable<Task<?>> taskFactory = new Callable<Task<?>>() {
+            @Override public Task<Void> call() {
+                return new BasicTask<Void>(new Callable<Void>() {
+                    public Void call() {
+                        try {
+                            rebind(null, null, ManagementNodeState.HOT_STANDBY);
+                            return null;
+                        } catch (RuntimeInterruptedException e) {
+                            LOG.debug("Interrupted rebinding (re-interrupting): "+e);
+                            if (LOG.isTraceEnabled())
+                                LOG.trace("Interrupted rebinding (re-interrupting): "+e, e);
+                            Thread.currentThread().interrupt();
+                            return null;
+                        } catch (Exception e) {
+                            // Don't rethrow: the behaviour of executionManager is different from a scheduledExecutorService,
+                            // if we throw an exception, then our task will never get executed again
+                            if (!readOnlyRunning) {
+                                if (LOG.isTraceEnabled())
+                                    LOG.trace("Problem rebinding (read-only running turned off): "+e, e);
+                            } else {
+                                LOG.warn("Problem rebinding: "+Exceptions.collapseText(e), e);
+                            }
+                            return null;
+                        } catch (Throwable t) {
+                            LOG.warn("Problem rebinding (rethrowing)", t);
+                            throw Exceptions.propagate(t);
+                        }
+                    }});
+            }
+        };
+        readOnlyTask = (ScheduledTask) managementContext.getExecutionManager().submit(
+            new ScheduledTask(taskFactory).period(periodicPersistPeriod));
+    }
+    
+    @Override
+    public void stopReadOnly() {
+        LOG.debug("Stopping read-only rebinding, mgmt "+managementContext.getManagementNodeId());
+        readOnlyRunning = false;
+        if (readOnlyTask!=null) {
+            readOnlyTask.cancel(true);
+            readOnlyTask.blockUntilEnded();
+            readOnlyTask = null;
+        }
+        LOG.debug("Stopped read-only rebinding, mgmt "+managementContext.getManagementNodeId());
+    }
+    
+    @Override
+    public void start() {
+        ManagementNodeState target = getRebindMode();
+        if (target==ManagementNodeState.HOT_STANDBY) {
+            startReadOnly();
+        } else if (target==ManagementNodeState.MASTER) {
+            startPersistence();
+        } else {
+            LOG.warn("Nothing to start in "+this+" when HA mode is "+target);
+        }
+    }
+
+    @Override
+    public void stop() {
+        stopReadOnly();
+        stopPersistence();
+    }
+    
+    protected ManagementNodeState getRebindMode() {
+        if (managementContext==null) throw new IllegalStateException("Invalid "+this+": no management context");
+        if (!(managementContext.getHighAvailabilityManager() instanceof HighAvailabilityManagerImpl))
+            throw new IllegalStateException("Invalid "+this+": unknown HA manager type "+managementContext.getHighAvailabilityManager());
+        ManagementNodeState target = ((HighAvailabilityManagerImpl)managementContext.getHighAvailabilityManager()).getTransitionTargetNodeState();
+        return target;
     }
     
     @Override
@@ -240,45 +344,54 @@ public class RebindManagerImpl implements RebindManager {
     @Override
     @VisibleForTesting
     public void waitForPendingComplete(Duration timeout) throws InterruptedException, TimeoutException {
-        if (persister == null || !running) return;
-        realChangeListener.waitForPendingComplete(timeout);
+        if (persister == null || !persistenceRunning) return;
+        persistenceRealChangeListener.waitForPendingComplete(timeout);
         persister.waitForWritesCompleted(timeout);
     }
     @Override
     @VisibleForTesting
     public void forcePersistNow() {
-        realChangeListener.persistNow();
+        persistenceRealChangeListener.persistNow();
     }
     
     @Override
     public ChangeListener getChangeListener() {
-        return changeListener;
+        return persistencePublicChangeListener;
     }
     
     @Override
     public List<Application> rebind() throws IOException {
-        return rebind(managementContext.getCatalog().getRootClassLoader());
+        return rebind(null, null, null);
     }
     
     @Override
     public List<Application> rebind(final ClassLoader classLoader) throws IOException {
-        RebindExceptionHandler exceptionHandler = RebindExceptionHandlerImpl.builder()
+        return rebind(classLoader, null);
+    }
+
+    @Override
+    public List<Application> rebind(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler) throws IOException {
+        return rebind(classLoader, exceptionHandler, null);
+    }
+    
+    protected List<Application> rebind(ClassLoader classLoaderO, RebindExceptionHandler exceptionHandlerO, ManagementNodeState modeO) throws IOException {
+        final ClassLoader classLoader = classLoaderO!=null ? classLoaderO :
+            managementContext.getCatalog().getRootClassLoader();
+        final RebindExceptionHandler exceptionHandler = exceptionHandlerO!=null ? exceptionHandlerO :
+            RebindExceptionHandlerImpl.builder()
                 .danglingRefFailureMode(danglingRefFailureMode)
                 .rebindFailureMode(rebindFailureMode)
                 .addPolicyFailureMode(addPolicyFailureMode)
                 .loadPolicyFailureMode(loadPolicyFailureMode)
                 .build();
-        return rebind(classLoader, exceptionHandler);
-    }
-
-    @Override
-    public List<Application> rebind(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler) throws IOException {
+        final ManagementNodeState mode = modeO!=null ? modeO : getRebindMode();
+        
         BasicExecutionContext ec = BasicExecutionContext.getCurrentExecutionContext();
         if (ec == null) {
             ec = new BasicExecutionContext(managementContext.getExecutionManager());
             Task<List<Application>> task = ec.submit(new Callable<List<Application>>() {
                 @Override public List<Application> call() throws Exception {
-                    return rebindImpl(classLoader, exceptionHandler);
+                    return rebindImpl(classLoader, exceptionHandler, mode);
                 }});
             try {
                 return task.get();
@@ -286,7 +399,7 @@ public class RebindManagerImpl implements RebindManager {
                 throw Exceptions.propagate(e);
             }
         } else {
-            return rebindImpl(classLoader, exceptionHandler);
+            return rebindImpl(classLoader, exceptionHandler, mode);
         }
     }
     
@@ -320,8 +433,10 @@ public class RebindManagerImpl implements RebindManager {
         }
     }
 
-    protected List<Application> rebindImpl(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler) throws IOException {
+    protected List<Application> rebindImpl(final ClassLoader classLoader, final RebindExceptionHandler exceptionHandler, ManagementNodeState mode) throws IOException {
         checkNotNull(classLoader, "classLoader");
+        boolean isReadOnly = mode==ManagementNodeState.HOT_STANDBY;
+        if (!isReadOnly) Preconditions.checkState(mode==ManagementNodeState.MASTER, "Must be either master or read only to rebind (mode "+mode+")");
 
         RebindTracker.setRebinding();
         try {
@@ -649,6 +764,7 @@ public class RebindManagerImpl implements RebindManager {
                     exceptionHandler.onNotFound(BrooklynObjectType.ENTITY, appId);
                 } else {
                     try {
+                        // TODO if read only we must do something special here!
                         Entities.startManagement((Application)entity, managementContext);
                     } catch (Exception e) {
                         exceptionHandler.onManageFailed(BrooklynObjectType.ENTITY, entity, e);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
index 0b95e47..5b224b1 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
@@ -102,6 +102,7 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
     private final ListeningExecutorService executor;
 
     private volatile boolean running = true;
+    private volatile boolean shuttingDown = false;
 
     /**
      * Lock used on writes (checkpoint + delta) so that {@link #waitForWritesCompleted(Duration)} can block
@@ -136,20 +137,25 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
     @Override
     public void stop(boolean graceful) {
-        running = false;
-        if (executor != null) {
-            if (graceful) {
-                // a very long timeout to ensure we don't lose state. 
-                // If persisting thousands of entities over slow network to Object Store, could take minutes.
-                executor.shutdown();
-                try {
-                    executor.awaitTermination(1, TimeUnit.HOURS);
-                } catch (InterruptedException e) {
-                    throw Exceptions.propagate(e);
+        shuttingDown = true;
+        try {
+            running = false;
+            if (executor != null) {
+                if (graceful) {
+                    // a very long timeout to ensure we don't lose state. 
+                    // If persisting thousands of entities over slow network to Object Store, could take minutes.
+                    executor.shutdown();
+                    try {
+                        executor.awaitTermination(1, TimeUnit.HOURS);
+                    } catch (InterruptedException e) {
+                        throw Exceptions.propagate(e);
+                    }
+                } else {
+                    executor.shutdownNow();
                 }
-            } else {
-                executor.shutdownNow();
             }
+        } finally {
+            shuttingDown = false;
         }
     }
     
@@ -325,7 +331,7 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
         public void visit(String contents, BrooklynObjectType type, String subPath) throws Exception;
     }
     protected void visitMemento(final Visitor visitor, final RebindExceptionHandler exceptionHandler) throws IOException {
-        if (!running) {
+        if (!running && !shuttingDown) {
             throw new IllegalStateException("Persister not running; cannot load memento manifest from " + objectStore.getSummaryName());
         }
 
@@ -425,7 +431,7 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
     @Beta
     public void checkpoint(BrooklynMementoRawData newMemento, PersistenceExceptionHandler exceptionHandler) {
-        if (!running) {
+        if (!running && !shuttingDown) {
             if (LOG.isDebugEnabled()) LOG.debug("Ignoring checkpointing entire memento, because not running");
             return;
         }
@@ -480,7 +486,7 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
     @Override
     public void checkpoint(BrooklynMemento newMemento, PersistenceExceptionHandler exceptionHandler) {
-        if (!running) {
+        if (!running && !shuttingDown) {
             if (LOG.isDebugEnabled()) LOG.debug("Ignoring checkpointing entire memento, because not running");
             return;
         }
@@ -500,7 +506,7 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
     @Override
     public void delta(Delta delta, PersistenceExceptionHandler exceptionHandler) {
-        if (!running) {
+        if (!running && !shuttingDown) {
             if (LOG.isDebugEnabled()) LOG.debug("Ignoring checkpointed delta of memento, because not running");
             return;
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/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 2d7bf9f..d63aa99 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -51,6 +51,7 @@ import brooklyn.util.task.BasicTask;
 import brooklyn.util.task.ScheduledTask;
 import brooklyn.util.time.Duration;
 
+import com.google.api.client.repackaged.com.google.common.base.Preconditions;
 import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
@@ -98,6 +99,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
 
     // TODO Should we pass in a classloader on construction, so it can be passed to {@link RebindManager#rebind(ClassLoader)} 
     
+    @VisibleForTesting /* only used in tests currently */
     public static interface PromotionListener {
         public void promotingToMaster();
     }
@@ -602,8 +604,21 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     /** starts hot standby, in foreground; the caller is responsible for publishing health afterwards.
      * @return whether hot standby was possible (if not, errors should be stored elsewhere) */
     protected boolean attemptHotStandby() {
-        nodeState = ManagementNodeState.HOT_STANDBY;
-        return true;
+        try {
+            Preconditions.checkState(nodeStateTransitionComplete==false, "Must be in transitioning state to go into hot standby");
+            nodeState = ManagementNodeState.HOT_STANDBY;
+            
+            // XXX
+            managementContext.getRebindManager().startReadOnly();
+            
+            return true;
+        } catch (Exception e) {
+            Exceptions.propagateIfFatal(e);
+            // TODO maybe move to failure state?  and record problems somewhere else for investigation subsequently.
+            nodeState = ManagementNodeState.STANDBY;
+            LOG.warn("Unable to promote "+ownNodeId+" to hot standby: "+e, e);
+            return false;
+        }
     }
     
     /**

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index cc2c916..a404bb8 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -447,6 +447,26 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
         }
 
         @Override
+        public void startPersistence() {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
+        
+        @Override
+        public void stopPersistence() {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
+
+        @Override
+        public void startReadOnly() {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
+        
+        @Override
+        public void stopReadOnly() {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
+
+        @Override
         public void start() {
             throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
index ad484b3..e482577 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
@@ -129,7 +129,7 @@ public class RebindCatalogEntityTest extends RebindTestFixture<StartableApplicat
         
         ClassLoader classLoader = newManagementContext.getCatalog().getRootClassLoader();
         List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader);
-        newManagementContext.getRebindManager().start();
+        newManagementContext.getRebindManager().startPersistence();
         return (StartableApplication) newApps.get(0);
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
index 1886619..fed6dd9 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
@@ -579,7 +579,7 @@ public class RebindEntityTest extends RebindTestFixtureWithApp {
         RebindTestUtils.waitForPersisted(origManagementContext);
         newManagementContext = RebindTestUtils.newPersistingManagementContextUnstarted(mementoDir, classLoader);
         List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader);
-        newManagementContext.getRebindManager().start();
+        newManagementContext.getRebindManager().startPersistence();
         
         assertEquals(newApps.size(), 0, "apps="+newApps);
         assertEquals(newManagementContext.getApplications().size(), 0, "apps="+newManagementContext.getApplications());

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
index 1fe4007..9714dc9 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
@@ -160,7 +160,7 @@ public class RebindFailuresTest extends RebindTestFixtureWithApp {
         RebindFailureMode danglingRefFailureMode = RebindManager.RebindFailureMode.CONTINUE;
         RebindFailureMode rebindFailureMode = RebindManager.RebindFailureMode.FAIL_AT_END;
         
-        origManagementContext.getRebindManager().stop();
+        origManagementContext.getRebindManager().stopPersistence();
         if (mementoDir != null) RebindTestUtils.deleteMementoDir(mementoDir);
         File entitiesDir = Os.mkdirs(new File(mementoDir, "entities"));
         Files.write("invalid text", new File(entitiesDir, "mycorruptfile"), Charsets.UTF_8);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java b/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
index 7993c33..65738a7 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
@@ -211,7 +211,7 @@ public class RebindTestUtils {
         public LocalManagementContext buildStarted() {
             LocalManagementContext unstarted = buildUnstarted();
             unstarted.getHighAvailabilityManager().disabled();
-            unstarted.getRebindManager().start();
+            unstarted.getRebindManager().startPersistence();
             return unstarted;
         }
 
@@ -256,7 +256,7 @@ public class RebindTestUtils {
             newApps = newManagementContext.getRebindManager().rebind(classLoader, exceptionHandler);
         }
         if (newApps.isEmpty()) throw new IllegalStateException("Application could not be rebinded; serialization probably failed");
-        newManagementContext.getRebindManager().start();
+        newManagementContext.getRebindManager().startPersistence();
         return newApps;
     }
 
@@ -292,7 +292,7 @@ public class RebindTestUtils {
         } else {
             newApps = newManagementContext.getRebindManager().rebind(classLoader, exceptionHandler);
         }
-        newManagementContext.getRebindManager().start();
+        newManagementContext.getRebindManager().startPersistence();
         return newApps;
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFileTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFileTest.java b/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFileTest.java
index d4edd41..5449791 100644
--- a/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFileTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFileTest.java
@@ -48,7 +48,7 @@ public class BrooklynMementoPersisterToMultiFileTest extends BrooklynMementoPers
         persister = new BrooklynMementoPersisterToMultiFile(mementoDir, BrooklynMementoPersisterToMultiFileTest.class.getClassLoader());
         mgmt.getRebindManager().setPersister(persister, PersistenceExceptionHandlerImpl.builder().build());
         mgmt.getHighAvailabilityManager().disabled();
-        mgmt.getRebindManager().start();
+        mgmt.getRebindManager().startPersistence();
         return mgmt;
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/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
new file mode 100644
index 0000000..e4219f7
--- /dev/null
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.management.ha;
+
+import static org.testng.Assert.assertEquals;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.rebind.PersistenceExceptionHandlerImpl;
+import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
+import brooklyn.entity.rebind.persister.InMemoryObjectStore;
+import brooklyn.entity.rebind.persister.ListeningObjectStore;
+import brooklyn.entity.rebind.persister.PersistMode;
+import brooklyn.entity.rebind.persister.PersistenceObjectStore;
+import brooklyn.location.Location;
+import brooklyn.management.internal.ManagementContextInternal;
+import brooklyn.test.entity.LocalManagementContextForTests;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.test.entity.TestEntity;
+import brooklyn.util.collections.MutableList;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.time.Duration;
+
+@Test
+public class HotStandbyTest {
+
+    private static final Logger log = LoggerFactory.getLogger(HotStandbyTest.class);
+    
+    private List<HaMgmtNode> nodes = new MutableList<HotStandbyTest.HaMgmtNode>();
+    Map<String,String> sharedBackingStore = MutableMap.of();
+    Map<String,Date> sharedBackingStoreDates = MutableMap.of();
+    private ClassLoader classLoader = getClass().getClassLoader();
+    
+    public class HaMgmtNode {
+        
+        private ManagementContextInternal mgmt;
+        private String ownNodeId;
+        private String nodeName;
+        private ListeningObjectStore objectStore;
+        private ManagementPlaneSyncRecordPersister persister;
+        private HighAvailabilityManagerImpl ha;
+
+        @BeforeMethod(alwaysRun=true)
+        public void setUp() throws Exception {
+            nodeName = "node "+nodes.size();
+            mgmt = newLocalManagementContext();
+            ownNodeId = mgmt.getManagementNodeId();
+            objectStore = new ListeningObjectStore(newPersistenceObjectStore());
+            objectStore.injectManagementContext(mgmt);
+            objectStore.prepareForSharedUse(PersistMode.CLEAN, HighAvailabilityMode.DISABLED);
+            persister = new ManagementPlaneSyncRecordPersisterToObjectStore(mgmt, objectStore, classLoader);
+            ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
+            BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
+            mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
+            ha = ((HighAvailabilityManagerImpl)mgmt.getHighAvailabilityManager())
+                .setPollPeriod(Duration.PRACTICALLY_FOREVER)
+                .setHeartbeatTimeout(Duration.THIRTY_SECONDS)
+                .setPersister(persister);
+            log.info("Created "+nodeName+" "+ownNodeId+" - RM "+mgmt.getRebindManager());
+        }
+        
+        public void tearDown() throws Exception {
+            if (ha != null) ha.stop();
+            if (mgmt != null) Entities.destroyAll(mgmt);
+            if (objectStore != null) objectStore.deleteCompletely();
+        }
+        
+        @Override
+        public String toString() {
+            return nodeName+" "+ownNodeId;
+        }
+    }
+    
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        nodes.clear();
+        sharedBackingStore.clear();
+    }
+    
+    public HaMgmtNode newNode() throws Exception {
+        HaMgmtNode node = new HaMgmtNode();
+        node.setUp();
+        nodes.add(node);
+        return node;
+    }
+
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        for (HaMgmtNode n: nodes)
+            n.tearDown();
+    }
+
+    protected ManagementContextInternal newLocalManagementContext() {
+        return new LocalManagementContextForTests();
+    }
+
+    protected PersistenceObjectStore newPersistenceObjectStore() {
+        return new InMemoryObjectStore(sharedBackingStore, sharedBackingStoreDates);
+    }
+
+    
+    
+    @Test
+    public void testHotStandby() throws Exception {
+        HaMgmtNode n1 = newNode();
+        n1.ha.start(HighAvailabilityMode.AUTO);
+        assertEquals(n1.ha.getNodeState(), ManagementNodeState.MASTER);
+        
+        TestApplication app = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        app.start(MutableList.<Location>of());
+        app.setAttribute(TestEntity.SEQUENCE, 3);
+        
+        n1.mgmt.getRebindManager().forcePersistNow();
+
+        HaMgmtNode n2 = newNode();
+        n2.ha.start(HighAvailabilityMode.AUTO);
+        assertEquals(n2.ha.getNodeState(), ManagementNodeState.HOT_STANDBY);
+
+        assertEquals(n2.mgmt.getApplications().size(), 1);
+    }
+    
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java b/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
index 2b358d0..b8c14d7 100644
--- a/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
+++ b/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
@@ -227,7 +227,7 @@ public abstract class AbstractWebAppFixtureIntegrationTest {
             newManagementContext = new LocalManagementContextForTests();
             newManagementContext.getRebindManager().setPersister(newPersister, PersistenceExceptionHandlerImpl.builder().build());
             newManagementContext.getRebindManager().rebind(getClass().getClassLoader());
-            newManagementContext.getRebindManager().start();
+            newManagementContext.getRebindManager().startPersistence();
             SoftwareProcess entity2 = (SoftwareProcess) newManagementContext.getEntityManager().getEntity(tokill.getId());
             entity2.stop();
         } finally {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
----------------------------------------------------------------------
diff --git a/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java b/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
index 47d80a4..bea6c2e 100644
--- a/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
+++ b/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
@@ -790,7 +790,7 @@ public class BrooklynLauncher {
             throw new FatalRuntimeException("Error rebinding to persisted state: "+
                 Exceptions.collapseText(e), e);
         }
-        rebindManager.start();
+        rebindManager.startPersistence();
     }
 
     protected void createApps() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/5188c137/utils/common/src/main/java/brooklyn/util/exceptions/Exceptions.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/exceptions/Exceptions.java b/utils/common/src/main/java/brooklyn/util/exceptions/Exceptions.java
index 53fb816..81831d9 100644
--- a/utils/common/src/main/java/brooklyn/util/exceptions/Exceptions.java
+++ b/utils/common/src/main/java/brooklyn/util/exceptions/Exceptions.java
@@ -196,25 +196,31 @@ public class Exceptions {
         return t2.toString();
     }
 
-    public static RuntimeException propagate(String prefix, Collection<? extends Throwable> exceptions) {
-        if (exceptions.size()==1)
-            throw new CompoundRuntimeException(prefix + ": " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
-        if (exceptions.isEmpty())
-            throw new CompoundRuntimeException(prefix, exceptions);
-        throw new CompoundRuntimeException(prefix + ", including: " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
-    }
 
     public static RuntimeException propagate(Collection<? extends Throwable> exceptions) {
         throw propagate(create(exceptions));
     }
+    public static RuntimeException propagate(String prefix, Collection<? extends Throwable> exceptions) {
+        throw propagate(create(prefix, exceptions));
+    }
 
     /** creates the given exception, but without propagating it, for use when caller will be wrapping */
     public static Throwable create(Collection<? extends Throwable> exceptions) {
-        if (exceptions.size()==1)
-            return exceptions.iterator().next();
-        if (exceptions.isEmpty())
-            return new CompoundRuntimeException("(empty compound exception)", exceptions);
-        return new CompoundRuntimeException(exceptions.size()+" errors, including: " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
+        return create(null, exceptions);
+    }
+    /** creates the given exception, but without propagating it, for use when caller will be wrapping */
+    public static RuntimeException create(String prefix, Collection<? extends Throwable> exceptions) {
+        if (exceptions.size()==1) {
+            Throwable e = exceptions.iterator().next();
+            if (Strings.isBlank(prefix)) return new PropagatedRuntimeException(e);
+            return new PropagatedRuntimeException(prefix + ": " + Exceptions.collapseText(e), e);
+        }
+        if (exceptions.isEmpty()) {
+            if (Strings.isBlank(prefix)) return new CompoundRuntimeException("(empty compound exception)", exceptions);
+            return new CompoundRuntimeException(prefix+": (empty compound exception)", exceptions);
+        }
+        if (Strings.isBlank(prefix)) return new CompoundRuntimeException(exceptions.size()+" errors, including: " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
+        return new CompoundRuntimeException(prefix+", "+exceptions.size()+" errors including: " + Exceptions.collapseText(exceptions.iterator().next()), exceptions);
     }
 
 }


[09/15] Ensure BrooklynObject management, remanagement, and unmanagement, by replacing Location instances, refreshing the catalog, and swapping out the delegate but keeping the entity proxy.

Posted by he...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/LocationManagerInternal.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocationManagerInternal.java b/core/src/main/java/brooklyn/management/internal/LocationManagerInternal.java
new file mode 100644
index 0000000..96e2ff3
--- /dev/null
+++ b/core/src/main/java/brooklyn/management/internal/LocationManagerInternal.java
@@ -0,0 +1,40 @@
+/*
+ * 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.management.internal;
+
+import brooklyn.location.Location;
+import brooklyn.management.LocationManager;
+import brooklyn.management.internal.ManagementTransitionInfo.ManagementTransitionMode;
+
+public interface LocationManagerInternal extends LocationManager {
+
+    public Iterable<String> getLocationIds();
+
+    ManagementTransitionMode getLastManagementTransitionMode(String itemId);
+    void setManagementTransitionMode(Location item, ManagementTransitionMode mode);
+
+    /** 
+     * Begins management for the given rebinded root, recursively; 
+     * if rebinding as a read-only copy, {@link #setReadOnly(Location, boolean)} should be called prior to this.
+     */
+    void manageRebindedRoot(Location item);
+
+    void unmanage(final Location item, final ManagementTransitionMode info);
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/ManagementTransitionInfo.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/ManagementTransitionInfo.java b/core/src/main/java/brooklyn/management/internal/ManagementTransitionInfo.java
index 170652f..06cd2e4 100644
--- a/core/src/main/java/brooklyn/management/internal/ManagementTransitionInfo.java
+++ b/core/src/main/java/brooklyn/management/internal/ManagementTransitionInfo.java
@@ -31,12 +31,30 @@ public class ManagementTransitionInfo {
      */
 
     public enum ManagementTransitionMode {
-        /** Entity is being created fresh, for the first time, or stopping permanently */ 
-        NORMAL,
-        /** Entity management is moving from one location to another (ie stopping at one location / starting at another) */
-        MIGRATORY, 
-        /** Entity is being created, from a serialized/specified state */
-        REBIND
+        /** Item is being created fresh, for the first time */ 
+        CREATING(false, false),
+        /** Item is being destroyed / stopping permanently */ 
+        DESTROYING(false, false),
+        
+        /** Item is being mirrored (or refreshed) here from a serialized/specified state */
+        REBINDING_READONLY(true, true),
+        /** Item management is stopping here, going elsewhere */
+        REBINDING_NO_LONGER_PRIMARY(false, true), 
+        /** Item management is starting here, having previously been running elsewhere */
+        REBINDING_BECOMING_PRIMARY(true, false),
+        /** Item was being mirrored but has now been destroyed  */
+        REBINDING_DESTROYED(true, true);
+        
+        private boolean wasReadOnly;
+        private boolean isReadOnly;
+
+        ManagementTransitionMode(boolean wasReadOnly, boolean isReadOnly) {
+            this.wasReadOnly = wasReadOnly;
+            this.isReadOnly = isReadOnly;
+        }
+        
+        public boolean wasReadOnly() { return wasReadOnly; }
+        public boolean isReadOnly() { return isReadOnly; }
     }
     
     public ManagementTransitionInfo(ManagementContext mgmtContext, ManagementTransitionMode mode) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
index f6cc2ef..c3720b1 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentEntityManager.java
@@ -27,6 +27,7 @@ import brooklyn.entity.Entity;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.proxying.EntityTypeRegistry;
 import brooklyn.management.ManagementContext;
+import brooklyn.management.internal.ManagementTransitionInfo.ManagementTransitionMode;
 import brooklyn.policy.Enricher;
 import brooklyn.policy.EnricherSpec;
 import brooklyn.policy.Policy;
@@ -129,6 +130,33 @@ public class NonDeploymentEntityManager implements EntityManagerInternal {
     }
 
     @Override
+    public Iterable<String> getEntityIds() {
+        if (isInitialManagementContextReal()) {
+            return ((EntityManagerInternal)initialManagementContext.getEntityManager()).getEntityIds();
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    @Override
+    public ManagementTransitionMode getLastManagementTransitionMode(String itemId) {
+        if (isInitialManagementContextReal()) {
+            return ((EntityManagerInternal)initialManagementContext.getEntityManager()).getLastManagementTransitionMode(itemId);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public void setManagementTransitionMode(Entity item, ManagementTransitionMode mode) {
+        if (isInitialManagementContextReal()) {
+            ((EntityManagerInternal)initialManagementContext.getEntityManager()).setManagementTransitionMode(item, mode);
+        } else {
+            throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation");
+        }
+    }
+    
+    @Override
     public boolean isManaged(Entity entity) {
         return false;
     }
@@ -139,6 +167,16 @@ public class NonDeploymentEntityManager implements EntityManagerInternal {
     }
 
     @Override
+    public void unmanage(Entity e, ManagementTransitionMode info) {
+        throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation");
+    }
+    
+    @Override
+    public void manageRebindedRoot(Entity item) {
+        throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation");
+    }
+
+    @Override
     public void unmanage(Entity e) {
         throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation: cannot unmanage "+e);
     }
@@ -156,14 +194,4 @@ public class NonDeploymentEntityManager implements EntityManagerInternal {
         }
     }
     
-    @Override
-    public void setReadOnly(Entity e) {
-        throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation: cannot manage "+e);
-    }
-
-    @Override
-    public Boolean isReadOnly(Entity item) {
-        // NB: must return null here, otherwise callers may cache this value
-        return null;
-    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
index 1aa3c32..3628231 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentLocationManager.java
@@ -24,10 +24,10 @@ import java.util.Map;
 
 import brooklyn.location.Location;
 import brooklyn.location.LocationSpec;
-import brooklyn.management.LocationManager;
 import brooklyn.management.ManagementContext;
+import brooklyn.management.internal.ManagementTransitionInfo.ManagementTransitionMode;
 
-public class NonDeploymentLocationManager implements LocationManager {
+public class NonDeploymentLocationManager implements LocationManagerInternal {
 
     private final ManagementContext initialManagementContext;
     
@@ -72,11 +72,30 @@ public class NonDeploymentLocationManager implements LocationManager {
     }
 
     @Override
+    public Iterable<String> getLocationIds() {
+        if (isInitialManagementContextReal()) {
+            return ((LocationManagerInternal)initialManagementContext.getLocationManager()).getLocationIds();
+        } else {
+            return null;
+        }
+    }
+    
+    @Override
     public boolean isManaged(Location loc) {
         return false;
     }
 
     @Override
+    public void manageRebindedRoot(Location loc) {
+        if (isInitialManagementContextReal()) {
+            ((LocationManagerInternal)initialManagementContext.getLocationManager()).manageRebindedRoot(loc);
+        } else {
+            throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation: cannot manage "+loc);
+        }
+    }
+
+    @Override
+    @Deprecated
     public Location manage(Location loc) {
         if (isInitialManagementContextReal()) {
             return initialManagementContext.getLocationManager().manage(loc);
@@ -85,6 +104,34 @@ public class NonDeploymentLocationManager implements LocationManager {
         }
     }
 
+
+    @Override
+    public ManagementTransitionMode getLastManagementTransitionMode(String itemId) {
+        if (isInitialManagementContextReal()) {
+            return ((LocationManagerInternal)initialManagementContext.getLocationManager()).getLastManagementTransitionMode(itemId);
+        } else {
+            throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation");
+        }
+    }
+
+    @Override
+    public void setManagementTransitionMode(Location item, ManagementTransitionMode mode) {
+        if (isInitialManagementContextReal()) {
+            ((LocationManagerInternal)initialManagementContext.getLocationManager()).setManagementTransitionMode(item, mode);
+        } else {
+            throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation");
+        }
+    }
+
+    @Override
+    public void unmanage(Location item, ManagementTransitionMode info) {
+        if (isInitialManagementContextReal()) {
+            ((LocationManagerInternal)initialManagementContext.getLocationManager()).unmanage(item, info);
+        } else {
+            throw new IllegalStateException("Non-deployment context "+this+" is not valid for this operation");
+        }
+    }
+
     @Override
     public void unmanage(Location loc) {
         if (isInitialManagementContextReal()) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index dbe50d9..afe42df 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -473,7 +473,7 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
         }
         
         @Override
-        public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler, ManagementNodeState mode) throws IOException {
+        public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler, ManagementNodeState mode) {
             throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
         }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/4c866cb8/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 03eec67..befdae4 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -50,11 +50,9 @@ import brooklyn.test.entity.TestEntity;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
 import brooklyn.util.time.Duration;
-import brooklyn.util.time.Time;
 
 import com.google.common.collect.Iterables;
 
-@Test
 public class HotStandbyTest {
 
     private static final Logger log = LoggerFactory.getLogger(HotStandbyTest.class);
@@ -158,12 +156,23 @@ public class HotStandbyTest {
         return app;
     }
     
-    private Application waitForRebindSequenceNumber(HaMgmtNode master, HaMgmtNode hotStandby, Application app, int expectedSensorSequenceValue) {
-        Time.sleep(Duration.FIVE_SECONDS);
+    private Application expectRebindSequenceNumber(HaMgmtNode master, HaMgmtNode hotStandby, Application app, int expectedSensorSequenceValue, boolean immediate) {
         Application appRO = hotStandby.mgmt.lookup(app.getId(), Application.class);
-        // TODO drop the sleep and the new lookup, when the implementation can do in-place replace
+
+        if (immediate) {
+            master.mgmt.getRebindManager().forcePersistNow();
+            hotStandby.mgmt.getRebindManager().rebind(null, null, ManagementNodeState.HOT_STANDBY);
+            EntityTestUtils.assertAttributeEquals(appRO, TestEntity.SEQUENCE, expectedSensorSequenceValue);
+        } else {
+            EntityTestUtils.assertAttributeEqualsEventually(appRO, TestEntity.SEQUENCE, expectedSensorSequenceValue);
+        }
+        
+        log.info("got sequence number "+expectedSensorSequenceValue+" from "+appRO);
+        
+        // make sure the instance (proxy) is unchanged
+        Application appRO2 = hotStandby.mgmt.lookup(app.getId(), Application.class);
+        Assert.assertTrue(appRO2==appRO);
         
-        EntityTestUtils.assertAttributeEqualsEventually(appRO, TestEntity.SEQUENCE, expectedSensorSequenceValue);
         return appRO;
     }
     
@@ -189,6 +198,7 @@ public class HotStandbyTest {
         }
     }
 
+    @Test
     public void testHotStandbySeesChangedNameConfigAndSensorValue() throws Exception {
         HaMgmtNode n1 = createMaster();
         TestApplication app = createFirstAppAndPersist(n1);
@@ -205,7 +215,7 @@ public class HotStandbyTest {
         app.setConfig(TestEntity.CONF_NAME, "first-app-renamed");
         app.setAttribute(TestEntity.SEQUENCE, 4);
 
-        appRO = waitForRebindSequenceNumber(n1, n2, app, 4);
+        appRO = expectRebindSequenceNumber(n1, n2, app, 4, true);
         assertEquals(n2.mgmt.getEntityManager().getEntities().size(), 1);
         assertEquals(appRO.getDisplayName(), "First App Renamed");
         assertEquals(appRO.getConfig(TestEntity.CONF_NAME), "first-app-renamed");
@@ -216,20 +226,23 @@ public class HotStandbyTest {
         app.setConfig(TestEntity.CONF_NAME, "first-app-restored");
         app.setAttribute(TestEntity.SEQUENCE, 5);
         
-        appRO = waitForRebindSequenceNumber(n1, n2, app, 4);
+        appRO = expectRebindSequenceNumber(n1, n2, app, 5, true);
         assertEquals(n2.mgmt.getEntityManager().getEntities().size(), 1);
         assertEquals(appRO.getDisplayName(), "First App");
         assertEquals(appRO.getConfig(TestEntity.CONF_NAME), "first-app-restored");
     }
 
 
-    @Test(groups="Integration", invocationCount=50)
-    public void testHotStandbySeesAdditionAndRemovalManyTimes() throws Exception {
-        testHotStandbySeesStructuralChangesIncludingRemoval();
+    public void testHotStandbySeesStructuralChangesIncludingRemoval() throws Exception {
+        doTestHotStandbySeesStructuralChangesIncludingRemoval(true);
     }
     
-    @Test(groups="Integration") // due to time (could speed up by forcing persist and rebind)
-    public void testHotStandbySeesStructuralChangesIncludingRemoval() throws Exception {
+    @Test(groups="Integration") // due to time (it waits for background persistence)
+    public void testHotStandbyUnforcedSeesStructuralChangesIncludingRemoval() throws Exception {
+        doTestHotStandbySeesStructuralChangesIncludingRemoval(false);
+    }
+    
+    public void doTestHotStandbySeesStructuralChangesIncludingRemoval(boolean immediate) throws Exception {
         HaMgmtNode n1 = createMaster();
         TestApplication app = createFirstAppAndPersist(n1);
         HaMgmtNode n2 = createHotStandby();
@@ -242,10 +255,12 @@ public class HotStandbyTest {
         // test additions - new child, new app
         
         TestEntity child = app.addChild(EntitySpec.create(TestEntity.class).configure(TestEntity.CONF_NAME, "first-child"));
+        Entities.manage(child);
         TestApplication app2 = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        app2.setConfig(TestEntity.CONF_NAME, "second-app");
         
         app.setAttribute(TestEntity.SEQUENCE, 4);
-        appRO = waitForRebindSequenceNumber(n1, n2, app, 4);
+        appRO = expectRebindSequenceNumber(n1, n2, app, 4, immediate);
         
         assertEquals(appRO.getChildren().size(), 1);
         Entity childRO = Iterables.getOnlyElement(appRO.getChildren());
@@ -265,7 +280,7 @@ public class HotStandbyTest {
         Entities.unmanage(app2);
         
         app.setAttribute(TestEntity.SEQUENCE, 5);
-        appRO = waitForRebindSequenceNumber(n1, n2, app, 5);
+        appRO = expectRebindSequenceNumber(n1, n2, app, 5, immediate);
         
         EntityTestUtils.assertAttributeEqualsEventually(appRO, TestEntity.SEQUENCE, 5);
         assertEquals(n2.mgmt.getEntityManager().getEntities().size(), 1);
@@ -274,5 +289,10 @@ public class HotStandbyTest {
         Assert.assertNull(n2.mgmt.lookup(app2.getId(), Application.class));
         Assert.assertNull(n2.mgmt.lookup(child.getId(), Application.class));
     }
+
+    @Test(groups="Integration", invocationCount=50)
+    public void testHotStandbySeesStructuralChangesIncludingRemovalManyTimes() throws Exception {
+        doTestHotStandbySeesStructuralChangesIncludingRemoval(true);
+    }
     
 }


[02/15] git commit: set up a new ManagementContext.getServerExecutionContext() and use that for node-level tasks, with a tag that can be tracked; improve lifecycle and configuration for rebind exception handler and for the persister itself

Posted by he...@apache.org.
set up a new ManagementContext.getServerExecutionContext() and use that for node-level tasks, with a tag that can be tracked; improve lifecycle and configuration for rebind exception handler and for the persister itself


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

Branch: refs/heads/master
Commit: ea4b4ccb4c26f2c6e7547853e91ff619f2db6365
Parents: 5188c13
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Fri Sep 26 23:27:54 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:15 2014 +0100

----------------------------------------------------------------------
 .../entity/rebind/RebindExceptionHandler.java   | 19 ++++-
 .../brooklyn/entity/rebind/RebindManager.java   |  9 +--
 .../brooklyn/management/ExecutionContext.java   |  8 ++
 .../brooklyn/management/ManagementContext.java  |  6 ++
 .../mementos/BrooklynMementoPersister.java      |  3 +
 .../brooklyn/entity/basic/BrooklynTaskTags.java |  4 +
 .../java/brooklyn/entity/basic/Entities.java    |  2 +-
 .../rebind/PeriodicDeltaChangeListener.java     | 16 ++--
 .../rebind/RebindExceptionHandlerImpl.java      | 25 ++++++
 .../entity/rebind/RebindManagerImpl.java        | 58 +++++++++-----
 .../AbstractBrooklynMementoPersister.java       |  9 +--
 .../BrooklynMementoPersisterToMultiFile.java    |  8 ++
 .../BrooklynMementoPersisterToObjectStore.java  | 81 +++++++++++---------
 .../ha/HighAvailabilityManagerImpl.java         | 16 +++-
 .../internal/AbstractManagementContext.java     |  8 +-
 .../internal/LocalManagementContext.java        |  4 +-
 .../NonDeploymentManagementContext.java         | 11 +++
 .../util/task/AbstractExecutionContext.java     |  7 +-
 .../util/task/BasicExecutionContext.java        |  7 +-
 .../entity/rebind/RebindCatalogEntityTest.java  |  3 +-
 .../entity/rebind/RebindEntityTest.java         |  5 +-
 .../entity/rebind/RebindFailuresTest.java       | 31 +++++---
 .../entity/rebind/RebindTestFixture.java        |  4 +-
 .../brooklyn/entity/rebind/RebindTestUtils.java | 16 +---
 .../rebind/RecordingRebindExceptionHandler.java |  5 --
 .../transformer/CompoundTransformerTest.java    |  1 +
 .../HighAvailabilityManagerSplitBrainTest.java  |  6 +-
 .../ha/HighAvailabilityManagerTestFixture.java  |  2 +-
 .../brooklyn/management/ha/HotStandbyTest.java  |  2 +-
 .../AbstractWebAppFixtureIntegrationTest.java   |  3 +-
 .../brooklyn/launcher/BrooklynLauncher.java     |  4 +-
 .../BrooklynLauncherHighAvailabilityTest.java   |  7 +-
 32 files changed, 261 insertions(+), 129 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 6c80f8b..34c4f05 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindExceptionHandler.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindExceptionHandler.java
@@ -31,6 +31,14 @@ import com.google.common.annotations.Beta;
 
 /**
  * Handler called on all exceptions to do with rebind.
+ * A handler instance is linked to a single rebind pass;
+ * it should not be invoked after {@link #onDone()}.
+ * <p>
+ * {@link #onStart()} must be invoked before the run.
+ * {@link #onDone()} must be invoked after a successful run, and it may throw.
+ * <p>
+ * Implementations may propagate errors or may catch them until {@link #onDone()} is invoked,
+ * and that may throw or report elsewhere, as appropriate.
  * 
  * @author aled
  */
@@ -59,7 +67,6 @@ public interface RebindExceptionHandler {
      */
     Enricher onDanglingEnricherRef(String id);
 
-    //XXX
     /**
      * @return the feed to use in place of the missing one, or null (if hasn't thrown an exception)
      */
@@ -84,7 +91,13 @@ public interface RebindExceptionHandler {
 
     void onManageFailed(BrooklynObjectType type, BrooklynObject instance, Exception e);
 
-    void onDone();
-    
+    /** invoked for any high-level, unexpected, or otherwise uncaught failure;
+     * may be invoked on catching above errors */
     RuntimeException onFailed(Exception e);
+
+    /** invoked before the rebind pass */
+    void onStart();
+    
+    /** 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/ea4b4ccb/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
index 8bbdcaa..6f15599 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
@@ -57,15 +57,14 @@ public interface RebindManager {
     @VisibleForTesting
     public BrooklynMementoPersister getPersister();
 
-    /**
-     * @deprecated since 0.7; use {@link #rebind(ClassLoader)}
-     */
-    @Deprecated
+    /** @deprecated since 0.7; use {@link #rebind(ClassLoader, RebindExceptionHandler, ManagementNodeState)} */ @Deprecated
     public List<Application> rebind() throws IOException;
     
+    /** @deprecated since 0.7; use {@link #rebind(ClassLoader, RebindExceptionHandler, ManagementNodeState)} */ @Deprecated
     public List<Application> rebind(ClassLoader classLoader) throws IOException;
-
+    /** @deprecated since 0.7; use {@link #rebind(ClassLoader, RebindExceptionHandler, ManagementNodeState)} */ @Deprecated
     public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler) throws IOException;
+    public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler, ManagementNodeState mode) throws IOException;
 
     public BrooklynMementoRawData retrieveMementoRawData() throws IOException;
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/api/src/main/java/brooklyn/management/ExecutionContext.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ExecutionContext.java b/api/src/main/java/brooklyn/management/ExecutionContext.java
index 0758ecb..2e43a91 100644
--- a/api/src/main/java/brooklyn/management/ExecutionContext.java
+++ b/api/src/main/java/brooklyn/management/ExecutionContext.java
@@ -48,6 +48,12 @@ public interface ExecutionContext extends Executor {
      */
     <T> Task<T> submit(Map<?,?> properties, Callable<T> callable);
 
+    /** {@link ExecutionManager#submit(Runnable) */
+    Task<?> submit(Runnable runnable);
+ 
+    /** {@link ExecutionManager#submit(Callable) */
+    <T> Task<T> submit(Callable<T> callable);
+
     /** See {@link ExecutionManager#submit(Map, TaskAdaptable)}. */
     <T> Task<T> submit(TaskAdaptable<T> task);
     
@@ -56,4 +62,6 @@ public interface ExecutionContext extends Executor {
      */
     <T> Task<T> submit(Map<?,?> properties, TaskAdaptable<T> task);
 
+    boolean isShutdown();
+
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/api/src/main/java/brooklyn/management/ManagementContext.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ManagementContext.java b/api/src/main/java/brooklyn/management/ManagementContext.java
index 68722d9..29cb20e 100644
--- a/api/src/main/java/brooklyn/management/ManagementContext.java
+++ b/api/src/main/java/brooklyn/management/ManagementContext.java
@@ -98,6 +98,12 @@ public interface ManagementContext {
      * to submit tasks and to observe what tasks are occurring
      */
     ExecutionManager getExecutionManager();
+    
+    /** 
+     * Returns an {@link ExecutionContext} within the {@link ExecutionManager} for tasks
+     * associated to the Brooklyn node's operation (not any entities). 
+     **/
+    ExecutionContext getServerExecutionContext();
 
     /**
      * Returns the {@link EntityDriverManager} entities can use to create drivers. This

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/api/src/main/java/brooklyn/mementos/BrooklynMementoPersister.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/mementos/BrooklynMementoPersister.java b/api/src/main/java/brooklyn/mementos/BrooklynMementoPersister.java
index 878a211..0317059 100644
--- a/api/src/main/java/brooklyn/mementos/BrooklynMementoPersister.java
+++ b/api/src/main/java/brooklyn/mementos/BrooklynMementoPersister.java
@@ -64,6 +64,9 @@ public interface BrooklynMementoPersister {
 
     void delta(Delta delta, PersistenceExceptionHandler exceptionHandler);
 
+    void enableWriteAccess();
+    void disableWriteAccess(boolean graceful);
+    /** permanently shuts down all access to the remote store */
     void stop(boolean graceful);
 
     @VisibleForTesting

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/entity/basic/BrooklynTaskTags.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/basic/BrooklynTaskTags.java b/core/src/main/java/brooklyn/entity/basic/BrooklynTaskTags.java
index e7a8c70..db16c4b 100644
--- a/core/src/main/java/brooklyn/entity/basic/BrooklynTaskTags.java
+++ b/core/src/main/java/brooklyn/entity/basic/BrooklynTaskTags.java
@@ -61,7 +61,11 @@ public class BrooklynTaskTags extends TaskTags {
 
     private static final Logger log = LoggerFactory.getLogger(BrooklynTaskTags.WrappedEntity.class);
 
+    /** Tag for tasks which are running on behalf of the management server, rather than any entity */
+    public static final String BROOKLYN_SERVER_TASK_TAG = "BROOKLYN-SERVER";
+    /** Tag for a task which represents an effector */
     public static final String EFFECTOR_TAG = "EFFECTOR";
+    /** Tag for a task which *is* interesting, in contrast to {@link #TRANSIENT_TASK_TAG} */
     public static final String NON_TRANSIENT_TASK_TAG = "NON-TRANSIENT";
     /** indicates a task is transient, roughly that is to say it is uninteresting -- 
      * specifically this means it can be GC'd as soon as it is completed, 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 516fbfa..9a75ab2 100644
--- a/core/src/main/java/brooklyn/entity/basic/Entities.java
+++ b/core/src/main/java/brooklyn/entity/basic/Entities.java
@@ -1007,7 +1007,7 @@ public class Entities {
     @Beta
     public static ProcessTaskWrapper<Integer> shell(ManagementContext mgmt, String command) {
         ProcessTaskWrapper<Integer> t = SystemTasks.exec(command).newTask();
-        mgmt.getExecutionManager().submit(t).getUnchecked();
+        mgmt.getServerExecutionContext().submit(t).getUnchecked();
         System.out.println(t.getStdout());
         System.err.println(t.getStderr());
         return t;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
index 0d708f4..e9cef0c 100644
--- a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
+++ b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
@@ -37,6 +37,7 @@ import brooklyn.entity.basic.EntityInternal;
 import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.location.Location;
 import brooklyn.location.basic.LocationInternal;
+import brooklyn.management.ExecutionContext;
 import brooklyn.management.ExecutionManager;
 import brooklyn.management.Task;
 import brooklyn.mementos.BrooklynMementoPersister;
@@ -44,6 +45,7 @@ import brooklyn.policy.Enricher;
 import brooklyn.policy.Policy;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.exceptions.RuntimeInterruptedException;
+import brooklyn.util.task.BasicExecutionContext;
 import brooklyn.util.task.BasicTask;
 import brooklyn.util.task.ScheduledTask;
 import brooklyn.util.time.Duration;
@@ -93,7 +95,7 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
         }
     }
     
-    private final ExecutionManager executionManager;
+    private final ExecutionContext executionContext;
     
     private final BrooklynMementoPersister persister;
 
@@ -117,14 +119,14 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
     
     private final Semaphore persistingMutex = new Semaphore(1);
     
-    /** @deprecated since 0.7.0 pass in a {@link Duration} */
+    /** @deprecated since 0.7.0 pass in an {@link ExecutionContext} and a {@link Duration} */
     @Deprecated
     public PeriodicDeltaChangeListener(ExecutionManager executionManager, BrooklynMementoPersister persister, PersistenceExceptionHandler exceptionHandler, long periodMillis) {
-        this(executionManager, persister, exceptionHandler, Duration.of(periodMillis, TimeUnit.MILLISECONDS));
+        this(new BasicExecutionContext(executionManager), persister, exceptionHandler, Duration.of(periodMillis, TimeUnit.MILLISECONDS));
     }
     
-    public PeriodicDeltaChangeListener(ExecutionManager executionManager, BrooklynMementoPersister persister, PersistenceExceptionHandler exceptionHandler, Duration period) {
-        this.executionManager = executionManager;
+    public PeriodicDeltaChangeListener(ExecutionContext executionContext, BrooklynMementoPersister persister, PersistenceExceptionHandler exceptionHandler, Duration period) {
+        this.executionContext = executionContext;
         this.persister = persister;
         this.exceptionHandler = exceptionHandler;
         this.period = period;
@@ -165,7 +167,7 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
                     }});
             }
         };
-        scheduledTask = (ScheduledTask) executionManager.submit(new ScheduledTask(taskFactory).period(period));
+        scheduledTask = (ScheduledTask) executionContext.submit(new ScheduledTask(taskFactory).period(period));
     }
     
     void stop() {
@@ -223,7 +225,7 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
      * Whether we have been stopped, in which case will not persist or store anything.
      */
     private boolean isStopped() {
-        return stopped || executionManager.isShutdown();
+        return stopped || executionContext.isShutdown();
     }
     
     private void addReferencedObjects(DeltaCollector deltaCollector) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 25523c6..de37f42 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindExceptionHandlerImpl.java
@@ -42,6 +42,7 @@ import brooklyn.util.text.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 
+/** Stateful handler, meant for a single rebind pass */
 public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
 
     private static final Logger LOG = LoggerFactory.getLogger(RebindExceptionHandlerImpl.class);
@@ -62,6 +63,9 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
     protected final Set<Exception> loadPolicyFailures = Sets.newConcurrentHashSet();
     protected final List<Exception> exceptions = Collections.synchronizedList(Lists.<Exception>newArrayList());
     
+    protected boolean started = false;
+    protected boolean done = false;
+    
     public static Builder builder() {
         return new Builder();
     }
@@ -99,6 +103,16 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
         this.addPolicyFailureMode = checkNotNull(builder.addPolicyFailureMode, "addPolicyFailureMode");
         this.loadPolicyFailureMode = checkNotNull(builder.deserializePolicyFailureMode, "deserializePolicyFailureMode");
     }
+
+    public void onStart() {
+        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");
+        }
+        started = true;
+    }
     
     @Override
     public void onLoadMementoFailed(BrooklynObjectType type, String msg, Exception e) {
@@ -329,6 +343,9 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
 
     @Override
     public RuntimeException onFailed(Exception e) {
+        if (done) 
+            throw Exceptions.propagate(e);
+        
         onDoneImpl(e);
         throw new IllegalStateException("Rebind failed", e); // should have thrown exception above
     }
@@ -338,9 +355,17 @@ public class RebindExceptionHandlerImpl implements RebindExceptionHandler {
         
         List<Exception> allExceptions = Lists.newArrayList();
         
+        if (done) {
+            allExceptions.add(new IllegalStateException(this+" has already been informed of rebind done"));
+        }
+        done = true;
+        if (!started) {
+            allExceptions.add(new IllegalStateException(this+" was not informed of start of rebind run"));
+        }
         if (e != null) {
             allExceptions.add(e);
         }
+        
         if (addPolicyFailureMode != RebindManager.RebindFailureMode.CONTINUE) {
             allExceptions.addAll(addPolicyFailures);
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 70a2ba7..47f3770 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -57,6 +57,7 @@ import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.location.Location;
 import brooklyn.location.basic.AbstractLocation;
 import brooklyn.location.basic.LocationInternal;
+import brooklyn.management.ExecutionContext;
 import brooklyn.management.Task;
 import brooklyn.management.ha.HighAvailabilityManagerImpl;
 import brooklyn.management.ha.ManagementNodeState;
@@ -127,10 +128,11 @@ public class RebindManagerImpl implements RebindManager {
     private volatile boolean persistenceRunning = false;
     private volatile PeriodicDeltaChangeListener persistenceRealChangeListener;
     private volatile ChangeListener persistencePublicChangeListener;
-    private volatile BrooklynMementoPersister persister;
     
     private volatile boolean readOnlyRunning = false;
     private volatile ScheduledTask readOnlyTask = null;
+    
+    private volatile BrooklynMementoPersister persistenceStoreAccess;
 
     private final boolean persistPoliciesEnabled;
     private final boolean persistEnrichersEnabled;
@@ -187,7 +189,7 @@ public class RebindManagerImpl implements RebindManager {
      * Must be called before setPerister()
      */
     public void setPeriodicPersistPeriod(Duration period) {
-        if (persister!=null) throw new IllegalStateException("Cannot set period after persister is generated.");
+        if (persistenceStoreAccess!=null) throw new IllegalStateException("Cannot set period after persister is generated.");
         this.periodicPersistPeriod = period;
     }
 
@@ -207,12 +209,12 @@ public class RebindManagerImpl implements RebindManager {
 
     @Override
     public void setPersister(BrooklynMementoPersister val, PersistenceExceptionHandler exceptionHandler) {
-        if (persister != null && persister != val) {
-            throw new IllegalStateException("Dynamically changing persister is not supported: old="+persister+"; new="+val);
+        if (persistenceStoreAccess != null && persistenceStoreAccess != val) {
+            throw new IllegalStateException("Dynamically changing persister is not supported: old="+persistenceStoreAccess+"; new="+val);
         }
-        this.persister = checkNotNull(val, "persister");
+        this.persistenceStoreAccess = checkNotNull(val, "persister");
         
-        this.persistenceRealChangeListener = new PeriodicDeltaChangeListener(managementContext.getExecutionManager(), persister, exceptionHandler, periodicPersistPeriod);
+        this.persistenceRealChangeListener = new PeriodicDeltaChangeListener(managementContext.getServerExecutionContext(), persistenceStoreAccess, exceptionHandler, periodicPersistPeriod);
         this.persistencePublicChangeListener = new SafeChangeListener(persistenceRealChangeListener);
         
         if (persistenceRunning) {
@@ -223,7 +225,7 @@ public class RebindManagerImpl implements RebindManager {
     @Override
     @VisibleForTesting
     public BrooklynMementoPersister getPersister() {
-        return persister;
+        return persistenceStoreAccess;
     }
     
     @Override
@@ -233,6 +235,7 @@ public class RebindManagerImpl implements RebindManager {
         }
         LOG.debug("Starting persistence, mgmt "+managementContext.getManagementNodeId());
         persistenceRunning = true;
+        persistenceStoreAccess.enableWriteAccess();
         if (persistenceRealChangeListener != null) persistenceRealChangeListener.start();
     }
     
@@ -241,7 +244,7 @@ public class RebindManagerImpl implements RebindManager {
         LOG.debug("Stopping persistence, mgmt "+managementContext.getManagementNodeId());
         persistenceRunning = false;
         if (persistenceRealChangeListener != null) persistenceRealChangeListener.stop();
-        if (persister != null) persister.stop(true);
+        if (persistenceStoreAccess != null) persistenceStoreAccess.disableWriteAccess(true);
         LOG.debug("Stopped persistence, mgmt "+managementContext.getManagementNodeId());
     }
     
@@ -294,8 +297,8 @@ public class RebindManagerImpl implements RebindManager {
                     }});
             }
         };
-        readOnlyTask = (ScheduledTask) managementContext.getExecutionManager().submit(
-            new ScheduledTask(taskFactory).period(periodicPersistPeriod));
+        readOnlyTask = (ScheduledTask) managementContext.getServerExecutionContext().submit(
+            new ScheduledTask(MutableMap.of("displayName", "Periodic read-only rebind"), taskFactory).period(periodicPersistPeriod));
     }
     
     @Override
@@ -326,6 +329,7 @@ public class RebindManagerImpl implements RebindManager {
     public void stop() {
         stopReadOnly();
         stopPersistence();
+        if (persistenceStoreAccess != null) persistenceStoreAccess.stop(true);
     }
     
     protected ManagementNodeState getRebindMode() {
@@ -344,9 +348,9 @@ public class RebindManagerImpl implements RebindManager {
     @Override
     @VisibleForTesting
     public void waitForPendingComplete(Duration timeout) throws InterruptedException, TimeoutException {
-        if (persister == null || !persistenceRunning) return;
+        if (persistenceStoreAccess == null || !persistenceRunning) return;
         persistenceRealChangeListener.waitForPendingComplete(timeout);
-        persister.waitForWritesCompleted(timeout);
+        persistenceStoreAccess.waitForWritesCompleted(timeout);
     }
     @Override
     @VisibleForTesting
@@ -366,7 +370,7 @@ public class RebindManagerImpl implements RebindManager {
     
     @Override
     public List<Application> rebind(final ClassLoader classLoader) throws IOException {
-        return rebind(classLoader, null);
+        return rebind(classLoader, null, null);
     }
 
     @Override
@@ -374,7 +378,8 @@ public class RebindManagerImpl implements RebindManager {
         return rebind(classLoader, exceptionHandler, null);
     }
     
-    protected List<Application> rebind(ClassLoader classLoaderO, RebindExceptionHandler exceptionHandlerO, ManagementNodeState modeO) throws IOException {
+    @Override
+    public List<Application> rebind(ClassLoader classLoaderO, RebindExceptionHandler exceptionHandlerO, ManagementNodeState modeO) throws IOException {
         final ClassLoader classLoader = classLoaderO!=null ? classLoaderO :
             managementContext.getCatalog().getRootClassLoader();
         final RebindExceptionHandler exceptionHandler = exceptionHandlerO!=null ? exceptionHandlerO :
@@ -386,9 +391,12 @@ public class RebindManagerImpl implements RebindManager {
                 .build();
         final ManagementNodeState mode = modeO!=null ? modeO : getRebindMode();
         
-        BasicExecutionContext ec = BasicExecutionContext.getCurrentExecutionContext();
+        if (mode!=ManagementNodeState.HOT_STANDBY && mode!=ManagementNodeState.MASTER)
+            throw new IllegalStateException("Must be either master or read only to rebind (mode "+mode+")");
+
+        ExecutionContext ec = BasicExecutionContext.getCurrentExecutionContext();
         if (ec == null) {
-            ec = new BasicExecutionContext(managementContext.getExecutionManager());
+            ec = managementContext.getServerExecutionContext();
             Task<List<Application>> task = ec.submit(new Callable<List<Application>>() {
                 @Override public List<Application> call() throws Exception {
                     return rebindImpl(classLoader, exceptionHandler, mode);
@@ -422,11 +430,11 @@ public class RebindManagerImpl implements RebindManager {
      */
     protected BrooklynMementoRawData loadMementoRawData(final RebindExceptionHandler exceptionHandler) throws IOException {
         try {
-            if (!(persister instanceof BrooklynMementoPersisterToObjectStore)) {
-                throw new IllegalStateException("Cannot load raw memento with persister "+persister);
+            if (!(persistenceStoreAccess instanceof BrooklynMementoPersisterToObjectStore)) {
+                throw new IllegalStateException("Cannot load raw memento with persister "+persistenceStoreAccess);
             }
             
-            return ((BrooklynMementoPersisterToObjectStore)persister).loadMementoRawData(exceptionHandler);
+            return ((BrooklynMementoPersisterToObjectStore)persistenceStoreAccess).loadMementoRawData(exceptionHandler);
             
         } catch (RuntimeException e) {
             throw exceptionHandler.onFailed(e);
@@ -440,6 +448,8 @@ public class RebindManagerImpl implements RebindManager {
 
         RebindTracker.setRebinding();
         try {
+            exceptionHandler.onStart();
+            
             Reflections reflections = new Reflections(classLoader);
             RebindContextImpl rebindContext = new RebindContextImpl(exceptionHandler, classLoader);
             LookupContext realLookupContext = new RebindContextLookupContext(managementContext, rebindContext, exceptionHandler);
@@ -463,7 +473,7 @@ public class RebindManagerImpl implements RebindManager {
             // PHASE ONE
             //
             
-            BrooklynMementoManifest mementoManifest = persister.loadMementoManifest(exceptionHandler);
+            BrooklynMementoManifest mementoManifest = persistenceStoreAccess.loadMementoManifest(exceptionHandler);
             
             boolean isEmpty = mementoManifest.isEmpty();
             if (!isEmpty) {
@@ -512,7 +522,7 @@ public class RebindManagerImpl implements RebindManager {
             // PHASE THREE
             //
             
-            BrooklynMemento memento = persister.loadMemento(realLookupContext, exceptionHandler);
+            BrooklynMemento memento = persistenceStoreAccess.loadMemento(realLookupContext, exceptionHandler);
 
             
             //
@@ -786,6 +796,7 @@ public class RebindManagerImpl implements RebindManager {
                     managementContext.getCatalog().reset(rebindContext.getCatalogItems());
                 } else if (shouldLoadDefaultCatalog) {
                     // Load catalogue as normal
+                    // TODO in read-only mode, should do this less frequently than entities etc
                     LOG.debug("RebindManager loading default catalog");
                     ((BasicBrooklynCatalog) managementContext.getCatalog()).resetCatalogToContentsAtConfiguredUrl();
                 } else {
@@ -1080,4 +1091,9 @@ public class RebindManagerImpl implements RebindManager {
             }
         }
     }
+    
+    @Override
+    public String toString() {
+        return super.toString()+"[mgmt="+managementContext.getManagementNodeId()+"]";
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/entity/rebind/persister/AbstractBrooklynMementoPersister.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/AbstractBrooklynMementoPersister.java b/core/src/main/java/brooklyn/entity/rebind/persister/AbstractBrooklynMementoPersister.java
index 32bfcc3..c230e9e 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/AbstractBrooklynMementoPersister.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/AbstractBrooklynMementoPersister.java
@@ -67,11 +67,10 @@ public abstract class AbstractBrooklynMementoPersister implements BrooklynMement
         }
         return builder.build();
     }
-    
-    @Override
-    public void stop(boolean graceful) {
-        // no-op
-    }
+
+    @Override public void enableWriteAccess() {}
+    @Override public void disableWriteAccess(boolean graceful) {}
+    @Override public void stop(boolean graceful) {}
     
     @Override
     public void checkpoint(BrooklynMemento newMemento, PersistenceExceptionHandler exceptionHandler) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFile.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFile.java b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFile.java
index ab0f831..aeedf61 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFile.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToMultiFile.java
@@ -128,6 +128,14 @@ public class BrooklynMementoPersisterToMultiFile implements BrooklynMementoPersi
         LOG.info("Memento-persister will use directory {}", dir);
     }
     
+    @Override public void enableWriteAccess() {
+        // no-op -- means subsequent writes will not be enabled
+    }
+    
+    @Override public void disableWriteAccess(boolean graceful) {
+        stop(graceful);
+    }
+
     @Override
     public void stop(boolean graceful) {
         running = false;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
index 5b224b1..e02de72 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
@@ -101,8 +101,8 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
     private final ListeningExecutorService executor;
 
-    private volatile boolean running = true;
-    private volatile boolean shuttingDown = false;
+    private volatile boolean writesAllowed = false;
+    private volatile boolean writesShuttingDown = false;
 
     /**
      * Lock used on writes (checkpoint + delta) so that {@link #waitForWritesCompleted(Duration)} can block
@@ -135,30 +135,45 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
             }}));
     }
 
+    @Override public void enableWriteAccess() {
+        writesAllowed = true;
+    }
+    
     @Override
-    public void stop(boolean graceful) {
-        shuttingDown = true;
+    public void disableWriteAccess(boolean graceful) {
+        writesShuttingDown = true;
         try {
-            running = false;
-            if (executor != null) {
-                if (graceful) {
-                    // a very long timeout to ensure we don't lose state. 
-                    // If persisting thousands of entities over slow network to Object Store, could take minutes.
-                    executor.shutdown();
-                    try {
-                        executor.awaitTermination(1, TimeUnit.HOURS);
-                    } catch (InterruptedException e) {
-                        throw Exceptions.propagate(e);
-                    }
-                } else {
-                    executor.shutdownNow();
-                }
-            }
+            writesAllowed = false;
+            // a very long timeout to ensure we don't lose state. 
+            // If persisting thousands of entities over slow network to Object Store, could take minutes.
+            waitForWritesCompleted(Duration.ONE_HOUR);
+            
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
         } finally {
-            shuttingDown = false;
+            writesShuttingDown = false;
         }
     }
     
+    @Override 
+    public void stop(boolean graceful) {
+        disableWriteAccess(graceful);
+        
+        if (executor != null) {
+            if (graceful) {
+                executor.shutdown();
+                try {
+                    // should be quick because we've just turned off writes, waiting for their completion
+                    executor.awaitTermination(1, TimeUnit.MINUTES);
+                } catch (InterruptedException e) {
+                    throw Exceptions.propagate(e);
+                }
+            } else {
+                executor.shutdownNow();
+            }
+        }
+    }
+
     public PersistenceObjectStore getObjectStore() {
         return objectStore;
     }
@@ -331,10 +346,6 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
         public void visit(String contents, BrooklynObjectType type, String subPath) throws Exception;
     }
     protected void visitMemento(final Visitor visitor, final RebindExceptionHandler exceptionHandler) throws IOException {
-        if (!running && !shuttingDown) {
-            throw new IllegalStateException("Persister not running; cannot load memento manifest from " + objectStore.getSummaryName());
-        }
-
         List<String> entitySubPathList;
         List<String> locationSubPathList;
         List<String> policySubPathList;
@@ -429,12 +440,15 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
         }
     }
 
+    protected void checkWritesAllowed() {
+        if (!writesAllowed && !writesShuttingDown) {
+            throw new IllegalStateException("Writes not allowed in "+this);
+        }
+    }
+    
     @Beta
     public void checkpoint(BrooklynMementoRawData newMemento, PersistenceExceptionHandler exceptionHandler) {
-        if (!running && !shuttingDown) {
-            if (LOG.isDebugEnabled()) LOG.debug("Ignoring checkpointing entire memento, because not running");
-            return;
-        }
+        checkWritesAllowed();
         
         try {
             lock.writeLock().lockInterruptibly();
@@ -486,10 +500,7 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
     @Override
     public void checkpoint(BrooklynMemento newMemento, PersistenceExceptionHandler exceptionHandler) {
-        if (!running && !shuttingDown) {
-            if (LOG.isDebugEnabled()) LOG.debug("Ignoring checkpointing entire memento, because not running");
-            return;
-        }
+        checkWritesAllowed();
 
         Delta delta = PersisterDeltaImpl.builder()
                 .entities(newMemento.getEntityMementos().values())
@@ -506,10 +517,8 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
 
     @Override
     public void delta(Delta delta, PersistenceExceptionHandler exceptionHandler) {
-        if (!running && !shuttingDown) {
-            if (LOG.isDebugEnabled()) LOG.debug("Ignoring checkpointed delta of memento, because not running");
-            return;
-        }
+        checkWritesAllowed();
+
         Stopwatch stopwatch = deltaImpl(delta, exceptionHandler);
         
         if (LOG.isDebugEnabled()) LOG.debug("Checkpointed delta of memento in {}; updated {} entities, {} locations, " +

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 d63aa99..52fffdc 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -74,7 +74,7 @@ import com.google.common.collect.Iterables;
  * Promotion to master involves:
  * <ol>
  *   <li>notifying the other management-nodes that it is now master
- *   <li>calling {@link RebindManager#rebind()} to read all persisted entity state, and thus reconstitute the entities.
+ *   <li>calling {@link RebindManager#rebind(ClassLoader, brooklyn.entity.rebind.RebindExceptionHandler, ManagementNodeState)} to read all persisted entity state, and thus reconstitute the entities.
  * </ol>
  * <p>
  * Future improvements in this area will include brooklyn-managing-brooklyn to decide + promote
@@ -212,6 +212,10 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         // so another node could have already become master or terminated in that window.
         ManagementNodeSyncRecord existingMaster = hasHealthyMaster();
         
+        // catch error in some tests where mgmt context has a different mgmt context
+        if (managementContext.getHighAvailabilityManager()!=this)
+            throw new IllegalStateException("Cannot start an HA manager on a management context with a different HA manager!");
+        
         switch (startMode) {
         case AUTO:
             // don't care; let's start and see if we promote ourselves
@@ -531,10 +535,16 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
                 LOG.warn("Problem in promption-listener (continuing)", e);
             }
         }
+        boolean wasHotStandby = nodeState==ManagementNodeState.HOT_STANDBY;
         nodeState = ManagementNodeState.MASTER;
         publishPromotionToMaster();
         try {
-            managementContext.getRebindManager().rebind(managementContext.getCatalog().getRootClassLoader());
+            if (wasHotStandby) {
+                // could just promote the standby items; but for now we stop the old read-only and re-load them, to make sure nothing has been missed
+                // TODO ideally there'd be an incremental rebind as well as an incremental persist
+                managementContext.getRebindManager().stopReadOnly();
+            }
+            managementContext.getRebindManager().rebind(managementContext.getCatalog().getRootClassLoader(), null, nodeState);
         } catch (Exception e) {
             LOG.error("Management node enountered problem during rebind when promoting self to master; demoting to FAILED and rethrowing: "+e);
             demoteToFailed();
@@ -568,7 +578,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     }
     
     protected void onDemotionStopTasks() {
-        managementContext.getRebindManager().stop();
+        managementContext.getRebindManager().stopPersistence();
         for (Application app: managementContext.getApplications())
             Entities.unmanage(app);
         // let's try forcibly interrupting tasks on managed entities

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java b/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
index 44a77ae..b91e99b 100644
--- a/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
@@ -212,7 +212,13 @@ public abstract class AbstractManagementContext implements ManagementContextInte
         // BEC is a thin wrapper around EM so fine to create a new one here
         return new BasicExecutionContext(MutableMap.of("tag", BrooklynTaskTags.tagForContextEntity(e)), getExecutionManager());
     }
-    
+
+    @Override
+    public ExecutionContext getServerExecutionContext() {
+        // BEC is a thin wrapper around EM so fine to create a new one here
+        return new BasicExecutionContext(MutableMap.of("tag", BrooklynTaskTags.BROOKLYN_SERVER_TASK_TAG), getExecutionManager());
+    }
+
     @Override
     public SubscriptionContext getSubscriptionContext(Entity e) {
         // BSC is a thin wrapper around SM so fine to create a new one here

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java b/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
index 2c3d43a..bf4a559 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalManagementContext.java
@@ -39,6 +39,7 @@ import brooklyn.config.BrooklynProperties.Factory.Builder;
 import brooklyn.entity.Application;
 import brooklyn.entity.Effector;
 import brooklyn.entity.Entity;
+import brooklyn.entity.basic.BrooklynTaskTags;
 import brooklyn.entity.drivers.downloads.BasicDownloadsManager;
 import brooklyn.entity.effector.Effectors;
 import brooklyn.entity.proxying.InternalEntityFactory;
@@ -54,6 +55,7 @@ import brooklyn.management.SubscriptionManager;
 import brooklyn.management.Task;
 import brooklyn.management.TaskAdaptable;
 import brooklyn.management.ha.OsgiManager;
+import brooklyn.util.collections.MutableMap;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.guava.Maybe;
 import brooklyn.util.task.BasicExecutionContext;
@@ -309,7 +311,7 @@ public class LocalManagementContext extends AbstractManagementContext {
         }
         return execution;
     }
-
+    
     @Override
     public void terminate() {
         INSTANCES.remove(this);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index a404bb8..21f1968 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -20,6 +20,7 @@ package brooklyn.management.internal;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import java.io.IOException;
 import java.net.URI;
 import java.net.URL;
 import java.util.Collection;
@@ -231,6 +232,11 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
         return initialManagementContext.getExecutionContext(entity);
     }
 
+    @Override
+    public ExecutionContext getServerExecutionContext() {
+        return initialManagementContext.getServerExecutionContext();
+    }
+
     // TODO the methods below should delegate to the application?
     @Override
     public EntityDriverManager getEntityDriverManager() {
@@ -445,6 +451,11 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
         public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler) {
             throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
         }
+        
+        @Override
+        public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler, ManagementNodeState mode) throws IOException {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
 
         @Override
         public void startPersistence() {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/util/task/AbstractExecutionContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/task/AbstractExecutionContext.java b/core/src/main/java/brooklyn/util/task/AbstractExecutionContext.java
index cd94915..3002922 100644
--- a/core/src/main/java/brooklyn/util/task/AbstractExecutionContext.java
+++ b/core/src/main/java/brooklyn/util/task/AbstractExecutionContext.java
@@ -37,21 +37,27 @@ public abstract class AbstractExecutionContext implements ExecutionContext {
      *   
      * @see ExecutionManager#submit(Map, Task) 
      */
+    @Override
     public Task<?> submit(Map<?,?> properties, Runnable runnable) { return submitInternal(properties, runnable); }
     
     /** @see #submit(Map, Runnable) */
+    @Override
     public Task<?> submit(Runnable runnable) { return submitInternal(Maps.newLinkedHashMap(), runnable); }
  
     /** @see #submit(Map, Runnable) */
+    @Override
     public <T> Task<T> submit(Callable<T> callable) { return submitInternal(Maps.newLinkedHashMap(), callable); }
     
     /** @see #submit(Map, Runnable) */
+    @Override
     public <T> Task<T> submit(Map<?,?> properties, Callable<T> callable) { return submitInternal(properties, callable); }
  
     /** @see #submit(Map, Runnable) */
+    @Override
     public <T> Task<T> submit(TaskAdaptable<T> task) { return submitInternal(Maps.newLinkedHashMap(), task.asTask()); }
 
     /** @see #submit(Map, Runnable) */
+    @Override
     public <T> Task<T> submit(Map<?,?> properties, TaskAdaptable<T> task) { return submitInternal(properties, task.asTask()); }
 
     /**
@@ -66,5 +72,4 @@ public abstract class AbstractExecutionContext implements ExecutionContext {
     /** does the work internally of submitting the task; note that the return value may be a wrapper task even if a task is passed in,
      * if the execution context where the target should run is different (e.g. submitting an effector task cross-context) */
     protected abstract <T> Task<T> submitInternal(Map<?,?> properties, Object task);
-    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/main/java/brooklyn/util/task/BasicExecutionContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/util/task/BasicExecutionContext.java b/core/src/main/java/brooklyn/util/task/BasicExecutionContext.java
index a7bcbbd..ee1818e 100644
--- a/core/src/main/java/brooklyn/util/task/BasicExecutionContext.java
+++ b/core/src/main/java/brooklyn/util/task/BasicExecutionContext.java
@@ -190,7 +190,12 @@ public class BasicExecutionContext extends AbstractExecutionContext {
     private void registerPerThreadExecutionContext() { perThreadExecutionContext.set(this); }
 
     private void clearPerThreadExecutionContext() { perThreadExecutionContext.remove(); }
-    
+
+    @Override
+    public boolean isShutdown() {
+        return getExecutionManager().isShutdown();
+    }
+
     @Override
     public String toString() {
         return super.toString()+"("+tags+")";

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
index e482577..eac2e35 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindCatalogEntityTest.java
@@ -39,6 +39,7 @@ import brooklyn.entity.basic.EntityInternal;
 import brooklyn.entity.basic.StartableApplication;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.event.basic.Sensors;
+import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.util.javalang.UrlClassLoader;
 
@@ -128,7 +129,7 @@ public class RebindCatalogEntityTest extends RebindTestFixture<StartableApplicat
         newManagementContext.getCatalog().addItem(appClazz);
         
         ClassLoader classLoader = newManagementContext.getCatalog().getRootClassLoader();
-        List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader);
+        List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader, null, ManagementNodeState.MASTER);
         newManagementContext.getRebindManager().startPersistence();
         return (StartableApplication) newApps.get(0);
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
index fed6dd9..3450c40 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindEntityTest.java
@@ -61,6 +61,7 @@ import brooklyn.event.basic.DependentConfiguration;
 import brooklyn.event.basic.Sensors;
 import brooklyn.location.Location;
 import brooklyn.location.basic.LocationConfigTest.MyLocation;
+import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.mementos.EntityMemento;
 import brooklyn.test.Asserts;
@@ -338,7 +339,7 @@ public class RebindEntityTest extends RebindTestFixtureWithApp {
         Thread thread = new Thread() {
             public void run() {
                 try {
-                    newManagementContext.getRebindManager().rebind(classLoader);
+                    newManagementContext.getRebindManager().rebind(classLoader, null, ManagementNodeState.MASTER);
                 } catch (Exception e) {
                     throw Throwables.propagate(e);
                 }
@@ -578,7 +579,7 @@ public class RebindEntityTest extends RebindTestFixtureWithApp {
 
         RebindTestUtils.waitForPersisted(origManagementContext);
         newManagementContext = RebindTestUtils.newPersistingManagementContextUnstarted(mementoDir, classLoader);
-        List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader);
+        List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader, null, ManagementNodeState.MASTER);
         newManagementContext.getRebindManager().startPersistence();
         
         assertEquals(newApps.size(), 0, "apps="+newApps);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java b/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
index 9714dc9..e070bd0 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindFailuresTest.java
@@ -27,6 +27,7 @@ import java.io.File;
 import java.util.Map;
 import java.util.Set;
 
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import brooklyn.config.ConfigKey;
@@ -43,12 +44,13 @@ import brooklyn.entity.rebind.RebindManager.RebindFailureMode;
 import brooklyn.entity.trait.Identifiable;
 import brooklyn.event.AttributeSensor;
 import brooklyn.management.EntityManager;
-import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.policy.Enricher;
 import brooklyn.policy.EnricherSpec;
 import brooklyn.policy.Policy;
 import brooklyn.policy.PolicySpec;
 import brooklyn.policy.basic.AbstractPolicy;
+import brooklyn.test.entity.LocalManagementContextForTests;
+import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.flags.SetFromFlag;
 import brooklyn.util.os.Os;
 
@@ -91,14 +93,15 @@ public class RebindFailuresTest extends RebindTestFixtureWithApp {
                 .impl(MyEntityFailingImpl.class)
                 .configure(MyEntityFailingImpl.FAIL_ON_REBIND, true));
         
-        newManagementContext = new LocalManagementContext();
+        newManagementContext = LocalManagementContextForTests.newInstance();
         EntityManager newEntityManager = newManagementContext.getEntityManager();
         RecordingRebindExceptionHandler exceptionHandler = new RecordingRebindExceptionHandler(danglingRefFailureMode, rebindFailureMode);
         try {
             newApp = rebind(newManagementContext, exceptionHandler);
             fail();
         } catch (Exception e) {
-            if (!e.toString().contains("Problems rebinding")) throw e; // expected
+            assertFailureRebindingError(e);
+            Assert.assertTrue(e.toString().toLowerCase().contains("rebinding entity"), "Wrong error: "+e);
         }
 
         // exception handler should have been told about failure
@@ -117,14 +120,15 @@ public class RebindFailuresTest extends RebindTestFixtureWithApp {
                 .impl(MyEntityFailingImpl.class)
                 .configure(MyEntityFailingImpl.FAIL_ON_REBIND, true));
         
-        newManagementContext = new LocalManagementContext();
+        newManagementContext = LocalManagementContextForTests.newInstance();
         EntityManager newEntityManager = newManagementContext.getEntityManager();
         RecordingRebindExceptionHandler exceptionHandler = new RecordingRebindExceptionHandler(danglingRefFailureMode, rebindFailureMode);
         try {
             newApp = rebind(newManagementContext, exceptionHandler);
             fail();
         } catch (Exception e) {
-            if (!e.toString().contains("Problems rebinding")) throw e; // expected
+            assertFailureRebindingError(e);
+            Assert.assertTrue(e.toString().toLowerCase().contains("rebinding entity"), "Wrong error: "+e);
         }
 
         // exception handler should have been told about failure
@@ -143,7 +147,7 @@ public class RebindFailuresTest extends RebindTestFixtureWithApp {
                 .impl(MyEntityFailingImpl.class)
                 .configure(MyEntityFailingImpl.FAIL_ON_REBIND, true));
         
-        newManagementContext = new LocalManagementContext();
+        newManagementContext = LocalManagementContextForTests.newInstance();
         EntityManager newEntityManager = newManagementContext.getEntityManager();
         RecordingRebindExceptionHandler exceptionHandler = new RecordingRebindExceptionHandler(danglingRefFailureMode, rebindFailureMode);
         newApp = rebind(newManagementContext, exceptionHandler);
@@ -165,16 +169,13 @@ public class RebindFailuresTest extends RebindTestFixtureWithApp {
         File entitiesDir = Os.mkdirs(new File(mementoDir, "entities"));
         Files.write("invalid text", new File(entitiesDir, "mycorruptfile"), Charsets.UTF_8);
         
-        newManagementContext = new LocalManagementContext();
+        newManagementContext = LocalManagementContextForTests.newInstance();
         RecordingRebindExceptionHandler exceptionHandler = new RecordingRebindExceptionHandler(danglingRefFailureMode, rebindFailureMode);
         try {
             newApp = rebind(newManagementContext, exceptionHandler);
             fail();
         } catch (Exception e) {
-            if (e.toString().contains("Problem rebinding") || e.toString().contains("Problems rebinding")) 
-                /* expected */ ;
-            else 
-                throw e; 
+            assertFailureRebindingError(e);
         }
         
         // exception handler should have been told about failure
@@ -182,6 +183,14 @@ public class RebindFailuresTest extends RebindTestFixtureWithApp {
         assertEquals(exceptionHandler.loadMementoFailures.size(), 2, "exceptions="+exceptionHandler.loadMementoFailures);
     }
 
+    protected void assertFailureRebindingError(Exception e) {
+        if (e.toString().toLowerCase().matches(".*(problem|failure)(s?) rebinding.*")) { 
+            // expected
+        } else {
+            throw Exceptions.propagate(e);
+        }
+    }
+
     @Test
     public void testRebindWithFailingPolicyContinuesWithoutPolicy() throws Exception {
         origApp.addPolicy(PolicySpec.create(MyPolicyFailingImpl.class)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 f487252..f2314f7 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindTestFixture.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindTestFixture.java
@@ -94,7 +94,7 @@ public abstract class RebindTestFixture<T extends StartableApplication> {
     }
 
     protected boolean useEmptyCatalog() {
-        return false;
+        return true;
     }
 
     protected int getPersistPeriodMillis() {
@@ -158,7 +158,7 @@ public abstract class RebindTestFixture<T extends StartableApplication> {
     }
     
     protected BrooklynMementoManifest loadMementoManifest() throws Exception {
-        newManagementContext = new LocalManagementContextForTests();
+        newManagementContext = createNewManagementContext();
         FileBasedObjectStore objectStore = new FileBasedObjectStore(mementoDir);
         objectStore.injectManagementContext(newManagementContext);
         objectStore.prepareForSharedUse(PersistMode.AUTO, HighAvailabilityMode.DISABLED);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java b/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
index 65738a7..ee34e54 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RebindTestUtils.java
@@ -45,6 +45,7 @@ import brooklyn.entity.trait.Identifiable;
 import brooklyn.location.Location;
 import brooklyn.management.ManagementContext;
 import brooklyn.management.ha.HighAvailabilityMode;
+import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.mementos.BrooklynMemento;
@@ -249,12 +250,8 @@ public class RebindTestUtils {
     public static Collection<Application> rebindAll(LocalManagementContext newManagementContext, ClassLoader classLoader, RebindExceptionHandler exceptionHandler) throws Exception {
         LOG.info("Rebinding app");
 
-        List<Application> newApps;
-        if (exceptionHandler == null) {
-            newApps = newManagementContext.getRebindManager().rebind(classLoader);
-        } else {
-            newApps = newManagementContext.getRebindManager().rebind(classLoader, exceptionHandler);
-        }
+        List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader, exceptionHandler, ManagementNodeState.MASTER);
+        
         if (newApps.isEmpty()) throw new IllegalStateException("Application could not be rebinded; serialization probably failed");
         newManagementContext.getRebindManager().startPersistence();
         return newApps;
@@ -286,12 +283,7 @@ public class RebindTestUtils {
                 ((ManagementContextInternal)newManagementContext).getBrooklynProperties(),
                 classLoader);
         newManagementContext.getRebindManager().setPersister(newPersister, PersistenceExceptionHandlerImpl.builder().build());
-        List<Application> newApps;
-        if (exceptionHandler == null) {
-            newApps = newManagementContext.getRebindManager().rebind(classLoader);
-        } else {
-            newApps = newManagementContext.getRebindManager().rebind(classLoader, exceptionHandler);
-        }
+        List<Application> newApps = newManagementContext.getRebindManager().rebind(classLoader, exceptionHandler, ManagementNodeState.MASTER);
         newManagementContext.getRebindManager().startPersistence();
         return newApps;
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/test/java/brooklyn/entity/rebind/RecordingRebindExceptionHandler.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/RecordingRebindExceptionHandler.java b/core/src/test/java/brooklyn/entity/rebind/RecordingRebindExceptionHandler.java
index 2cec642..d2c8b8c 100644
--- a/core/src/test/java/brooklyn/entity/rebind/RecordingRebindExceptionHandler.java
+++ b/core/src/test/java/brooklyn/entity/rebind/RecordingRebindExceptionHandler.java
@@ -82,11 +82,6 @@ public class RecordingRebindExceptionHandler extends RebindExceptionHandlerImpl
     }
 
     @Override
-    public void onDone() {
-        super.onDone();
-    }
-
-    @Override
     public RuntimeException onFailed(Exception e) {
         failed = e;
         return super.onFailed(e);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/core/src/test/java/brooklyn/entity/rebind/transformer/CompoundTransformerTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/entity/rebind/transformer/CompoundTransformerTest.java b/core/src/test/java/brooklyn/entity/rebind/transformer/CompoundTransformerTest.java
index 6e9ab65..9474cd8 100644
--- a/core/src/test/java/brooklyn/entity/rebind/transformer/CompoundTransformerTest.java
+++ b/core/src/test/java/brooklyn/entity/rebind/transformer/CompoundTransformerTest.java
@@ -141,6 +141,7 @@ public class CompoundTransformerTest extends RebindTestFixtureWithApp {
                 objectStore,
                 ((ManagementContextInternal)origManagementContext).getBrooklynProperties(),
                 origManagementContext.getCatalog().getRootClassLoader());
+        persister.enableWriteAccess();
 
         PersistenceExceptionHandler exceptionHandler = PersistenceExceptionHandlerImpl.builder().build();
         persister.checkpoint(rawData, exceptionHandler);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 dbdf3d3..f7399d4 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 {
             ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
             BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
             mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
-            ha = new HighAvailabilityManagerImpl(mgmt)
+            ha = ((HighAvailabilityManagerImpl)mgmt.getHighAvailabilityManager())
                 .setPollPeriod(Duration.PRACTICALLY_FOREVER)
                 .setHeartbeatTimeout(Duration.THIRTY_SECONDS)
                 .setLocalTicker(ticker)
@@ -270,9 +270,9 @@ public class HighAvailabilityManagerSplitBrainTest {
         assertEquals(memento2c.getManagementNodes().get(n1.ownNodeId).getRemoteTimestamp(), time2);
         assertEquals(memento2c.getManagementNodes().get(n2.ownNodeId).getRemoteTimestamp(), time2);
 
-        // and no entities at n1
-        assertEquals(n1.mgmt.getApplications().size(), 0);
+        // right number of entities at n2; n1 may or may not depending whether hot standby is default
         assertEquals(n2.mgmt.getApplications().size(), 1);
+//        assertEquals(n1.mgmt.getApplications().size(), 0);
     }
     
     @Test(invocationCount=50, groups="Integration")

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 a657636..c646f69 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerTestFixture.java
@@ -91,7 +91,7 @@ public abstract class HighAvailabilityManagerTestFixture {
                 managementContext.getBrooklynProperties(), 
                 classLoader);
         managementContext.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
-        manager = new HighAvailabilityManagerImpl(managementContext)
+        manager = ((HighAvailabilityManagerImpl)managementContext.getHighAvailabilityManager())
                 .setPollPeriod(getPollPeriod())
                 .setHeartbeatTimeout(Duration.THIRTY_SECONDS)
                 .setPromotionListener(promotionListener)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/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 e4219f7..cc9f065 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -81,7 +81,7 @@ public class HotStandbyTest {
                 .setPollPeriod(Duration.PRACTICALLY_FOREVER)
                 .setHeartbeatTimeout(Duration.THIRTY_SECONDS)
                 .setPersister(persister);
-            log.info("Created "+nodeName+" "+ownNodeId+" - RM "+mgmt.getRebindManager());
+            log.info("Created "+nodeName+" "+ownNodeId);
         }
         
         public void tearDown() throws Exception {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
----------------------------------------------------------------------
diff --git a/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java b/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
index b8c14d7..2767444 100644
--- a/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
+++ b/software/webapp/src/test/java/brooklyn/entity/webapp/AbstractWebAppFixtureIntegrationTest.java
@@ -59,6 +59,7 @@ import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
 import brooklyn.management.ManagementContext;
 import brooklyn.management.SubscriptionContext;
 import brooklyn.management.SubscriptionHandle;
+import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.management.internal.LocalManagementContext;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.mementos.BrooklynMemento;
@@ -226,7 +227,7 @@ public abstract class AbstractWebAppFixtureIntegrationTest {
             BrooklynMementoPersisterToMultiFile newPersister = new BrooklynMementoPersisterToMultiFile(tempDir , getClass().getClassLoader());
             newManagementContext = new LocalManagementContextForTests();
             newManagementContext.getRebindManager().setPersister(newPersister, PersistenceExceptionHandlerImpl.builder().build());
-            newManagementContext.getRebindManager().rebind(getClass().getClassLoader());
+            newManagementContext.getRebindManager().rebind(getClass().getClassLoader(), null, ManagementNodeState.MASTER);
             newManagementContext.getRebindManager().startPersistence();
             SoftwareProcess entity2 = (SoftwareProcess) newManagementContext.getEntityManager().getEntity(tokill.getId());
             entity2.stop();

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
----------------------------------------------------------------------
diff --git a/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java b/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
index bea6c2e..df190d1 100644
--- a/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
+++ b/usage/launcher/src/main/java/brooklyn/launcher/BrooklynLauncher.java
@@ -19,7 +19,6 @@
 package brooklyn.launcher;
 
 import static com.google.common.base.Preconditions.checkNotNull;
-
 import brooklyn.catalog.CatalogLoadMode;
 import brooklyn.internal.BrooklynFeatureEnablement;
 import io.brooklyn.camp.CampPlatform;
@@ -73,6 +72,7 @@ import brooklyn.management.ManagementContext;
 import brooklyn.management.ha.HighAvailabilityManager;
 import brooklyn.management.ha.HighAvailabilityManagerImpl;
 import brooklyn.management.ha.HighAvailabilityMode;
+import brooklyn.management.ha.ManagementNodeState;
 import brooklyn.management.ha.ManagementPlaneSyncRecordPersister;
 import brooklyn.management.ha.ManagementPlaneSyncRecordPersisterToObjectStore;
 import brooklyn.management.internal.LocalManagementContext;
@@ -783,7 +783,7 @@ public class BrooklynLauncher {
 
         ClassLoader classLoader = managementContext.getCatalog().getRootClassLoader();
         try {
-            rebindManager.rebind(classLoader);
+            rebindManager.rebind(classLoader, null, ManagementNodeState.MASTER);
         } catch (Exception e) {
             Exceptions.propagateIfFatal(e);
             LOG.debug("Error rebinding to persisted state (rethrowing): "+e, e);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/ea4b4ccb/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
----------------------------------------------------------------------
diff --git a/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java b/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
index 419124d..f151d92 100644
--- a/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
+++ b/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherHighAvailabilityTest.java
@@ -130,7 +130,8 @@ public class BrooklynLauncherHighAvailabilityTest {
         ManagementContext secondaryManagementContext = secondary.getServerDetails().getManagementContext();
         log.info("started mgmt secondary "+secondaryManagementContext);
         
-        assertNoApps(secondary.getServerDetails().getManagementContext());
+        // TODO can assert it sees the apps read only
+//        assertNoApps(secondary.getServerDetails().getManagementContext());
 
         // Terminate primary; expect secondary to take over
         if (stopGracefully) {
@@ -143,11 +144,11 @@ public class BrooklynLauncherHighAvailabilityTest {
         
         assertOnlyAppEventually(secondaryManagementContext, TestApplication.class);
         
-        // Start tertiary (will come up as standby)
+        // Start tertiary (force up as standby)
         tertiary = BrooklynLauncher.newInstance();
         tertiary.webconsole(false)
                 .brooklynProperties(LocalManagementContextForTests.setEmptyCatalogAsDefault(BrooklynProperties.Factory.newEmpty()))
-                .highAvailabilityMode(HighAvailabilityMode.AUTO)
+                .highAvailabilityMode(HighAvailabilityMode.STANDBY)
                 .persistMode(PersistMode.AUTO)
                 .persistenceDir(persistenceDir)
                 .persistPeriod(Duration.millis(10))


[11/15] git commit: address @sjcorbett review comments

Posted by he...@apache.org.
address @sjcorbett review comments


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

Branch: refs/heads/master
Commit: 6d58882f9e6a33b870609017abc1433ae7611884
Parents: 32c926b
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Oct 1 16:17:33 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 17:01:52 2014 +0100

----------------------------------------------------------------------
 api/src/main/java/brooklyn/entity/Entity.java         | 11 +++++++++--
 .../java/brooklyn/entity/rebind/RebindManager.java    |  4 ++--
 .../java/brooklyn/management/ManagementContext.java   |  7 ++++---
 .../brooklyn/management/ha/ManagementNodeState.java   | 14 +++++++-------
 .../java/brooklyn/entity/basic/AbstractEntity.java    |  4 ++--
 .../src/main/java/brooklyn/entity/basic/Entities.java | 11 +++++++----
 .../internal/AbstractManagementContext.java           |  2 +-
 .../internal/NonDeploymentManagementContext.java      |  2 +-
 8 files changed, 33 insertions(+), 22 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/api/src/main/java/brooklyn/entity/Entity.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/Entity.java b/api/src/main/java/brooklyn/entity/Entity.java
index 8e32dcd..dba9a28 100644
--- a/api/src/main/java/brooklyn/entity/Entity.java
+++ b/api/src/main/java/brooklyn/entity/Entity.java
@@ -122,13 +122,20 @@ public interface Entity extends BrooklynObject {
     /** 
      * Add a child {@link Entity}, and set this entity as its parent,
      * returning the added child.
+     * <p>
+     * As with {@link #addChild(EntitySpec)} the child is <b>not</b> brought under management
+     * as part of this call.  It should not be managed prior to this call either.
      */
     <T extends Entity> T addChild(T child);
     
     /** 
      * Creates an {@link Entity} from the given spec and adds it, setting this entity as the parent,
-     * returning the added child. The added child is not yet managed, even if the parent is managed,
-     * so if adding post-management an explicit call to manage the child will be needed. */
+     * returning the added child.
+     * <p>
+     * The added child is <b>not</b> managed as part of this call, even if the parent is managed,
+     * so if adding post-management an explicit call to manage the child will be needed;
+     * see the convenience method <code>Entities.manage(...)</code>. 
+     * */
     <T extends Entity> T addChild(EntitySpec<T> spec);
     
     /** 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
index 9e62fa4..5f9ccf8 100644
--- a/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
+++ b/api/src/main/java/brooklyn/entity/rebind/RebindManager.java
@@ -65,7 +65,7 @@ public interface RebindManager {
     public List<Application> rebind(ClassLoader classLoader);
     /** @deprecated since 0.7; use {@link #rebind(ClassLoader, RebindExceptionHandler, ManagementNodeState)} */ @Deprecated
     public List<Application> rebind(ClassLoader classLoader, RebindExceptionHandler exceptionHandler);
-    /** causes this management context to rebind, loading data from the given backing store.
+    /** Causes this management context to rebind, loading data from the given backing store.
      * use wisely, as this can cause local entities to be completely lost, or will throw in many other situations.
      * in general it may be invoked for a new node becoming {@link ManagementNodeState#MASTER} 
      * or periodically for a node in {@link ManagementNodeState#HOT_STANDBY}. */
@@ -110,7 +110,7 @@ public interface RebindManager {
     /** waits for any needed or pending writes to complete */
     @VisibleForTesting
     public void waitForPendingComplete(Duration duration) throws InterruptedException, TimeoutException;
-    /** forcibly performs persistence, in the foreground */
+    /** Forcibly performs persistence, in the foreground */
     @VisibleForTesting
     public void forcePersistNow();
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/api/src/main/java/brooklyn/management/ManagementContext.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ManagementContext.java b/api/src/main/java/brooklyn/management/ManagementContext.java
index 6e3a3b1..26024b0 100644
--- a/api/src/main/java/brooklyn/management/ManagementContext.java
+++ b/api/src/main/java/brooklyn/management/ManagementContext.java
@@ -214,10 +214,11 @@ public interface ManagementContext {
     EntitlementManager getEntitlementManager();
  
 
-    /** as {@link #lookup(String, Class)} but not constraining the return type */
+    /** As {@link #lookup(String, Class)} but not constraining the return type */
     public BrooklynObject lookup(String id);
-    /** finds an entity with the given ID known at this management context */
+    
+    /** Finds an entity with the given ID known at this management context */
     // TODO in future support policies etc
-    public <T> T lookup(String id, Class<T> type); 
+    public <T extends BrooklynObject> T lookup(String id, Class<T> type); 
 
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java b/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
index d3b0508..c8a90f1 100644
--- a/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
+++ b/api/src/main/java/brooklyn/management/ha/ManagementNodeState.java
@@ -19,23 +19,23 @@
 package brooklyn.management.ha;
 
 public enum ManagementNodeState {
-    /** @deprecated since 0.7.0 synonym for maintenance (plus, it should have been UK english!) */
+    /** @deprecated since 0.7.0 synonym for maintenance (plus, it should have been US English!) */
     UNINITIALISED,
-    /** node is either coming online, or is in some kind of recovery/transitioning mode */
+    /** Node is either coming online, or is in some kind of recovery/transitioning mode */
     INITIALIZING,
     
-    /** node is in "lukewarm standby" mode, where it is available to be promoted to master,
+    /** Node is in "lukewarm standby" mode, where it is available to be promoted to master,
      * but does not have entities loaded and will require some effort to be promoted */
     STANDBY,
-    /** node is acting as read-only proxy */
+    /** Node is acting as read-only proxy */
     HOT_STANDBY,
-    /** node is running as primary/master, able to manage entities and create new ones */
+    /** Node is running as primary/master, able to manage entities and create new ones */
     // the semantics are intended to support multi-master here; we could have multiple master nodes,
     // but we need to look up who is master for any given entity
     MASTER,
 
-    /** node has failed and requires maintenance attention */
+    /** Node has failed and requires maintenance attention */
     FAILED,
-    /** node has gone away; maintenance not possible */
+    /** Node has gone away; maintenance not possible */
     TERMINATED;
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/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 de00c8a..e1429e9 100644
--- a/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
+++ b/core/src/main/java/brooklyn/entity/basic/AbstractEntity.java
@@ -686,8 +686,8 @@ public abstract class AbstractEntity extends AbstractBrooklynObject implements E
     @Override
     public ManagementContext getManagementContext() {
         // NB Sept 2014 - removed synch keyword above due to deadlock;
-        // it also synchs in ManagementSupport..getManagementContext();
-        // no apparent reason why it was here also.  @aledsage can you review?
+        // it also synchs in ManagementSupport.getManagementContext();
+        // no apparent reason why it was here also
         return getManagementSupport().getManagementContext();
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/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 aa669db..d32afa6 100644
--- a/core/src/main/java/brooklyn/entity/basic/Entities.java
+++ b/core/src/main/java/brooklyn/entity/basic/Entities.java
@@ -658,13 +658,16 @@ public class Entities {
      */
     public static void destroy(Entity e) {
         if (isManaged(e)) {
-            if (!isReadOnly(e)) {
+            if (isReadOnly(e)) {
+                unmanage(e);
+                log.debug("destroyed and unmanaged read-only copy of "+e);
+            } else {
                 if (e instanceof Startable) Entities.invokeEffector((EntityLocal)e, e, Startable.STOP).getUnchecked();
                 if (e instanceof EntityInternal) ((EntityInternal)e).destroy();
-            }
-            unmanage(e);
-            log.debug("destroyed and unmanaged "+e+"; mgmt now "+
+                unmanage(e);
+                log.debug("destroyed and unmanaged "+e+"; mgmt now "+
                     (e.getApplicationId()==null ? "(no app)" : e.getApplication().getManagementContext())+" - managed? "+isManaged(e));
+            }
         } else {
             log.debug("skipping destroy of "+e+": not managed");
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java b/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
index 4c6d7d9..ee4e518 100644
--- a/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/AbstractManagementContext.java
@@ -413,7 +413,7 @@ public abstract class AbstractManagementContext implements ManagementContextInte
     }
     
     @SuppressWarnings("unchecked")
-    public <T> T lookup(String id, Class<T> type) {
+    public <T extends BrooklynObject> T lookup(String id, Class<T> type) {
         Object result;
         result = getEntityManager().getEntity(id);
         if (result!=null && type.isInstance(result)) return (T)result;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/6d58882f/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index 46e3f97..7f07f14 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -424,7 +424,7 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
     }
 
     @Override
-    public <T> T lookup(String id, Class<T> type) {
+    public <T extends BrooklynObject> T lookup(String id, Class<T> type) {
         checkInitialManagementContextReal();
         return initialManagementContext.lookup(id, type);
     }


[07/15] git commit: Better memory leak testing, fixing some leaks, including escaped proxy/reference chains, and an issue with subscriptions not being cleared when children are removed (which it turned out was there previously).

Posted by he...@apache.org.
Better memory leak testing, fixing some leaks, including escaped proxy/reference chains,
and an issue with subscriptions not being cleared when children are removed
(which it turned out was there previously).


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

Branch: refs/heads/master
Commit: c3aac5e3ec69032ea51b71639549a22956f03e95
Parents: 4c866cb
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Oct 1 03:21:52 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:16 2014 +0100

----------------------------------------------------------------------
 .../entity/proxying/EntityProxyImpl.java        |  42 +++-
 .../rebind/PeriodicDeltaChangeListener.java     |  27 ++-
 .../entity/rebind/RebindManagerImpl.java        |  12 +-
 .../BrooklynMementoPersisterToObjectStore.java  |   3 +
 .../persister/RetryingMementoSerializer.java    |   4 +
 .../ha/HighAvailabilityManagerImpl.java         |   2 +-
 .../internal/BrooklynGarbageCollector.java      |   1 +
 .../internal/EntityManagementSupport.java       |   2 +-
 .../management/internal/LocalEntityManager.java |   6 +-
 .../brooklyn/management/ha/HotStandbyTest.java  | 224 +++++++++++++++++--
 .../entity/TestApplicationNoEnrichersImpl.java  |  15 --
 .../main/java/brooklyn/util/time/Duration.java  |  12 +
 12 files changed, 300 insertions(+), 50 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
index 094c24c..04b5d44 100644
--- a/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
+++ b/core/src/main/java/brooklyn/entity/proxying/EntityProxyImpl.java
@@ -21,9 +21,11 @@ package brooklyn.entity.proxying;
 import static com.google.common.base.Preconditions.checkNotNull;
 
 import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
 import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.WeakHashMap;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -59,6 +61,8 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
 
     private Entity delegate;
     private Boolean isMaster;
+
+    private WeakHashMap<Entity,Void> temporaryProxies = new WeakHashMap<Entity, Void>();
     
     private static final Set<MethodSignature> OBJECT_METHODS = Sets.newLinkedHashSet();
     static {
@@ -92,18 +96,46 @@ public class EntityProxyImpl implements java.lang.reflect.InvocationHandler {
                 extras);
         }
     }
-
+    
     public EntityProxyImpl(Entity entity) {
         this.delegate = checkNotNull(entity, "entity");
     }
     
     /** invoked to specify that a different underlying delegate should be used, 
      * e.g. because we are switching copy impls or switching primary/copy*/
-    // TODO XXX because the delegate has a backpointer to the proxy, there is potentially
-    // a nasty problem wherein an old proxy is kept, with a pointer to an old delegate.  
-    public synchronized void resetDelegate(Entity delegate) {
-        this.delegate = delegate;
+    public synchronized void resetDelegate(Entity thisProxy, Entity preferredProxy, Entity newDelegate) {
+        if (LOG.isTraceEnabled()) {
+            LOG.trace("updating "+Integer.toHexString(System.identityHashCode(thisProxy))
+                +" to be the same as "+Integer.toHexString(System.identityHashCode(preferredProxy))
+                +" pointing at "+Integer.toHexString(System.identityHashCode(newDelegate)) 
+                +" ("+temporaryProxies.size()+" temporary proxies)");
+        }
+
+        Entity oldDelegate = delegate;
+        this.delegate = newDelegate;
         this.isMaster = null;
+        
+        if (newDelegate==oldDelegate)
+            return;
+        
+        /* we have to make sure that any newly created proxy of the newDelegate 
+         * which have leaked (eg by being set as a child) also get repointed to this new delegate */
+        if (oldDelegate!=null) {
+            Entity temporaryProxy = ((AbstractEntity)oldDelegate).getProxy();
+            if (temporaryProxy!=null) temporaryProxies.put(temporaryProxy, null);
+            ((AbstractEntity)oldDelegate).resetProxy(preferredProxy);
+        }
+        if (newDelegate!=null) {   
+            Entity temporaryProxy = ((AbstractEntity)newDelegate).getProxy();
+            if (temporaryProxy!=null) temporaryProxies.put(temporaryProxy, null);
+            ((AbstractEntity)newDelegate).resetProxy(preferredProxy);
+        }
+        
+        // update any proxies which might be in use
+        for (Entity tp: temporaryProxies.keySet()) {
+            if (tp==thisProxy || tp==preferredProxy) continue;
+            ((EntityProxyImpl)(Proxy.getInvocationHandler(tp))).resetDelegate(tp, preferredProxy, newDelegate);
+        }
     }
     
     @Override

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
index e9cef0c..acf9de7 100644
--- a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
+++ b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
@@ -48,6 +48,8 @@ import brooklyn.util.exceptions.RuntimeInterruptedException;
 import brooklyn.util.task.BasicExecutionContext;
 import brooklyn.util.task.BasicTask;
 import brooklyn.util.task.ScheduledTask;
+import brooklyn.util.task.Tasks;
+import brooklyn.util.time.CountdownTimer;
 import brooklyn.util.time.Duration;
 import brooklyn.util.time.Time;
 
@@ -169,11 +171,32 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
         };
         scheduledTask = (ScheduledTask) executionContext.submit(new ScheduledTask(taskFactory).period(period));
     }
-    
+
+    /** stops persistence, waiting for it to complete */
     void stop() {
+        stop(Duration.TEN_SECONDS, Duration.ONE_SECOND);
+    }
+    void stop(Duration timeout, Duration graceTimeoutForSubsequentOperations) {
         stopped = true;
         running = false;
-        if (scheduledTask != null) scheduledTask.cancel();
+        
+        if (scheduledTask != null) {
+            CountdownTimer expiry = timeout.countdownTimer();
+            scheduledTask.cancel(false);
+            try {
+                waitForPendingComplete(expiry.getDurationRemaining().minimum(Duration.ZERO).add(graceTimeoutForSubsequentOperations));
+            } catch (Exception e) {
+                throw Exceptions.propagate(e);
+            }
+            scheduledTask.blockUntilEnded(expiry.getDurationRemaining().minimum(Duration.ZERO).add(graceTimeoutForSubsequentOperations));
+            scheduledTask.cancel(true);
+            boolean reallyEnded = Tasks.blockUntilInternalTasksEnded(scheduledTask, expiry.getDurationRemaining().minimum(Duration.ZERO).add(graceTimeoutForSubsequentOperations));
+            if (!reallyEnded) {
+                LOG.warn("Persistence tasks took too long to complete when stopping persistence (ignoring): "+scheduledTask);
+            }
+            scheduledTask = null;
+        }
+
 
         // Discard all state that was waiting to be persisted
         synchronized (this) {

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/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 8c067f1..2f85db6 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -87,7 +87,6 @@ import brooklyn.util.exceptions.RuntimeInterruptedException;
 import brooklyn.util.flags.FlagUtils;
 import brooklyn.util.javalang.Reflections;
 import brooklyn.util.task.BasicExecutionContext;
-import brooklyn.util.task.BasicTask;
 import brooklyn.util.task.ScheduledTask;
 import brooklyn.util.task.Tasks;
 import brooklyn.util.text.Strings;
@@ -266,8 +265,12 @@ public class RebindManagerImpl implements RebindManager {
             return;
         }
         LOG.debug("Starting read-only rebinding, mgmt "+managementContext.getManagementNodeId());
-        readOnlyRunning = true;
         
+        if (persistenceRealChangeListener != null) persistenceRealChangeListener.stop();
+        if (persistenceStoreAccess != null) persistenceStoreAccess.disableWriteAccess(true);
+        
+        readOnlyRunning = true;
+
         try {
             rebind(null, null, ManagementNodeState.HOT_STANDBY);
         } catch (Exception e) {
@@ -276,7 +279,7 @@ public class RebindManagerImpl implements RebindManager {
         
         Callable<Task<?>> taskFactory = new Callable<Task<?>>() {
             @Override public Task<Void> call() {
-                return new BasicTask<Void>(new Callable<Void>() {
+                return Tasks.<Void>builder().dynamic(false).name("rebind (periodic run").body(new Callable<Void>() {
                     public Void call() {
                         try {
                             rebind(null, null, ManagementNodeState.HOT_STANDBY);
@@ -301,7 +304,7 @@ public class RebindManagerImpl implements RebindManager {
                             LOG.warn("Problem rebinding (rethrowing)", t);
                             throw Exceptions.propagate(t);
                         }
-                    }});
+                    }}).build();
             }
         };
         readOnlyTask = (ScheduledTask) managementContext.getServerExecutionContext().submit(
@@ -353,6 +356,7 @@ public class RebindManagerImpl implements RebindManager {
     
     @Override
     @VisibleForTesting
+    @Deprecated /** @deprecated since 0.7.0 use Duration as argument */
     public void waitForPendingComplete(long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {
         waitForPendingComplete(Duration.of(timeout, unit));
     }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
index f3a2882..a751ada 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/BrooklynMementoPersisterToObjectStore.java
@@ -644,6 +644,9 @@ public class BrooklynMementoPersisterToObjectStore implements BrooklynMementoPer
     
     private void persist(String subPath, BrooklynObjectType type, String id, String content, PersistenceExceptionHandler exceptionHandler) {
         try {
+            if (content==null) {
+                LOG.warn("Null content for "+type+" "+id);
+            }
             getWriter(getPath(subPath, id)).put(content);
         } catch (Exception e) {
             exceptionHandler.onPersistRawMementoFailed(type, id, e);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/core/src/main/java/brooklyn/entity/rebind/persister/RetryingMementoSerializer.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/persister/RetryingMementoSerializer.java b/core/src/main/java/brooklyn/entity/rebind/persister/RetryingMementoSerializer.java
index 9860571..f94fa45 100644
--- a/core/src/main/java/brooklyn/entity/rebind/persister/RetryingMementoSerializer.java
+++ b/core/src/main/java/brooklyn/entity/rebind/persister/RetryingMementoSerializer.java
@@ -58,6 +58,9 @@ public class RetryingMementoSerializer<T> implements MementoSerializer<T> {
     
     @Override
     public T fromString(String string) {
+        if (string==null)
+            return null;
+        
         RuntimeException lastException = null;
         int attempt = 0;
         do {
@@ -65,6 +68,7 @@ public class RetryingMementoSerializer<T> implements MementoSerializer<T> {
             try {
                 return delegate.fromString(string);
             } catch (RuntimeException e) {
+                // trying multiple times only makes sense for a few errors (namely ConcModExceptions); perhaps deprecate that strategy?
                 LOG.warn("Error deserializing memento (attempt "+attempt+" of "+maxAttempts+")", e);
                 lastException = e;
             }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/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 0595db5..bdd6eba 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -245,7 +245,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         case HOT_STANDBY:
             if (existingMaster != null) {
                 publishAndCheck(true);
-                LOG.info("Management node "+ownNodeId+" started as "+startMode+" explicitly, status "+nodeState);
+                LOG.info("Management node "+ownNodeId+" started as "+startMode+" explicitly");
             } else {
                 throw new IllegalStateException("No existing master; cannot start as standby");
             }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/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 b345ab3..1e10de7 100644
--- a/core/src/main/java/brooklyn/management/internal/BrooklynGarbageCollector.java
+++ b/core/src/main/java/brooklyn/management/internal/BrooklynGarbageCollector.java
@@ -180,6 +180,7 @@ public class BrooklynGarbageCollector {
             TimeUnit.MILLISECONDS);
     }
 
+    /** force a round of Brooklyn garbage collection */
     public void gcIteration() {
         try {
             logUsage("brooklyn gc (before)");

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java b/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
index 61ff168..026cf49 100644
--- a/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
+++ b/core/src/main/java/brooklyn/management/internal/EntityManagementSupport.java
@@ -315,7 +315,7 @@ public class EntityManagementSupport {
             if (managementContext != info.getManagementContext()) {
                 throw new IllegalStateException("Has different management context: "+managementContext+"; expected "+info.getManagementContext());
             }
-            if (subscriptionContext != null) subscriptionContext.unsubscribeAll();
+            getSubscriptionContext().unsubscribeAll();
             entityChangeListener = EntityChangeListener.NOOP;
             managementContextUsable.set(false);
             currentlyDeployed.set(false);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
index 52050a4..0eae80b 100644
--- a/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
+++ b/core/src/main/java/brooklyn/management/internal/LocalEntityManager.java
@@ -487,10 +487,10 @@ public class LocalEntityManager implements EntityManagerInternal {
             if (mode==ManagementTransitionMode.CREATING) {
                 throw new IllegalStateException("call to manage entity "+e+" but already had proxy "+oldProxy+" already known under that id at "+this);
             }
-            // set the new entity to the old proxy
-            ((AbstractEntity)realE).resetProxy(oldProxy);
+            // make the old proxy point at this new delegate
+            // (some other tricks done in the call below)
+            ((EntityProxyImpl)(Proxy.getInvocationHandler(oldProxy))).resetDelegate(oldProxy, oldProxy, realE);
             proxyE = oldProxy;
-            ((EntityProxyImpl)(Proxy.getInvocationHandler(proxyE))).resetDelegate(realE);
         } else {
             proxyE = toProxyEntityIfAvailable(e);
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/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 befdae4..492f2f1 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -20,7 +20,9 @@ package brooklyn.management.ha;
 
 import static org.testng.Assert.assertEquals;
 
+import java.util.ArrayDeque;
 import java.util.Date;
+import java.util.Deque;
 import java.util.List;
 import java.util.Map;
 
@@ -36,12 +38,14 @@ import brooklyn.entity.Entity;
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.rebind.PersistenceExceptionHandlerImpl;
+import brooklyn.entity.rebind.RebindManagerImpl;
 import brooklyn.entity.rebind.persister.BrooklynMementoPersisterToObjectStore;
 import brooklyn.entity.rebind.persister.InMemoryObjectStore;
 import brooklyn.entity.rebind.persister.ListeningObjectStore;
 import brooklyn.entity.rebind.persister.PersistMode;
 import brooklyn.entity.rebind.persister.PersistenceObjectStore;
 import brooklyn.location.Location;
+import brooklyn.management.internal.AbstractManagementContext;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.test.EntityTestUtils;
 import brooklyn.test.entity.LocalManagementContextForTests;
@@ -49,7 +53,10 @@ import brooklyn.test.entity.TestApplication;
 import brooklyn.test.entity.TestEntity;
 import brooklyn.util.collections.MutableList;
 import brooklyn.util.collections.MutableMap;
+import brooklyn.util.javalang.JavaClassNames;
+import brooklyn.util.text.ByteSizeStrings;
 import brooklyn.util.time.Duration;
+import brooklyn.util.time.Time;
 
 import com.google.common.collect.Iterables;
 
@@ -70,8 +77,8 @@ public class HotStandbyTest {
         private ListeningObjectStore objectStore;
         private ManagementPlaneSyncRecordPersister persister;
         private HighAvailabilityManagerImpl ha;
+        private Duration persistOrRebindPeriod = Duration.ONE_SECOND;
 
-        @BeforeMethod(alwaysRun=true)
         public void setUp() throws Exception {
             nodeName = "node "+nodes.size();
             mgmt = newLocalManagementContext();
@@ -82,6 +89,7 @@ public class HotStandbyTest {
             persister = new ManagementPlaneSyncRecordPersisterToObjectStore(mgmt, objectStore, classLoader);
             ((ManagementPlaneSyncRecordPersisterToObjectStore)persister).allowRemoteTimestampInMemento();
             BrooklynMementoPersisterToObjectStore persisterObj = new BrooklynMementoPersisterToObjectStore(objectStore, mgmt.getBrooklynProperties(), classLoader);
+            ((RebindManagerImpl)mgmt.getRebindManager()).setPeriodicPersistPeriod(persistOrRebindPeriod);
             mgmt.getRebindManager().setPersister(persisterObj, PersistenceExceptionHandlerImpl.builder().build());
             ha = ((HighAvailabilityManagerImpl)mgmt.getHighAvailabilityManager())
                 .setPollPeriod(Duration.PRACTICALLY_FOREVER)
@@ -90,11 +98,15 @@ public class HotStandbyTest {
             log.info("Created "+nodeName+" "+ownNodeId);
         }
         
-        public void tearDown() throws Exception {
+        public void tearDownThisOnly() throws Exception {
             if (ha != null) ha.stop();
             if (mgmt!=null) mgmt.getRebindManager().stop();
-            
             if (mgmt != null) Entities.destroyAll(mgmt);
+        }
+        
+        public void tearDownAll() throws Exception {
+            tearDownThisOnly();
+            // can't delete the object store until all being torn down
             if (objectStore != null) objectStore.deleteCompletely();
         }
         
@@ -110,8 +122,9 @@ public class HotStandbyTest {
         sharedBackingStore.clear();
     }
     
-    public HaMgmtNode newNode() throws Exception {
+    public HaMgmtNode newNode(Duration persistOrRebindPeriod) throws Exception {
         HaMgmtNode node = new HaMgmtNode();
+        node.persistOrRebindPeriod = persistOrRebindPeriod;
         node.setUp();
         nodes.add(node);
         return node;
@@ -120,7 +133,7 @@ public class HotStandbyTest {
     @AfterMethod(alwaysRun=true)
     public void tearDown() throws Exception {
         for (HaMgmtNode n: nodes)
-            n.tearDown();
+            n.tearDownAll();
     }
 
     protected ManagementContextInternal newLocalManagementContext() {
@@ -131,37 +144,43 @@ public class HotStandbyTest {
         return new InMemoryObjectStore(sharedBackingStore, sharedBackingStoreDates);
     }
 
-    private HaMgmtNode createMaster() throws Exception {
-        HaMgmtNode n1 = newNode();
+    private HaMgmtNode createMaster(Duration persistOrRebindPeriod) throws Exception {
+        HaMgmtNode n1 = newNode(persistOrRebindPeriod);
         n1.ha.start(HighAvailabilityMode.AUTO);
         assertEquals(n1.ha.getNodeState(), ManagementNodeState.MASTER);
         return n1;
     }
     
-    private HaMgmtNode createHotStandby() throws Exception {
-        HaMgmtNode n2 = newNode();
+    private HaMgmtNode createHotStandby(Duration rebindPeriod) throws Exception {
+        HaMgmtNode n2 = newNode(rebindPeriod);
         n2.ha.start(HighAvailabilityMode.HOT_STANDBY);
         assertEquals(n2.ha.getNodeState(), ManagementNodeState.HOT_STANDBY);
         return n2;
     }
 
-    private TestApplication createFirstAppAndPersist(HaMgmtNode n1) throws Exception {        
+    private TestApplication createFirstAppAndPersist(HaMgmtNode n1) throws Exception {
         TestApplication app = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        // for testing without enrichers, if desired:
+//        TestApplication app = ApplicationBuilder.newManagedApp(EntitySpec.create(TestApplication.class).impl(TestApplicationNoEnrichersImpl.class), n1.mgmt);
         app.setDisplayName("First App");
         app.start(MutableList.<Location>of());
         app.setConfig(TestEntity.CONF_NAME, "first-app");
         app.setAttribute(TestEntity.SEQUENCE, 3);
         
-        n1.mgmt.getRebindManager().forcePersistNow();
+        forcePersistNow(n1);
         return app;
     }
+
+    protected void forcePersistNow(HaMgmtNode n1) {
+        n1.mgmt.getRebindManager().forcePersistNow();
+    }
     
     private Application expectRebindSequenceNumber(HaMgmtNode master, HaMgmtNode hotStandby, Application app, int expectedSensorSequenceValue, boolean immediate) {
         Application appRO = hotStandby.mgmt.lookup(app.getId(), Application.class);
 
         if (immediate) {
-            master.mgmt.getRebindManager().forcePersistNow();
-            hotStandby.mgmt.getRebindManager().rebind(null, null, ManagementNodeState.HOT_STANDBY);
+            forcePersistNow(master);
+            forceRebindNow(hotStandby);
             EntityTestUtils.assertAttributeEquals(appRO, TestEntity.SEQUENCE, expectedSensorSequenceValue);
         } else {
             EntityTestUtils.assertAttributeEqualsEventually(appRO, TestEntity.SEQUENCE, expectedSensorSequenceValue);
@@ -175,12 +194,16 @@ public class HotStandbyTest {
         
         return appRO;
     }
+
+    private void forceRebindNow(HaMgmtNode hotStandby) {
+        hotStandby.mgmt.getRebindManager().rebind(null, null, ManagementNodeState.HOT_STANDBY);
+    }
     
     @Test
     public void testHotStandbySeesChangedNameConfigAndSensorValueButDoesntAllowChange() throws Exception {
-        HaMgmtNode n1 = createMaster();
+        HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
         TestApplication app = createFirstAppAndPersist(n1);
-        HaMgmtNode n2 = createHotStandby();
+        HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
 
         assertEquals(n2.mgmt.getApplications().size(), 1);
         Application appRO = n2.mgmt.lookup(app.getId(), Application.class);
@@ -200,9 +223,9 @@ public class HotStandbyTest {
 
     @Test
     public void testHotStandbySeesChangedNameConfigAndSensorValue() throws Exception {
-        HaMgmtNode n1 = createMaster();
+        HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
         TestApplication app = createFirstAppAndPersist(n1);
-        HaMgmtNode n2 = createHotStandby();
+        HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
 
         assertEquals(n2.mgmt.getApplications().size(), 1);
         Application appRO = n2.mgmt.lookup(app.getId(), Application.class);
@@ -243,9 +266,9 @@ public class HotStandbyTest {
     }
     
     public void doTestHotStandbySeesStructuralChangesIncludingRemoval(boolean immediate) throws Exception {
-        HaMgmtNode n1 = createMaster();
+        HaMgmtNode n1 = createMaster(immediate ? Duration.PRACTICALLY_FOREVER : Duration.millis(200));
         TestApplication app = createFirstAppAndPersist(n1);
-        HaMgmtNode n2 = createHotStandby();
+        HaMgmtNode n2 = createHotStandby(immediate ? Duration.PRACTICALLY_FOREVER : Duration.millis(200));
 
         assertEquals(n2.mgmt.getApplications().size(), 1);
         Application appRO = n2.mgmt.lookup(app.getId(), Application.class);
@@ -294,5 +317,168 @@ public class HotStandbyTest {
     public void testHotStandbySeesStructuralChangesIncludingRemovalManyTimes() throws Exception {
         doTestHotStandbySeesStructuralChangesIncludingRemoval(true);
     }
+
+    Deque<Long> usedMemory = new ArrayDeque<Long>();
+    protected long noteUsedMemory(String message) {
+        Time.sleep(Duration.millis(200));
+        for (HaMgmtNode n: nodes) {
+            ((AbstractManagementContext)n.mgmt).getGarbageCollector().gcIteration();
+        }
+        System.gc(); System.gc();
+        Time.sleep(Duration.millis(50));
+        System.gc(); System.gc();
+        long mem = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
+        usedMemory.addLast(mem);
+        log.info("Memory used - "+message+": "+ByteSizeStrings.java().apply(mem));
+        return mem;
+    }
+    public void assertUsedMemoryLessThan(String event, long max) {
+        noteUsedMemory(event);
+        long nowUsed = usedMemory.peekLast();
+        if (nowUsed > max) {
+            // aggressively try to force GC
+            Time.sleep(Duration.ONE_SECOND);
+            usedMemory.removeLast();
+            noteUsedMemory(event+" (extra GC)");
+            nowUsed = usedMemory.peekLast();
+            if (nowUsed > max) {
+                Assert.fail("Too much memory used - "+ByteSizeStrings.java().apply(nowUsed)+" > max "+ByteSizeStrings.java().apply(max));
+            }
+        }
+    }
+    public void assertUsedMemoryMaxDelta(String event, long deltaMegabytes) {
+        assertUsedMemoryLessThan(event, usedMemory.peekLast() + deltaMegabytes*1024*1024);
+    }
+
+    @Test(groups="Integration")
+    public void testHotStandbyDoesNotLeakLotsOfRebinds() throws Exception {
+        log.info("Starting test "+JavaClassNames.niceClassAndMethod());
+        final int DELTA = 2;
+        
+        HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
+        TestApplication app = createFirstAppAndPersist(n1);
+        long initialUsed = noteUsedMemory("Created app");
+        
+        HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
+        assertUsedMemoryMaxDelta("Standby created", DELTA);
+        
+        forcePersistNow(n1);
+        forceRebindNow(n2);
+        assertUsedMemoryMaxDelta("Persisted and rebinded once", DELTA);
+        
+        for (int i=0; i<10; i++) {
+            forcePersistNow(n1);
+            forceRebindNow(n2);
+        }
+        assertUsedMemoryMaxDelta("Persisted and rebinded 10x", DELTA);
+        
+        for (int i=0; i<1000; i++) {
+            if ((i+1)%100==0) {
+                noteUsedMemory("iteration "+(i+1));
+                usedMemory.removeLast();
+            }
+            forcePersistNow(n1);
+            forceRebindNow(n2);
+        }
+        assertUsedMemoryMaxDelta("Persisted and rebinded 1000x", DELTA);
+        
+        Entities.unmanage(app);
+        forcePersistNow(n1);
+        forceRebindNow(n2);
+        
+        assertUsedMemoryLessThan("And now all unmanaged", initialUsed + DELTA*1000*1000);
+    }
+
+    static class BigObject {
+        public BigObject(int sizeBytes) { array = new byte[sizeBytes]; }
+        byte[] array;
+    }
     
+    @Test(groups="Integration")
+    public void testHotStandbyDoesNotLeakBigObjects() throws Exception {
+        log.info("Starting test "+JavaClassNames.niceClassAndMethod());
+        final int SIZE = 5;
+        final int SIZE_UP_BOUND = SIZE+2;
+        final int SIZE_DOWN_BOUND = SIZE-1;
+        final int GRACE = 2;
+        // the XML persistence uses a lot of space, we approx at between 2x and 3c
+        final int SIZE_IN_XML = 3*SIZE;
+        final int SIZE_IN_XML_DOWN = 2*SIZE;
+        
+        HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
+        TestApplication app = createFirstAppAndPersist(n1);        
+        noteUsedMemory("Finished seeding");
+        Long initialUsed = usedMemory.peekLast();
+        app.setConfig(TestEntity.CONF_OBJECT, new BigObject(SIZE*1000*1000));
+        assertUsedMemoryMaxDelta("Set a big config object", SIZE_UP_BOUND);
+        forcePersistNow(n1);
+        assertUsedMemoryMaxDelta("Persisted a big config object", SIZE_IN_XML);
+        
+        HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
+        forceRebindNow(n2);
+        assertUsedMemoryMaxDelta("Rebinded", SIZE_UP_BOUND);
+        
+        for (int i=0; i<10; i++)
+            forceRebindNow(n2);
+        assertUsedMemoryMaxDelta("Several more rebinds", GRACE);
+        for (int i=0; i<10; i++) {
+            forcePersistNow(n1);
+            forceRebindNow(n2);
+        }
+        assertUsedMemoryMaxDelta("And more rebinds and more persists", GRACE);
+        
+        app.setConfig(TestEntity.CONF_OBJECT, "big is now small");
+        assertUsedMemoryMaxDelta("Big made small at primary", -SIZE_DOWN_BOUND);
+        forcePersistNow(n1);
+        assertUsedMemoryMaxDelta("And persisted", -SIZE_IN_XML_DOWN);
+        
+        forceRebindNow(n2);
+        assertUsedMemoryMaxDelta("And at secondary", -SIZE_DOWN_BOUND);
+        
+        Entities.unmanage(app);
+        forcePersistNow(n1);
+        forceRebindNow(n2);
+        
+        assertUsedMemoryLessThan("And now all unmanaged", initialUsed+GRACE*1000*1000);
+    }
+
+    @Test(groups="Integration") // because it's slow
+    public void testHotStandbyDoesNotLeakLotsOfRebindsCreatingAndDestroyingAChildEntity() throws Exception {
+        log.info("Starting test "+JavaClassNames.niceClassAndMethod());
+        final int DELTA = 2;
+        
+        HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
+        TestApplication app = createFirstAppAndPersist(n1);
+        long initialUsed = noteUsedMemory("Created app");
+        
+        HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
+        assertUsedMemoryMaxDelta("Standby created", DELTA);
+        
+        TestEntity lastChild = app.addChild(EntitySpec.create(TestEntity.class).configure(TestEntity.CONF_NAME, "first-child"));
+        Entities.manage(lastChild);
+        forcePersistNow(n1);
+        forceRebindNow(n2);
+        assertUsedMemoryMaxDelta("Child created and rebinded once", DELTA);
+        
+        for (int i=0; i<1000; i++) {
+            if (i==9 || (i+1)%100==0) {
+                noteUsedMemory("iteration "+(i+1));
+                usedMemory.removeLast();
+            }
+            TestEntity newChild = app.addChild(EntitySpec.create(TestEntity.class).configure(TestEntity.CONF_NAME, "first-child"));
+            Entities.manage(newChild);
+            Entities.unmanage(lastChild);
+            lastChild = newChild;
+            
+            forcePersistNow(n1);
+            forceRebindNow(n2);
+        }
+        assertUsedMemoryMaxDelta("Persisted and rebinded 1000x", DELTA);
+        
+        Entities.unmanage(app);
+        forcePersistNow(n1);
+        forceRebindNow(n2);
+        
+        assertUsedMemoryLessThan("And now all unmanaged", initialUsed + DELTA*1000*1000);
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/core/src/test/java/brooklyn/test/entity/TestApplicationNoEnrichersImpl.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/test/entity/TestApplicationNoEnrichersImpl.java b/core/src/test/java/brooklyn/test/entity/TestApplicationNoEnrichersImpl.java
index 1435443..a56fbcb 100644
--- a/core/src/test/java/brooklyn/test/entity/TestApplicationNoEnrichersImpl.java
+++ b/core/src/test/java/brooklyn/test/entity/TestApplicationNoEnrichersImpl.java
@@ -18,21 +18,6 @@
  */
 package brooklyn.test.entity;
 
-import java.util.Map;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import brooklyn.entity.Entity;
-import brooklyn.entity.Group;
-import brooklyn.entity.basic.AbstractApplication;
-import brooklyn.entity.proxying.EntitySpec;
-import brooklyn.event.Sensor;
-import brooklyn.event.SensorEventListener;
-import brooklyn.location.LocationSpec;
-import brooklyn.location.basic.LocalhostMachineProvisioningLocation;
-import brooklyn.management.SubscriptionHandle;
-import brooklyn.util.logging.LoggingSetup;
 
 /**
  * Mock application for testing.

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/c3aac5e3/utils/common/src/main/java/brooklyn/util/time/Duration.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/time/Duration.java b/utils/common/src/main/java/brooklyn/util/time/Duration.java
index 73cc314..c6622ba 100644
--- a/utils/common/src/main/java/brooklyn/util/time/Duration.java
+++ b/utils/common/src/main/java/brooklyn/util/time/Duration.java
@@ -281,4 +281,16 @@ public class Duration implements Comparable<Duration>, Serializable {
         return compareTo(x) < 0;
     }
 
+    /** returns the larger of this value or the argument */
+    public Duration minimum(Duration alternateMinimumValue) {
+        if (isShorterThan(alternateMinimumValue)) return alternateMinimumValue;
+        return this;
+    }
+
+    /** returns the smaller of this value or the argument */
+    public Duration maximum(Duration alternateMaximumValue) {
+        if (isLongerThan(alternateMaximumValue)) return alternateMaximumValue;
+        return this;
+    }
+
 }


[08/15] git commit: support programmatic API on HighAvailabilityManager to changeMode and setPriority; priority persisted and used to elect master (also prefers non-snapshot and more recent Brooklyn versions by default, unless explicit priority overrides

Posted by he...@apache.org.
support programmatic API on HighAvailabilityManager to changeMode and setPriority;
priority persisted and used to elect master (also prefers non-snapshot and more recent Brooklyn versions by default, unless explicit priority overrides).
good test for changing mode in HotStandbyTest.testChangeMode.


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

Branch: refs/heads/master
Commit: 32c926b95c8e89538778cd85010fa2e9a20ecf74
Parents: c3aac5e
Author: Alex Heneveld <al...@cloudsoftcorp.com>
Authored: Wed Oct 1 11:32:26 2014 +0100
Committer: Alex Heneveld <al...@cloudsoftcorp.com>
Committed: Wed Oct 1 16:40:16 2014 +0100

----------------------------------------------------------------------
 .../management/ha/HighAvailabilityManager.java  |  13 ++
 .../management/ha/ManagementNodeSyncRecord.java |   2 +
 .../ha/ManagementPlaneSyncRecordPersister.java  |   1 +
 .../rebind/PeriodicDeltaChangeListener.java     |   5 +-
 .../entity/rebind/RebindManagerImpl.java        |  12 +-
 .../dto/BasicManagementNodeSyncRecord.java      |  22 ++-
 .../management/ha/BasicMasterChooser.java       |  64 ++++++--
 .../ha/HighAvailabilityManagerImpl.java         | 148 +++++++++++++++----
 .../ha/ManagementPlaneSyncRecordDeltaImpl.java  |  11 +-
 ...ntPlaneSyncRecordPersisterToObjectStore.java |  15 +-
 .../NonDeploymentManagementContext.java         |  13 +-
 .../HighAvailabilityManagerSplitBrainTest.java  |   8 +-
 .../brooklyn/management/ha/HotStandbyTest.java  |  78 ++++++++++
 .../management/ha/MasterChooserTest.java        |  36 ++++-
 ...lynLauncherRebindToCloudObjectStoreTest.java |   4 -
 .../util/text/NaturalOrderComparator.java       |   3 +-
 16 files changed, 373 insertions(+), 62 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java b/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
index d9888f9..72bb70f 100644
--- a/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
+++ b/api/src/main/java/brooklyn/management/ha/HighAvailabilityManager.java
@@ -61,6 +61,7 @@ public interface HighAvailabilityManager {
      * instead of {@link #start(HighAvailabilityMode)}. It may be an error if
      * this is called after this HA Manager is started.
      */
+    @Beta
     void disabled();
 
     /** Whether HA mode is operational */
@@ -83,6 +84,18 @@ public interface HighAvailabilityManager {
      */
     void stop();
 
+    /** changes the mode that this HA server is running in
+     * <p>
+     * note it will be an error to {@link #changeMode(HighAvailabilityMode)} to {@link ManagementNodeState#MASTER} 
+     * when there is already a master; to promote a node explicitly set its priority higher than
+     * the others and invoke {@link #changeMode(HighAvailabilityMode)} to a standby mode on the existing master */
+    void changeMode(HighAvailabilityMode mode);
+
+    /** sets the priority, and publishes it synchronously so it is canonical */
+    void setPriority(long priority);
+    
+    long getPriority();
+    
     /**
      * Returns a snapshot of the management-plane's current / most-recently-known status.
      * <p>

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/api/src/main/java/brooklyn/management/ha/ManagementNodeSyncRecord.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ha/ManagementNodeSyncRecord.java b/api/src/main/java/brooklyn/management/ha/ManagementNodeSyncRecord.java
index bbd8665..38150bf 100644
--- a/api/src/main/java/brooklyn/management/ha/ManagementNodeSyncRecord.java
+++ b/api/src/main/java/brooklyn/management/ha/ManagementNodeSyncRecord.java
@@ -46,6 +46,8 @@ public interface ManagementNodeSyncRecord {
     
     ManagementNodeState getStatus();
 
+    Long getPriority();
+    
     /** timestamp set by the originating management machine */
     long getLocalTimestamp();
 

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/api/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersister.java
----------------------------------------------------------------------
diff --git a/api/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersister.java b/api/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersister.java
index ebacd7a..f2d42fc 100644
--- a/api/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersister.java
+++ b/api/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersister.java
@@ -63,5 +63,6 @@ public interface ManagementPlaneSyncRecordPersister {
         Collection<String> getRemovedNodeIds();
         MasterChange getMasterChange();
         String getNewMasterOrNull();
+        String getExpectedMasterToClear();
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
index acf9de7..48da586 100644
--- a/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
+++ b/core/src/main/java/brooklyn/entity/rebind/PeriodicDeltaChangeListener.java
@@ -144,6 +144,7 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
             LOG.warn("Request to start "+this+" when already running - "+scheduledTask+"; ignoring");
             return;
         }
+        stopped = false;
         running = true;
         
         Callable<Task<?>> taskFactory = new Callable<Task<?>>() {
@@ -298,7 +299,9 @@ public class PeriodicDeltaChangeListener implements ChangeListener {
     
     @VisibleForTesting
     public void persistNow() {
-        if (!isActive()) return;
+        if (!isActive()) {
+            return;
+        }
         try {
             persistingMutex.acquire();
             if (!isActive()) return;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/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 2f85db6..745b7cb 100644
--- a/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
+++ b/core/src/main/java/brooklyn/entity/rebind/RebindManagerImpl.java
@@ -206,6 +206,14 @@ public class RebindManagerImpl implements RebindManager {
         setPeriodicPersistPeriod(Duration.of(periodMillis, TimeUnit.MILLISECONDS));
     }
 
+    public boolean isPersistenceRunning() {
+        return persistenceRunning;
+    }
+    
+    public boolean isReadOnlyRunning() {
+        return readOnlyRunning;
+    }
+    
     @Override
     public void setPersister(BrooklynMementoPersister val) {
         PersistenceExceptionHandler exceptionHandler = PersistenceExceptionHandlerImpl.builder()
@@ -905,9 +913,9 @@ public class RebindManagerImpl implements RebindManager {
             if (wasReadOnly && isNowReadOnly)
                 return ManagementTransitionMode.REBINDING_READONLY;
             else if (wasReadOnly)
-                return ManagementTransitionMode.REBINDING_NO_LONGER_PRIMARY;
-            else if (isNowReadOnly)
                 return ManagementTransitionMode.REBINDING_BECOMING_PRIMARY;
+            else if (isNowReadOnly)
+                return ManagementTransitionMode.REBINDING_NO_LONGER_PRIMARY;
             else
                 throw new IllegalStateException("Rebinding master not supported: "+item);
         }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/core/src/main/java/brooklyn/entity/rebind/plane/dto/BasicManagementNodeSyncRecord.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/entity/rebind/plane/dto/BasicManagementNodeSyncRecord.java b/core/src/main/java/brooklyn/entity/rebind/plane/dto/BasicManagementNodeSyncRecord.java
index c7aa008..bc1e137 100644
--- a/core/src/main/java/brooklyn/entity/rebind/plane/dto/BasicManagementNodeSyncRecord.java
+++ b/core/src/main/java/brooklyn/entity/rebind/plane/dto/BasicManagementNodeSyncRecord.java
@@ -32,7 +32,8 @@ import brooklyn.util.time.Time;
 import com.google.common.base.Objects;
 
 /**
- * Represents the state of a management node within the Brooklyn management plane.
+ * Represents the state of a management node within the Brooklyn management plane
+ * (DTO class).
  * 
  * @author aled
  */
@@ -50,6 +51,7 @@ public class BasicManagementNodeSyncRecord implements ManagementNodeSyncRecord,
         protected String nodeId;
         protected URI uri;
         protected ManagementNodeState status;
+        protected Long priority;
         protected long localTimestamp;
         protected Long remoteTimestamp;
 
@@ -68,6 +70,9 @@ public class BasicManagementNodeSyncRecord implements ManagementNodeSyncRecord,
         public Builder status(ManagementNodeState val) {
             status = val; return self();
         }
+        public Builder priority(Long val) {
+            priority = val; return self();
+        }
         public Builder localTimestamp(long val) {
             localTimestamp = val; return self();
         }
@@ -79,12 +84,13 @@ public class BasicManagementNodeSyncRecord implements ManagementNodeSyncRecord,
         }
         public Builder from(ManagementNodeSyncRecord other, boolean ignoreNulls) {
             if (ignoreNulls && other==null) return this;
-            if (other.getNodeId()!=null) nodeId = other.getNodeId();
             if (other.getBrooklynVersion()!=null) brooklynVersion = other.getBrooklynVersion();
-            if (other.getLocalTimestamp()>0) localTimestamp = other.getLocalTimestamp();
-            if (other.getRemoteTimestamp()!=null) remoteTimestamp = other.getRemoteTimestamp();
+            if (other.getNodeId()!=null) nodeId = other.getNodeId();
             if (other.getUri()!=null) uri = other.getUri();
             if (other.getStatus()!=null) status = other.getStatus();
+            if (other.getPriority()!=null) priority = other.getPriority();
+            if (other.getLocalTimestamp()>0) localTimestamp = other.getLocalTimestamp();
+            if (other.getRemoteTimestamp()!=null) remoteTimestamp = other.getRemoteTimestamp();
             return this;
         }
         public ManagementNodeSyncRecord build() {
@@ -96,6 +102,7 @@ public class BasicManagementNodeSyncRecord implements ManagementNodeSyncRecord,
     private String nodeId;
     private URI uri;
     private ManagementNodeState status;
+    private Long priority;
     private Long localTimestamp;
     private Long remoteTimestamp;
     
@@ -116,6 +123,7 @@ public class BasicManagementNodeSyncRecord implements ManagementNodeSyncRecord,
         nodeId = builder.nodeId;
         uri = builder.uri;
         status = builder.status;
+        priority = builder.priority;
         localTimestamp = builder.localTimestamp;
         remoteTimestamp = builder.remoteTimestamp;
     }
@@ -141,6 +149,11 @@ public class BasicManagementNodeSyncRecord implements ManagementNodeSyncRecord,
     }
     
     @Override
+    public Long getPriority() {
+        return priority;
+    }
+    
+    @Override
     public long getLocalTimestamp() {
         if (localTimestamp!=null) return localTimestamp;
         if (timestampUtc!=null) return timestampUtc;
@@ -167,6 +180,7 @@ public class BasicManagementNodeSyncRecord implements ManagementNodeSyncRecord,
                 .add("nodeId", getNodeId())
                 .add("uri", getUri())
                 .add("status", getStatus())
+                .add("priority", getPriority())
                 .add("localTimestamp", getLocalTimestamp()+"="+Time.makeDateString(getLocalTimestamp()))
                 .add("remoteTimestamp", getRemoteTimestamp()+(getRemoteTimestamp()==null ? "" : 
                     "="+Time.makeDateString(getRemoteTimestamp())))

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java b/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
index 78c110a..80f40d0 100644
--- a/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
+++ b/core/src/main/java/brooklyn/management/ha/BasicMasterChooser.java
@@ -27,10 +27,13 @@ import org.slf4j.LoggerFactory;
 
 import brooklyn.entity.trait.Identifiable;
 import brooklyn.util.collections.MutableList;
+import brooklyn.util.text.NaturalOrderComparator;
 import brooklyn.util.time.Duration;
 
 import com.google.common.annotations.Beta;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Ordering;
 
 /**
  * @since 0.7.0
@@ -92,19 +95,58 @@ public abstract class BasicMasterChooser implements MasterChooser {
         return min.record;
     }
 
+    public static class AlphabeticChooserScore implements Comparable<AlphabeticChooserScore> {
+        long priority;
+        int versionBias;
+        String brooklynVersion;
+        int statePriority;
+        String nodeId;
+        
+        @Override
+        public int compareTo(AlphabeticChooserScore o) {
+            if (o==null) return -1;
+            return ComparisonChain.start()
+                // invert the order where we prefer higher values
+                .compare(o.priority, this.priority)
+                .compare(o.versionBias, this.versionBias)
+                .compare(o.brooklynVersion, this.brooklynVersion, 
+                    Ordering.from(NaturalOrderComparator.INSTANCE).nullsFirst())
+                .compare(o.statePriority, this.statePriority)
+                .compare(this.nodeId, o.nodeId, Ordering.usingToString().nullsLast())
+                .result();
+        }
+    }
+    
+    /** comparator which prefers, in order:
+     * <li> higher explicit priority
+     * <li> non-snapshot Brooklyn version, then any Brooklyn version, and lastly null version 
+     *      (using {@link NaturalOrderComparator} so e.g. v10 > v3.20 > v3.9 )
+     * <li> higher version (null last)
+     * <li> node which reports it's master, hot standby, then standby 
+     * <li> finally (common case): lower (alphabetically) node id
+     */
     public static class AlphabeticMasterChooser extends BasicMasterChooser {
-        final boolean preferHot;
-        public AlphabeticMasterChooser(boolean preferHot) { this.preferHot = preferHot; }
-        public AlphabeticMasterChooser() { this.preferHot = true; }
+        final boolean preferHotStandby;
+        public AlphabeticMasterChooser(boolean preferHotStandby) { this.preferHotStandby = preferHotStandby; }
+        public AlphabeticMasterChooser() { this.preferHotStandby = true; }
         @Override
-        protected String score(ManagementNodeSyncRecord contender) {
-            if (!preferHot)
-                return contender.getNodeId();
-            // simple prefix with the rating
-            String state = (contender.getStatus()==ManagementNodeState.MASTER ? "1" :
-                contender.getStatus()==ManagementNodeState.HOT_STANDBY ? "2" :
-                contender.getStatus()==ManagementNodeState.STANDBY ? "3" : "9");
-            return state + ":" + contender.getNodeId();
+        protected AlphabeticChooserScore score(ManagementNodeSyncRecord contender) {
+            AlphabeticChooserScore score = new AlphabeticChooserScore();
+            score.priority = contender.getPriority()!=null ? contender.getPriority() : 0;
+            score.brooklynVersion = contender.getBrooklynVersion();
+            score.versionBias = contender.getBrooklynVersion()==null ? -2 :
+                contender.getBrooklynVersion().toLowerCase().indexOf("snapshot")>=0 ? -1 :
+                0;
+            if (preferHotStandby) {
+                // other master should be preferred before we get invoked, but including for good measure
+                score.statePriority = contender.getStatus()==ManagementNodeState.MASTER ? 3 :
+                    contender.getStatus()==ManagementNodeState.HOT_STANDBY ? 2 :
+                        contender.getStatus()==ManagementNodeState.STANDBY ? 1 : -1;
+            } else {
+                score.statePriority = 0;
+            }
+            score.nodeId = contender.getNodeId();
+            return score;
         }
     }
     

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/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 bdd6eba..200a902 100644
--- a/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/HighAvailabilityManagerImpl.java
@@ -50,6 +50,7 @@ import brooklyn.util.collections.MutableSet;
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.task.BasicTask;
 import brooklyn.util.task.ScheduledTask;
+import brooklyn.util.text.Strings;
 import brooklyn.util.time.Duration;
 
 import com.google.api.client.repackaged.com.google.common.base.Preconditions;
@@ -128,6 +129,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
     private volatile boolean running;
     private volatile ManagementNodeState nodeState = ManagementNodeState.INITIALIZING;
     private volatile boolean nodeStateTransitionComplete = false;
+    private volatile long priority = 0;
 
     public HighAvailabilityManagerImpl(ManagementContextInternal managementContext) {
         this.managementContext = managementContext;
@@ -204,54 +206,112 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
 
     @Override
     public void start(HighAvailabilityMode startMode) {
-        ownNodeId = managementContext.getManagementNodeId();
         nodeStateTransitionComplete = true;
         // always start in standby; it may get promoted to master or hot_standby in this method
+        // (depending on startMode; but for startMode STANDBY or HOT_STANDBY it will not promote until the next election)
         nodeState = ManagementNodeState.STANDBY;
         running = true;
+        changeMode(startMode, true, true);
+    }
+    
+    @Override
+    public void changeMode(HighAvailabilityMode startMode) {
+        changeMode(startMode, false, false);
+    }
+    
+    @VisibleForTesting
+    @Beta
+    public void changeMode(HighAvailabilityMode startMode, boolean preventElectionOnExplicitStandbyMode, boolean failOnExplicitStandbyModeIfNoMaster) {
+        if (!running)
+            throw new IllegalStateException("Can only change mode when already running; invoke 'start' first");
         
+        ownNodeId = managementContext.getManagementNodeId();
         // TODO Small race in that we first check, and then we'll do checkMaster() on first poll,
         // so another node could have already become master or terminated in that window.
         ManagementNodeSyncRecord existingMaster = hasHealthyMaster();
+        boolean weAreMaster = existingMaster!=null && ownNodeId.equals(existingMaster.getNodeId());
         
         // catch error in some tests where mgmt context has a different mgmt context
         if (managementContext.getHighAvailabilityManager()!=this)
             throw new IllegalStateException("Cannot start an HA manager on a management context with a different HA manager!");
         
+        if (weAreMaster) {
+            // demotion may be required; do this before triggering an election
+            switch (startMode) {
+            case MASTER:
+            case AUTO:
+                // no action needed
+                break;
+            case HOT_STANDBY: demoteToStandby(true); break;
+            case STANDBY: demoteToStandby(false); break;
+            case DISABLED: demoteToFailed(); break;
+            default:
+                throw new IllegalStateException("Unexpected high availability mode "+startMode+" requested for "+this);
+            }
+        }
+        
+        // now do election
         switch (startMode) {
         case AUTO:
             // don't care; let's start and see if we promote ourselves
             publishAndCheck(true);
             if (nodeState == ManagementNodeState.STANDBY || nodeState == ManagementNodeState.HOT_STANDBY) {
-                String masterNodeId = getManagementPlaneSyncState().getMasterNodeId();
-                ManagementNodeSyncRecord masterNodeDetails = getManagementPlaneSyncState().getManagementNodes().get(masterNodeId);
-                LOG.info("Management node "+ownNodeId+" started as HA " + nodeState + " autodetected, master is "+masterNodeId +
-                    (masterNodeDetails==null || masterNodeDetails.getUri()==null ? " (no url)" : " at "+masterNodeDetails.getUri()));
+                ManagementPlaneSyncRecord newState = getManagementPlaneSyncState();
+                String masterNodeId = newState.getMasterNodeId();
+                ManagementNodeSyncRecord masterNodeDetails = newState.getManagementNodes().get(masterNodeId);
+                LOG.info("Management node "+ownNodeId+" running as HA " + nodeState + " autodetected, " +
+                    (Strings.isBlank(masterNodeId) ? "no master currently (other node should promote itself soon)" : "master "
+                        + (existingMaster==null ? "(new) " : "")
+                        + "is "+masterNodeId +
+                        (masterNodeDetails==null || masterNodeDetails.getUri()==null ? " (no url)" : " at "+masterNodeDetails.getUri())));
             } else if (nodeState == ManagementNodeState.MASTER) {
-                LOG.info("Management node "+ownNodeId+" starting as HA MASTER autodetected");
+                LOG.info("Management node "+ownNodeId+" running as HA MASTER autodetected");
             } else {
-                throw new IllegalStateException("Management node "+ownNodeId+" started as auto in unexpected mode "+nodeState);
+                throw new IllegalStateException("Management node "+ownNodeId+" set to HA AUTO, encountered unexpected mode "+nodeState);
             }
             break;
         case MASTER:
             if (existingMaster == null) {
                 promoteToMaster();
-                LOG.info("Management node "+ownNodeId+" started as HA MASTER explicitly");
+                LOG.info("Management node "+ownNodeId+" running as HA MASTER explicitly");
+            } else if (!weAreMaster) {
+                throw new IllegalStateException("Master already exists; cannot run as master (master "+existingMaster.toVerboseString()+"); "
+                    + "to trigger a promotion, set a priority and demote the current master");
             } else {
-                throw new IllegalStateException("Master already exists; cannot start as master ("+existingMaster.toVerboseString()+")");
+                LOG.info("Management node "+ownNodeId+" already running as HA MASTER, when set explicitly");
             }
             break;
         case STANDBY:
         case HOT_STANDBY:
-            if (existingMaster != null) {
+            if (!preventElectionOnExplicitStandbyMode)
                 publishAndCheck(true);
-                LOG.info("Management node "+ownNodeId+" started as "+startMode+" explicitly");
+            if (failOnExplicitStandbyModeIfNoMaster && existingMaster==null) {
+                LOG.error("Management node "+ownNodeId+" detected no master when "+startMode+" requested and existing master required; failing.");
+                throw new IllegalStateException("No existing master; cannot start as "+startMode);
+            }
+            
+            String message = "Management node "+ownNodeId+" running as HA "+getNodeState()+" (";
+            if (getNodeState().toString().equals(startMode.toString()))
+                message += "explicitly requested";
+            else if (startMode==HighAvailabilityMode.HOT_STANDBY && getNodeState()==ManagementNodeState.STANDBY)
+                message += "caller requested "+startMode+", will attempt rebind directly";
+            else
+                message += "caller requested "+startMode;
+            
+            if (getNodeState()==ManagementNodeState.MASTER) {
+                message += " but election re-promoted this node)";
             } else {
-                throw new IllegalStateException("No existing master; cannot start as standby");
+                ManagementPlaneSyncRecord newState = getManagementPlaneSyncState();
+                if (Strings.isBlank(newState.getMasterNodeId())) {
+                    message += "); no master currently (subsequent election may repair)";
+                } else {
+                    message += "); master "+newState.getMasterNodeId();
+                }
             }
+            LOG.info(message);
             break;
         default:
-            throw new IllegalStateException("Unexpected high availability start-mode "+startMode+" for "+this);
+            throw new IllegalStateException("Unexpected high availability mode "+startMode+" requested for "+this);
         }
         
         if (startMode==HighAvailabilityMode.AUTO) {
@@ -266,15 +326,40 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
             nodeStateTransitionComplete = false;
             // inform the world that we are transitioning (not eligible for promotion while going in to hot standby)
             publishHealth();
-            attemptHotStandby();
-            publishHealth();
+            try {
+                attemptHotStandby();
+                nodeStateTransitionComplete = true;
+                publishHealth();
+                
+                if (getNodeState()==ManagementNodeState.HOT_STANDBY) {
+                    LOG.info("Management node "+ownNodeId+" now running as HA "+ManagementNodeState.HOT_STANDBY+"; "
+                        + managementContext.getApplications().size()+" application"+Strings.s(managementContext.getApplications().size())+" loaded");
+                } else {
+                    LOG.warn("Management node "+ownNodeId+" unable to promote to "+ManagementNodeState.HOT_STANDBY+" (currently "+getNodeState()+"); "
+                        + "(see log for further details)");
+                }
+            } catch (Exception e) {
+                LOG.warn("Management node "+ownNodeId+" unable to promote to "+ManagementNodeState.HOT_STANDBY+" (currently "+getNodeState()+"); rethrowing: "+Exceptions.collapseText(e));
+                throw Exceptions.propagate(e);
+            }
+        } else {
+            nodeStateTransitionComplete = true;
         }
-        
-        nodeStateTransitionComplete = true;
         registerPollTask();
     }
 
     @Override
+    public void setPriority(long priority) {
+        this.priority = priority;
+        if (persister!=null) publishHealth();
+    }
+    
+    @Override
+    public long getPriority() {
+        return priority;
+    }
+    
+    @Override
     public void stop() {
         LOG.debug("Stopping "+this);
         boolean wasRunning = running; // ensure idempotent
@@ -373,7 +458,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         if (LOG.isTraceEnabled()) LOG.trace("Published management-node health: {}", memento);
     }
     
-    protected synchronized void publishDemotionFromMaster(boolean clearMaster) {
+    protected synchronized void publishDemotionFromMaster() {
         checkState(getNodeState() != ManagementNodeState.MASTER, "node status must not be master when demoting", getNodeState());
         
         if (persister == null) {
@@ -384,7 +469,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         ManagementNodeSyncRecord memento = createManagementNodeSyncRecord(false);
         ManagementPlaneSyncRecordDeltaImpl.Builder deltaBuilder = ManagementPlaneSyncRecordDeltaImpl.builder()
                 .node(memento);
-        if (clearMaster) deltaBuilder.clearMaster(ownNodeId);
+        deltaBuilder.clearMaster(ownNodeId);
         
         Delta delta = deltaBuilder.build();
         persister.delta(delta);
@@ -466,7 +551,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
                 return;
             } else {
                 if (ownNodeRecord!=null && ownNodeRecord.getStatus() == ManagementNodeState.MASTER) {
-                    LOG.error("HA subsystem detected change of master, stolen from us ("+ownNodeId+"), deferring to "+currMasterNodeId);
+                    LOG.error("Management node "+ownNodeId+" detected master change, stolen from us, deferring to "+currMasterNodeId);
                     newMasterNodeRecord = currMasterNodeRecord;
                     demotingSelfInFavourOfOtherMaster = true;
                 } else {
@@ -494,7 +579,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         
         if (demotingSelfInFavourOfOtherMaster) {
             LOG.debug("Master-change for this node only, demoting "+ownNodeRecord.toVerboseString()+" in favour of official master "+newMasterNodeRecord.toVerboseString());
-            demoteToStandby(true);
+            demoteToStandby(BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY));
             return;
         }
         
@@ -516,13 +601,16 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
                 });
         }
         if (!initializing) {
-            LOG.warn("HA subsystem detected change of master, from " 
-                + currMasterNodeId + " (" + (currMasterNodeRecord==null ? "?" : currMasterNodeRecord.getRemoteTimestamp()) + ")"
+            String message = "Management node "+ownNodeId+" detected ";
+            if (weAreNewMaster) message += "we should be master, changing from ";
+            else message += "master change, from ";
+            message +=currMasterNodeId + " (" + (currMasterNodeRecord==null ? "?" : currMasterNodeRecord.getRemoteTimestamp()) + ")"
                 + " to "
                 + (newMasterNodeId == null ? "<none>" :
                     (weAreNewMaster ? "us " : "")
                     + newMasterNodeId + " (" + newMasterNodeRecord.getRemoteTimestamp() + ")" 
-                    + (newMasterNodeUri!=null ? " "+newMasterNodeUri : "")  ));
+                    + (newMasterNodeUri!=null ? " "+newMasterNodeUri : "")  );
+            LOG.warn(message);
         }
 
         // New master is ourself: promote
@@ -567,7 +655,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         nodeState = ManagementNodeState.FAILED;
         onDemotionStopTasks();
         nodeStateTransitionComplete = true;
-        publishDemotionFromMaster(true);
+        publishDemotionFromMaster();
     }
 
     protected void demoteToStandby(boolean hot) {
@@ -579,12 +667,15 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
         nodeStateTransitionComplete = false;
         nodeState = ManagementNodeState.STANDBY;
         onDemotionStopTasks();
+        nodeStateTransitionComplete = true;
+        publishDemotionFromMaster();
+        
         if (hot) {
-            publishDemotionFromMaster(false);
+            nodeStateTransitionComplete = false;
             attemptHotStandby();
+            nodeStateTransitionComplete = true;
+            publishHealth();
         }
-        nodeStateTransitionComplete = true;
-        publishDemotionFromMaster(false);
     }
     
     protected void onDemotionStopTasks() {
@@ -703,6 +794,7 @@ public class HighAvailabilityManagerImpl implements HighAvailabilityManager {
                 .brooklynVersion(BrooklynVersion.get())
                 .nodeId(ownNodeId)
                 .status(getNodeState())
+                .priority(getPriority())
                 .localTimestamp(timestamp)
                 .uri(managementContext.getManagementNodeUri().orNull());
         if (useLocalTimestampAsRemoteTimestamp)

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordDeltaImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordDeltaImpl.java b/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordDeltaImpl.java
index a9be0fd..b54b9ef 100644
--- a/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordDeltaImpl.java
+++ b/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordDeltaImpl.java
@@ -45,6 +45,7 @@ public class ManagementPlaneSyncRecordDeltaImpl implements Delta {
         private Collection <String> removedNodeIds = Sets.newLinkedHashSet();
         private MasterChange masterChange = MasterChange.NO_CHANGE;
         private String master;
+        private String expectedOldMaster;
         
         public Builder node(ManagementNodeSyncRecord node) {
             nodes.add(checkNotNull(node, "node")); return this;
@@ -57,8 +58,9 @@ public class ManagementPlaneSyncRecordDeltaImpl implements Delta {
             master = checkNotNull(nodeId, "masterId");
             return this;
         }
-        public Builder clearMaster(String nodeId) {
+        public Builder clearMaster(String optionalExpectedNodeId) {
             masterChange = MasterChange.CLEAR_MASTER;
+            this.expectedOldMaster = optionalExpectedNodeId;
             return this;
         }
         public Delta build() {
@@ -70,12 +72,14 @@ public class ManagementPlaneSyncRecordDeltaImpl implements Delta {
     private final Collection <String> removedNodeIds;
     private final MasterChange masterChange;
     private String masterId;
+    private String expectedOldMaster;
     
     ManagementPlaneSyncRecordDeltaImpl(Builder builder) {
         nodes = builder.nodes;
         removedNodeIds = builder.removedNodeIds;
         masterChange = builder.masterChange;
         masterId = builder.master;
+        this.expectedOldMaster = builder.expectedOldMaster;
         checkState((masterChange == MasterChange.SET_MASTER) ? (masterId != null) : (masterId == null), 
                 "invalid combination: change=%s; masterId=%s", masterChange, masterId);
     }
@@ -99,4 +103,9 @@ public class ManagementPlaneSyncRecordDeltaImpl implements Delta {
     public String getNewMasterOrNull() {
         return masterId;
     }
+    
+    @Override
+    public String getExpectedMasterToClear() {
+        return expectedOldMaster;
+    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/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 3a74e9a..a00f197 100644
--- a/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersisterToObjectStore.java
+++ b/core/src/main/java/brooklyn/management/ha/ManagementPlaneSyncRecordPersisterToObjectStore.java
@@ -248,17 +248,26 @@ public class ManagementPlaneSyncRecordPersisterToObjectStore implements Manageme
         case NO_CHANGE:
             break; // no-op
         case SET_MASTER:
-            persistMaster(checkNotNull(delta.getNewMasterOrNull()));
+            persistMaster(checkNotNull(delta.getNewMasterOrNull()), null);
             break;
         case CLEAR_MASTER:
-            persistMaster("");
+            persistMaster("", delta.getExpectedMasterToClear());
             break; // no-op
         default:
             throw new IllegalStateException("Unknown state for master-change: "+delta.getMasterChange());
         }
     }
 
-    private void persistMaster(String nodeId) {
+    private void persistMaster(String nodeId, String optionalExpectedId) {
+        if (optionalExpectedId!=null) {
+            String currentRemoteMaster = masterWriter.get();
+            if (currentRemoteMaster==null) {
+                // nothing at remote is okay
+            } else if (!currentRemoteMaster.trim().equals(optionalExpectedId.trim())) {
+                LOG.warn("Master at server is "+currentRemoteMaster+"; expected "+optionalExpectedId+" in order to set as "+nodeId+", so not applying (yet)");
+                return;
+            }
+        }
         masterWriter.put(nodeId);
         try {
             masterWriter.waitForCurrentWrites(SYNC_WRITE_TIMEOUT);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
index afe42df..46e3f97 100644
--- a/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
+++ b/core/src/main/java/brooklyn/management/internal/NonDeploymentManagementContext.java
@@ -20,7 +20,6 @@ package brooklyn.management.internal;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
-import java.io.IOException;
 import java.net.URI;
 import java.net.URL;
 import java.util.Collection;
@@ -563,6 +562,18 @@ public class NonDeploymentManagementContext implements ManagementContextInternal
         public ManagementPlaneSyncRecord getManagementPlaneSyncState() {
             throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
         }
+        @Override
+        public void changeMode(HighAvailabilityMode startMode) {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
+        @Override
+        public void setPriority(long priority) {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
+        @Override
+        public long getPriority() {
+            throw new IllegalStateException("Non-deployment context "+NonDeploymentManagementContext.this+" is not valid for this operation.");
+        }
     }
     
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/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 da1fd0a..a0b7feb 100644
--- a/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HighAvailabilityManagerSplitBrainTest.java
@@ -41,6 +41,7 @@ import brooklyn.entity.rebind.persister.InMemoryObjectStore;
 import brooklyn.entity.rebind.persister.ListeningObjectStore;
 import brooklyn.entity.rebind.persister.PersistMode;
 import brooklyn.entity.rebind.persister.PersistenceObjectStore;
+import brooklyn.internal.BrooklynFeatureEnablement;
 import brooklyn.location.Location;
 import brooklyn.management.internal.ManagementContextInternal;
 import brooklyn.test.Asserts;
@@ -256,8 +257,11 @@ public class HighAvailabilityManagerSplitBrainTest {
         ManagementPlaneSyncRecord memento1b = n1.ha.getManagementPlaneSyncState();
         log.info(n1+" HA now: "+memento1b);
         
+        ManagementNodeState expectedStateAfterDemotion = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_DEFAULT_STANDBY_IS_HOT_PROPERTY) ?
+            ManagementNodeState.HOT_STANDBY : ManagementNodeState.STANDBY;
+        
         // n1 comes back and demotes himself 
-        assertEquals(memento1b.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.HOT_STANDBY);
+        assertEquals(memento1b.getManagementNodes().get(n1.ownNodeId).getStatus(), expectedStateAfterDemotion);
         assertEquals(memento1b.getManagementNodes().get(n2.ownNodeId).getStatus(), ManagementNodeState.MASTER);
         assertEquals(memento1b.getMasterNodeId(), n2.ownNodeId);
         assertEquals(memento1b.getManagementNodes().get(n1.ownNodeId).getRemoteTimestamp(), time2);
@@ -266,7 +270,7 @@ public class HighAvailabilityManagerSplitBrainTest {
         // n2 now sees itself as master, with n1 in standby again
         ManagementPlaneSyncRecord memento2c = n2.ha.getManagementPlaneSyncState();
         log.info(n2+" HA now: "+memento2c);
-        assertEquals(memento2c.getManagementNodes().get(n1.ownNodeId).getStatus(), ManagementNodeState.HOT_STANDBY);
+        assertEquals(memento2c.getManagementNodes().get(n1.ownNodeId).getStatus(), expectedStateAfterDemotion);
         assertEquals(memento2c.getManagementNodes().get(n2.ownNodeId).getStatus(), ManagementNodeState.MASTER);
         assertEquals(memento2c.getMasterNodeId(), n2.ownNodeId);
         assertEquals(memento2c.getManagementNodes().get(n1.ownNodeId).getRemoteTimestamp(), time2);

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/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 492f2f1..1a41c19 100644
--- a/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
+++ b/core/src/test/java/brooklyn/management/ha/HotStandbyTest.java
@@ -114,6 +114,10 @@ public class HotStandbyTest {
         public String toString() {
             return nodeName+" "+ownNodeId;
         }
+
+        public RebindManagerImpl rebinder() {
+            return (RebindManagerImpl)mgmt.getRebindManager();
+        }
     }
     
     @BeforeMethod(alwaysRun=true)
@@ -443,6 +447,8 @@ public class HotStandbyTest {
     }
 
     @Test(groups="Integration") // because it's slow
+    // Sept 2014 - there is a small leak, of 200 bytes per child created and destroyed;
+    // but this goes away when the app is destroyed; it may be a benign record
     public void testHotStandbyDoesNotLeakLotsOfRebindsCreatingAndDestroyingAChildEntity() throws Exception {
         log.info("Starting test "+JavaClassNames.niceClassAndMethod());
         final int DELTA = 2;
@@ -481,4 +487,76 @@ public class HotStandbyTest {
         
         assertUsedMemoryLessThan("And now all unmanaged", initialUsed + DELTA*1000*1000);
     }
+    
+    protected void assertHotStandby(HaMgmtNode n1) {
+        assertEquals(n1.ha.getNodeState(), ManagementNodeState.HOT_STANDBY);
+        Assert.assertTrue(n1.rebinder().isReadOnlyRunning());
+        Assert.assertFalse(n1.rebinder().isPersistenceRunning());
+    }
+
+    protected void assertMaster(HaMgmtNode n1) {
+        assertEquals(n1.ha.getNodeState(), ManagementNodeState.MASTER);
+        Assert.assertFalse(n1.rebinder().isReadOnlyRunning());
+        Assert.assertTrue(n1.rebinder().isPersistenceRunning());
+    }
+
+    @Test
+    public void testChangeMode() throws Exception {
+        HaMgmtNode n1 = createMaster(Duration.PRACTICALLY_FOREVER);
+        TestApplication app = createFirstAppAndPersist(n1);
+        HaMgmtNode n2 = createHotStandby(Duration.PRACTICALLY_FOREVER);
+
+        TestEntity child = app.addChild(EntitySpec.create(TestEntity.class).configure(TestEntity.CONF_NAME, "first-child"));
+        Entities.manage(child);
+        TestApplication app2 = TestApplication.Factory.newManagedInstanceForTests(n1.mgmt);
+        app2.setConfig(TestEntity.CONF_NAME, "second-app");
+
+        forcePersistNow(n1);
+        n2.ha.setPriority(1);
+        n1.ha.changeMode(HighAvailabilityMode.HOT_STANDBY);
+        
+        // both now hot standby
+        assertHotStandby(n1);
+        assertHotStandby(n2);
+        
+        assertEquals(n1.mgmt.getApplications().size(), 2);
+        Application app2RO = n1.mgmt.lookup(app2.getId(), Application.class);
+        Assert.assertNotNull(app2RO);
+        assertEquals(app2RO.getConfig(TestEntity.CONF_NAME), "second-app");
+        try {
+            ((TestApplication)app2RO).setAttribute(TestEntity.SEQUENCE, 4);
+            Assert.fail("Should not have allowed sensor to be set");
+        } catch (Exception e) {
+            Assert.assertTrue(e.toString().toLowerCase().contains("read-only"), "Error message did not contain expected text: "+e);
+        }
+
+        n1.ha.changeMode(HighAvailabilityMode.AUTO);
+        n2.ha.changeMode(HighAvailabilityMode.HOT_STANDBY, true, false);
+        // both still hot standby (n1 will defer to n2 as it has higher priority)
+        assertHotStandby(n1);
+        assertHotStandby(n2);
+        
+        // with priority 1, n2 will now be elected
+        n2.ha.changeMode(HighAvailabilityMode.AUTO);
+        assertHotStandby(n1);
+        assertMaster(n2);
+        
+        assertEquals(n2.mgmt.getApplications().size(), 2);
+        Application app2B = n2.mgmt.lookup(app2.getId(), Application.class);
+        Assert.assertNotNull(app2B);
+        assertEquals(app2B.getConfig(TestEntity.CONF_NAME), "second-app");
+        ((TestApplication)app2B).setAttribute(TestEntity.SEQUENCE, 4);
+        
+        forcePersistNow(n2);
+        forceRebindNow(n1);
+        Application app2BRO = n1.mgmt.lookup(app2.getId(), Application.class);
+        Assert.assertNotNull(app2BRO);
+        EntityTestUtils.assertAttributeEquals(app2BRO, TestEntity.SEQUENCE, 4);
+    }
+
+    @Test(groups="Integration", invocationCount=20)
+    public void testChangeModeManyTimes() throws Exception {
+        testChangeMode();
+    }
+    
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java b/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
index 6f8ba48..4373ea0 100644
--- a/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
+++ b/core/src/test/java/brooklyn/management/ha/MasterChooserTest.java
@@ -106,11 +106,39 @@ public class MasterChooserTest {
         memento.addNode(newManagerMemento("node5", ManagementNodeState.MASTER, now));
         if (includeHot)
             memento.addNode(newManagerMemento("node6",  ManagementNodeState.HOT_STANDBY, now));
-        Duration heartbeatTimeout = Duration.THIRTY_SECONDS;
-        return getIds(chooser.sort(chooser.filterHealthy(memento, heartbeatTimeout, now)));
+        return getIds(chooser.sort(chooser.filterHealthy(memento, Duration.THIRTY_SECONDS, now)));
     }
-    
+
+    @Test
+    public void testExplicityPriority() throws Exception {
+        chooser = new AlphabeticMasterChooser();
+        memento.addNode(newManagerMemento("node1", ManagementNodeState.STANDBY, now, BrooklynVersion.get(), 2L));
+        memento.addNode(newManagerMemento("node2", ManagementNodeState.STANDBY, now, BrooklynVersion.get(), -1L));
+        memento.addNode(newManagerMemento("node3", ManagementNodeState.STANDBY, now, BrooklynVersion.get(), null));
+        List<String> order = getIds(chooser.sort(chooser.filterHealthy(memento, Duration.THIRTY_SECONDS, now)));
+        assertEquals(order, ImmutableList.of("node1", "node3", "node2"));
+    }
+
+    @Test
+    public void testVersionsMaybeNull() throws Exception {
+        chooser = new AlphabeticMasterChooser();
+        memento.addNode(newManagerMemento("node1", ManagementNodeState.STANDBY, now, "v10", null));
+        memento.addNode(newManagerMemento("node2", ManagementNodeState.STANDBY, now, "v3-snapshot", null));
+        memento.addNode(newManagerMemento("node3", ManagementNodeState.STANDBY, now, "v3-snapshot", -1L));
+        memento.addNode(newManagerMemento("node4", ManagementNodeState.STANDBY, now, "v3-snapshot", null));
+        memento.addNode(newManagerMemento("node5", ManagementNodeState.STANDBY, now, "v3-stable", null));
+        memento.addNode(newManagerMemento("node6", ManagementNodeState.STANDBY, now, "v1", null));
+        memento.addNode(newManagerMemento("node7", ManagementNodeState.STANDBY, now, null, null));
+        List<String> order = getIds(chooser.sort(chooser.filterHealthy(memento, Duration.THIRTY_SECONDS, now)));
+        assertEquals(order, ImmutableList.of("node1", "node5", "node6", "node2", "node4", "node7", "node3"));
+    }
+
     private ManagementNodeSyncRecord newManagerMemento(String nodeId, ManagementNodeState status, long timestamp) {
-        return BasicManagementNodeSyncRecord.builder().brooklynVersion(BrooklynVersion.get()).nodeId(nodeId).status(status).localTimestamp(timestamp).remoteTimestamp(timestamp).build();
+        return newManagerMemento(nodeId, status, timestamp, BrooklynVersion.get(), null);
+    }
+    private ManagementNodeSyncRecord newManagerMemento(String nodeId, ManagementNodeState status, long timestamp,
+            String version, Long priority) {
+        return BasicManagementNodeSyncRecord.builder().brooklynVersion(version).nodeId(nodeId).status(status).localTimestamp(timestamp).remoteTimestamp(timestamp).
+            priority(priority).build();
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherRebindToCloudObjectStoreTest.java
----------------------------------------------------------------------
diff --git a/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherRebindToCloudObjectStoreTest.java b/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherRebindToCloudObjectStoreTest.java
index 6d9a71a..e4462e3 100644
--- a/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherRebindToCloudObjectStoreTest.java
+++ b/usage/launcher/src/test/java/brooklyn/launcher/BrooklynLauncherRebindToCloudObjectStoreTest.java
@@ -20,13 +20,9 @@ package brooklyn.launcher;
 
 import static org.testng.Assert.assertEquals;
 
-import java.io.File;
-
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
-import com.google.common.io.Files;
-
 import brooklyn.config.BrooklynProperties;
 import brooklyn.config.BrooklynServerConfig;
 import brooklyn.entity.proxying.EntitySpec;

http://git-wip-us.apache.org/repos/asf/incubator-brooklyn/blob/32c926b9/utils/common/src/main/java/brooklyn/util/text/NaturalOrderComparator.java
----------------------------------------------------------------------
diff --git a/utils/common/src/main/java/brooklyn/util/text/NaturalOrderComparator.java b/utils/common/src/main/java/brooklyn/util/text/NaturalOrderComparator.java
index 5826a73..34d96e3 100644
--- a/utils/common/src/main/java/brooklyn/util/text/NaturalOrderComparator.java
+++ b/utils/common/src/main/java/brooklyn/util/text/NaturalOrderComparator.java
@@ -54,7 +54,8 @@ import java.util.Comparator;
  * e.g. "10">"9", including when those numbers occur in the midst of equal text; e.g. "a10" > "a9";
  * but not if the text differs; e.g. "a10" < "b9"
  * <p>
- * class is thread-safe  
+ * class is thread-safe. nulls not supported. (to support nulls, wrap in guava:
+ * <code>Ordering.from(NaturalOrderComparator.INSTANCE).nullsFirst()</code>)
  */
 public class NaturalOrderComparator implements Comparator<String> {