Merge pull request #1417 from aledsage/feature/policy-enricher-mement-incorporate-comments

policy enricher memento: incorporate comments
diff --git a/core/src/main/java/brooklyn/entity/group/AbstractMembershipTrackingPolicy.java b/core/src/main/java/brooklyn/entity/group/AbstractMembershipTrackingPolicy.java
index 45cb8b6..049d49c 100644
--- a/core/src/main/java/brooklyn/entity/group/AbstractMembershipTrackingPolicy.java
+++ b/core/src/main/java/brooklyn/entity/group/AbstractMembershipTrackingPolicy.java
@@ -1,25 +1,30 @@
 package brooklyn.entity.group;
 
-import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import brooklyn.config.ConfigKey;
 import brooklyn.entity.Entity;
 import brooklyn.entity.Group;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.ConfigKeys;
 import brooklyn.entity.basic.DynamicGroup;
 import brooklyn.entity.trait.Startable;
 import brooklyn.event.Sensor;
 import brooklyn.event.SensorEvent;
 import brooklyn.event.SensorEventListener;
 import brooklyn.policy.basic.AbstractPolicy;
-import brooklyn.util.flags.SetFromFlag;
+import brooklyn.util.collections.MutableMap;
 
+import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
-import com.google.common.collect.Sets;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.reflect.TypeToken;
 
 /** abstract class which helps track membership of a group, invoking (empty) methods in this class on MEMBER{ADDED,REMOVED} events, as well as SERVICE_UP {true,false} for those members. */
 public abstract class AbstractMembershipTrackingPolicy extends AbstractPolicy {
@@ -27,18 +32,34 @@
     
     private Group group;
     
-    @SetFromFlag
-    private Set<Sensor<?>> sensorsToTrack;
+    private ConcurrentMap<String,Map<Sensor<Object>, Object>> entitySensorCache = new ConcurrentHashMap<String, Map<Sensor<Object>, Object>>();
+    
+    @SuppressWarnings("serial")
+    public static final ConfigKey<Set<Sensor<?>>> SENSORS_TO_TRACK = ConfigKeys.newConfigKey(
+            new TypeToken<Set<Sensor<?>>>() {},
+            "sensorsToTrack",
+            "Sensors of members to be monitored (implicitly adds service-up to this list, but that behaviour may be deleted in a subsequent release!)",
+            ImmutableSet.<Sensor<?>>of());
+
+    public static final ConfigKey<Boolean> NOTIFY_ON_DUPLICATES = ConfigKeys.newBooleanConfigKey("notifyOnDuplicates",
+            "Whether to notify listeners when a sensor is published with the same value as last time",
+            true);
     
     public AbstractMembershipTrackingPolicy(Map<?,?> flags) {
         super(flags);
-        if (sensorsToTrack == null)  sensorsToTrack = Sets.newLinkedHashSet();
     }
     
     public AbstractMembershipTrackingPolicy() {
-        this(Collections.emptyMap());
+        super();
     }
 
+    protected Set<Sensor<?>> getSensorsToTrack() {
+        return ImmutableSet.<Sensor<?>>builder()
+                .addAll(getRequiredConfig(SENSORS_TO_TRACK))
+                .add(Attributes.SERVICE_UP)
+                .build();
+    }
+    
     /**
      * Sets the group to be tracked; unsubscribes from any previous group, and subscribes to this group.
      * 
@@ -73,15 +94,11 @@
             subscribeToGroup();
         }
     }
-
-    // TODO having "subscribe to changes only" semantics as part of subscription would be much cleaner
-    // than this lightweight map
-    Map<String,Boolean> lastKnownServiceUpCache = new ConcurrentHashMap<String, Boolean>();
     
     protected void subscribeToGroup() {
         Preconditions.checkNotNull(group, "The group cannot be null");
 
-        LOG.debug("Subscribing to group "+group+", for memberAdded, memberRemoved, serviceUp, and {}", sensorsToTrack);
+        LOG.debug("Subscribing to group "+group+", for memberAdded, memberRemoved, serviceUp, and {}", getSensorsToTrack());
         
         subscribe(group, DynamicGroup.MEMBER_ADDED, new SensorEventListener<Entity>() {
             @Override public void onEvent(SensorEvent<Entity> event) {
@@ -90,21 +107,37 @@
         });
         subscribe(group, DynamicGroup.MEMBER_REMOVED, new SensorEventListener<Entity>() {
             @Override public void onEvent(SensorEvent<Entity> event) {
-                lastKnownServiceUpCache.remove(event.getSource());
+                entitySensorCache.remove(event.getSource().getId());
                 onEntityEvent(EventType.ENTITY_REMOVED, event.getValue());
             }
         });
-        subscribeToMembers(group, Startable.SERVICE_UP, new SensorEventListener<Boolean>() {
-            @Override public void onEvent(SensorEvent<Boolean> event) {
-                if (event.getValue() == lastKnownServiceUpCache.put(event.getSource().getId(), event.getValue()))
-                    // ignore if value has not changed
-                    return;
-                onEntityEvent(EventType.ENTITY_CHANGE, event.getSource());
-            }
-        });
-        for (Sensor<?> sensor : sensorsToTrack) {
+
+        for (Sensor<?> sensor : getSensorsToTrack()) {
             subscribeToMembers(group, sensor, new SensorEventListener<Object>() {
+                boolean hasWarnedOfServiceUp = false;
+                
                 @Override public void onEvent(SensorEvent<Object> event) {
+                    boolean notifyOnDuplicates = getRequiredConfig(NOTIFY_ON_DUPLICATES);
+                    if (Startable.SERVICE_UP.equals(event.getSensor()) && notifyOnDuplicates && !hasWarnedOfServiceUp) {
+                        LOG.warn("Deprecated behaviour: not notifying of duplicate value for service-up in {}, group {}", AbstractMembershipTrackingPolicy.this, group);
+                        hasWarnedOfServiceUp = true;
+                        notifyOnDuplicates = false;
+                    }
+                    
+                    String entityId = event.getSource().getId();
+
+                    Map<Sensor<Object>, Object> newMap = MutableMap.<Sensor<Object>, Object>of();
+                    // NOTE: putIfAbsent returns null if the key is not present, or the *previous* value if present
+                    Map<Sensor<Object>, Object> sensorCache = entitySensorCache.putIfAbsent(entityId, newMap);
+                    if (sensorCache == null) {
+                        sensorCache = newMap;
+                    }
+                    
+                    if (!notifyOnDuplicates && Objects.equal(event.getValue(), sensorCache.put(event.getSensor(), event.getValue()))) {
+                        // ignore if value has not changed
+                        return;
+                    }
+
                     onEntityEvent(EventType.ENTITY_CHANGE, event.getSource());
                 }
             });
diff --git a/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.groovy b/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.groovy
deleted file mode 100644
index 636abbc..0000000
--- a/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.groovy
+++ /dev/null
@@ -1,726 +0,0 @@
-package brooklyn.entity.group
-
-import brooklyn.entity.basic.Attributes
-import brooklyn.entity.basic.Lifecycle
-
-import static org.testng.Assert.*
-
-import java.util.concurrent.CopyOnWriteArrayList
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.ExecutionException
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicInteger
-
-import org.testng.annotations.AfterMethod
-import org.testng.annotations.BeforeMethod
-import org.testng.annotations.Test
-
-import brooklyn.entity.Application
-import brooklyn.entity.Entity
-import brooklyn.entity.basic.ApplicationBuilder
-import brooklyn.entity.basic.BrooklynTaskTags
-import brooklyn.entity.basic.Entities
-import brooklyn.entity.proxying.EntitySpec
-import brooklyn.entity.trait.Changeable
-import brooklyn.location.Location
-import brooklyn.location.basic.SimulatedLocation
-import brooklyn.management.Task
-import brooklyn.test.TestUtils
-import brooklyn.test.entity.TestApplication
-import brooklyn.test.entity.TestEntity
-import brooklyn.test.entity.TestEntityImpl
-import brooklyn.util.GroovyJavaMethods
-import brooklyn.util.exceptions.Exceptions
-
-import com.google.common.base.Predicates
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList
-import com.google.common.collect.ImmutableSet
-import com.google.common.collect.Iterables
-
-
-class DynamicClusterTest {
-
-    private static final int TIMEOUT_MS = 2000
-
-    TestApplication app
-    SimulatedLocation loc
-    SimulatedLocation loc2
-    Random random = new Random()
-
-    @BeforeMethod
-    public void setUp() {
-        app = ApplicationBuilder.newManagedApp(TestApplication.class);
-        loc = new SimulatedLocation()
-        loc2 = new SimulatedLocation()
-    }
-
-    @AfterMethod(alwaysRun = true)
-    public void tearDown(){
-        if (app != null) Entities.destroyAll(app.getManagementContext());
-    }
-
-    @Test
-    public void creationOkayWithoutNewEntityFactoryArgument() {
-        app.createAndManageChild(EntitySpec.create(DynamicCluster.class));
-    }
-
-    @Test
-    public void constructionRequiresThatNewEntityArgumentIsAnEntityFactory() {
-        try {
-            app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                    .configure("factory", "error"));
-            fail();
-        } catch (Exception e) {
-            if (Exceptions.getFirstThrowableOfType(e, IllegalArgumentException.class) == null) throw e;
-        }
-    }
-
-    @Test
-    public void startRequiresThatNewEntityArgumentIsGiven() {
-        DynamicCluster c = app.createAndManageChild(EntitySpec.create(DynamicCluster.class));
-        try {
-            c.start([loc]);
-            fail();
-        } catch (Exception e) {
-            if (Exceptions.getFirstThrowableOfType(e, IllegalStateException.class) == null) throw e;
-        }
-    }
-
-    @Test
-    public void startMethodFailsIfLocationsParameterHasMoreThanOneElement() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { new TestEntityImpl() }));
-        try {
-            cluster.start([ loc, loc2 ])
-            fail();
-        } catch (Exception e) {
-            if (Exceptions.getFirstThrowableOfType(e, IllegalArgumentException.class) == null) throw e;
-        }
-    }
-
-    @Test
-    public void testClusterHasOneLocationAfterStarting() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { new TestEntityImpl() }));
-        cluster.start([loc])
-        assertEquals(cluster.getLocations().size(), 1)
-        assertEquals(cluster.getLocations() as List, [loc])
-    }
-
-    @Test
-    public void testServiceUpAfterStartingWithNoMembers() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class))
-                .configure(DynamicCluster.INITIAL_SIZE, 0))
-        cluster.start([loc])
-        assertEquals(cluster.getAttribute(Attributes.SERVICE_STATE), Lifecycle.RUNNING)
-        assertTrue(cluster.getAttribute(Attributes.SERVICE_UP))
-    }
-
-    @Test
-    public void usingEntitySpecResizeFromZeroToOneStartsANewEntityAndSetsItsParent() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("memberSpec", EntitySpec.create(TestEntity.class)));
-        
-        cluster.start([loc])
-
-        cluster.resize(1)
-        Entity entity = Iterables.getOnlyElement(cluster.getMembers());
-        assertEquals entity.count, 1
-        assertEquals entity.parent, cluster
-        assertEquals entity.application, app
-    }
-
-    @Test
-    public void resizeFromZeroToOneStartsANewEntityAndSetsItsParent() {
-        TestEntity entity
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> entity = new TestEntityImpl(properties) }));
-
-        cluster.start([loc])
-
-        cluster.resize(1)
-        assertEquals entity.counter.get(), 1
-        assertEquals entity.parent, cluster
-        assertEquals entity.application, app
-    }
-
-    @Test
-    public void currentSizePropertyReflectsActualClusterSize() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) }));
-
-        assertEquals cluster.currentSize, 0
-
-        cluster.start([loc])
-        assertEquals cluster.currentSize, 1
-        assertEquals cluster.getAttribute(Changeable.GROUP_SIZE), 1
-
-        int newSize = cluster.resize(0)
-        assertEquals newSize, 0
-        assertEquals newSize, cluster.currentSize
-        assertEquals newSize, cluster.members.size()
-        assertEquals newSize, cluster.getAttribute(Changeable.GROUP_SIZE)
-
-        newSize = cluster.resize(4)
-        assertEquals newSize, 4
-        assertEquals newSize, cluster.currentSize
-        assertEquals newSize, cluster.members.size()
-        assertEquals newSize, cluster.getAttribute(Changeable.GROUP_SIZE)
-
-        newSize = cluster.resize(0)
-        assertEquals newSize, 0
-        assertEquals newSize, cluster.currentSize
-        assertEquals newSize, cluster.members.size()
-        assertEquals newSize, cluster.getAttribute(Changeable.GROUP_SIZE)
-    }
-
-    @Test
-    public void clusterSizeAfterStartIsInitialSize() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) })
-                .configure("initialSize", 2));
-
-        cluster.start([loc])
-        assertEquals cluster.currentSize, 2
-        assertEquals cluster.members.size(), 2
-        assertEquals cluster.getAttribute(Changeable.GROUP_SIZE), 2
-    }
-
-    @Test
-    public void clusterLocationIsPassedOnToEntityStart() {
-        Collection<Location> locations = [ loc ]
-        TestEntity entity
-        def newEntity = { properties, cluster ->
-            entity = new TestEntityImpl(parent:cluster) {
-	            List<Location> stashedLocations = null
-	            @Override
-	            void start(Collection<? extends Location> loc) {
-	                super.start(loc)
-	                stashedLocations = loc
-	            }
-	        }
-        }
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", newEntity)
-                .configure("initialSize", 1));
-
-        cluster.start(locations)
-
-        assertNotNull entity.stashedLocations
-        assertEquals entity.stashedLocations.size(), 1
-        assertEquals entity.stashedLocations[0], locations[0]
-    }
-
-    @Test
-    public void resizeFromOneToZeroChangesClusterSize() {
-        TestEntity entity
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> entity = new TestEntityImpl(properties) })
-                .configure("initialSize", 1));
-
-        cluster.start([loc])
-        assertEquals cluster.currentSize, 1
-        assertEquals entity.counter.get(), 1
-        cluster.resize(0)
-        assertEquals cluster.currentSize, 0
-        assertEquals entity.counter.get(), 0
-    }
-
-    @Test
-    public void concurrentResizesToSameNumberCreatesCorrectNumberOfNodes() {
-        final int OVERHEAD_MS = 500
-        final int STARTUP_TIME_MS = 50
-        final AtomicInteger numStarted = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { 
-                    Map flags, Entity cluster -> 
-                    Thread.sleep(STARTUP_TIME_MS); numStarted.incrementAndGet(); new TestEntityImpl(flags, cluster)
-                }));
-
-        assertEquals cluster.currentSize, 0
-        cluster.start([loc])
-
-        ExecutorService executor = Executors.newCachedThreadPool()
-        List<Throwable> throwables = new CopyOnWriteArrayList<Throwable>()
-
-        try {
-            for (int i in 1..10) {
-                executor.submit( {
-                    try {
-                        cluster.resize(2)
-                    } catch (Throwable e) {
-                        throwables.add(e)
-                    }
-                })
-            }
-
-            executor.shutdown()
-            assertTrue(executor.awaitTermination(10*STARTUP_TIME_MS+OVERHEAD_MS, TimeUnit.MILLISECONDS))
-            if (throwables.size() > 0) throw throwables.get(0)
-            assertEquals(cluster.currentSize, 2)
-            assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), 2)
-            assertEquals(numStarted.get(), 2)
-        } finally {
-            executor.shutdownNow();
-        }
-    }
-
-    @Test(enabled = false)
-    public void stoppingTheClusterStopsTheEntity() {
-        TestEntity entity
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> entity = new TestEntityImpl(properties) })
-                .configure("initialSize", 1));
-
-        cluster.start([loc])
-        assertEquals entity.counter.get(), 1
-        cluster.stop()
-        assertEquals entity.counter.get(), 0
-    }
-
-    /**
-     * This tests the fix for ENGR-1826.
-     */
-    @Test
-    public void failingEntitiesDontBreakClusterActions() {
-        final int failNum = 2
-        final AtomicInteger counter = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 0)
-                .configure("factory", { properties ->
-                    int num = counter.incrementAndGet();
-                    return new FailingEntity(properties, (num==failNum))
-                }));
-
-        cluster.start([loc])
-        cluster.resize(3)
-        assertEquals(cluster.currentSize, 2)
-        assertEquals(cluster.getMembers().size(), 2)
-        for (Entity member : cluster.getMembers()) {
-            assertFalse(((FailingEntity)member).failOnStart)
-        }
-    }
-
-    @Test
-    public void testInitialQuorumSizeSufficientForStartup() {
-        final int failNum = 1;
-        final AtomicInteger counter = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 2)
-                .configure(DynamicCluster.INITIAL_QUORUM_SIZE, 1)
-                .configure("factory", { properties ->
-                    int num = counter.incrementAndGet();
-                    return new FailingEntity(properties, (num==failNum))
-                }));
-
-        cluster.start([loc])
-        
-        // note that children include quarantine group; and quarantined nodes
-        assertEquals(cluster.getCurrentSize(), 1)
-        assertEquals(cluster.getMembers().size(), 1)
-        for (Entity member : cluster.getMembers()) {
-            assertFalse(((FailingEntity)member).failOnStart)
-        }
-    }
-
-    @Test
-    public void testInitialQuorumSizeDefaultsToInitialSize() throws Exception {
-        final int failNum = 1;
-        final AtomicInteger counter = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 2)
-                .configure("factory", { properties ->
-                    int num = counter.incrementAndGet();
-                    return new FailingEntity(properties, (num==failNum))
-                }));
-
-        try {
-            cluster.start([loc])
-        } catch (Exception e) {
-            IllegalStateException unwrapped = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
-            if (unwrapped != null && unwrapped.getMessage().contains("failed to get to initial size")) {
-                // success
-            } else {
-                throw e; // fail
-            }
-        }
-        
-        // note that children include quarantine group; and quarantined nodes
-        assertEquals(cluster.getCurrentSize(), 1)
-        assertEquals(cluster.getMembers().size(), 1)
-        for (Entity member : cluster.getMembers()) {
-            assertFalse(((FailingEntity)member).failOnStart)
-        }
-    }
-
-    @Test
-    public void testCanQuarantineFailedEntities() {
-        final int failNum = 2
-        final AtomicInteger counter = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("quarantineFailedEntities", true)
-                .configure("initialSize", 0)
-                .configure("factory", { properties ->
-                    int num = counter.incrementAndGet();
-                    return new FailingEntity(properties, (num==failNum))
-                }));
-
-        cluster.start([loc])
-        cluster.resize(3)
-        assertEquals(cluster.currentSize, 2)
-        assertEquals(cluster.getMembers().size(), 2)
-        assertEquals(Iterables.size(Iterables.filter(cluster.getChildren(), Predicates.instanceOf(FailingEntity.class))), 3)
-        cluster.members.each {
-            assertFalse(((FailingEntity)it).failOnStart)
-        }
-
-        assertEquals(cluster.getAttribute(DynamicCluster.QUARANTINE_GROUP).members.size(), 1)
-        cluster.getAttribute(DynamicCluster.QUARANTINE_GROUP).members.each {
-            assertTrue(((FailingEntity)it).failOnStart)
-        }
-    }
-
-    @Test
-    public void testDoNotQuarantineFailedEntities() {
-        final int failNum = 2
-        final AtomicInteger counter = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                // default is quarantineFailedEntities==true
-                .configure("quarantineFailedEntities", false)
-                .configure("initialSize", 0)
-                .configure("factory", { properties ->
-                    int num = counter.incrementAndGet();
-                    return new FailingEntity(properties, (num==failNum))
-                }));
-
-        cluster.start([loc])
-        
-        // no quarantine group, as a child
-        assertEquals(cluster.getChildren().size(), 0, "children="+cluster.getChildren())
-        
-        // Failed node will not be a member or child
-        cluster.resize(3)
-        assertEquals(cluster.currentSize, 2)
-        assertEquals(cluster.getMembers().size(), 2)
-        assertEquals(cluster.getChildren().size(), 2, "children="+cluster.getChildren())
-        
-        // Failed node will not be managed either
-        assertEquals(Iterables.size(Iterables.filter(cluster.getChildren(), Predicates.instanceOf(FailingEntity.class))), 2)
-        for (Entity member : cluster.getMembers()) {
-            assertFalse(((FailingEntity)member).failOnStart)
-        }
-    }
-
-    @Test
-    public void defaultRemovalStrategyShutsDownNewestFirstWhenResizing() {
-        TestEntity entity
-        final int failNum = 2
-        final List<Entity> creationOrder = []
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 0)
-                .configure("factory", { properties ->
-                    Entity result = new TestEntityImpl(properties)
-                    creationOrder << result
-                    return result
-                }));
-
-        cluster.start([loc])
-        cluster.resize(1)
-        cluster.resize(2)
-        assertEquals(cluster.currentSize, 2)
-        assertEquals(ImmutableSet.copyOf(cluster.getMembers()), ImmutableSet.copyOf(creationOrder), "actual="+cluster.getMembers())
-
-        // Now stop one
-        cluster.resize(1)
-        assertEquals(cluster.currentSize, 1)
-        assertEquals(ImmutableList.copyOf(cluster.getMembers()), creationOrder.subList(0, 1))
-    }
-
-    @Test
-    public void resizeLoggedAsEffectorCall() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) }));
-
-        app.start([loc])
-        cluster.resize(1)
-
-        Set<Task> tasks = app.getManagementContext().getExecutionManager().getTasksWithAllTags([
-            BrooklynTaskTags.tagForContextEntity(cluster),"EFFECTOR"])
-        assertEquals(tasks.size(), 2)
-        assertTrue(Iterables.get(tasks, 0).getDescription().contains("start"))
-        assertTrue(Iterables.get(tasks, 1).getDescription().contains("resize"))
-    }
-
-    @Test
-    public void testStoppedChildIsRemoveFromGroup() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 1)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) }));
-
-        cluster.start([loc])
-
-        TestEntity child = Iterables.get(cluster.getMembers(), 0);
-        child.stop()
-        Entities.unmanage(child)
-
-        TestUtils.executeUntilSucceeds(timeout:TIMEOUT_MS) {
-            assertFalse(cluster.getChildren().contains(child), "children="+cluster.getChildren())
-            assertEquals(cluster.currentSize, 0)
-            assertEquals(cluster.members.size(), 0)
-        }
-    }
-
-    @Test
-    public void testPluggableRemovalStrategyIsUsed() {
-        List<Entity> removedEntities = []
-
-        Closure removalStrategy = { Collection<Entity> contenders ->
-            Entity choice = Iterables.get(contenders, random.nextInt(contenders.size()))
-            removedEntities.add(choice)
-            return choice
-        }
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) })
-                .configure("initialSize", 10)
-                .configure("removalStrategy", removalStrategy));
-
-        cluster.start([loc])
-        Set origMembers = cluster.members as Set
-
-        for (int i = 10; i >= 0; i--) {
-            cluster.resize(i)
-            assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), i)
-            assertEquals(removedEntities.size(), 10-i)
-            assertEquals(ImmutableSet.copyOf(Iterables.concat(cluster.members, removedEntities)), origMembers)
-        }
-    }
-
-    @Test
-    public void testPluggableRemovalStrategyCanBeSetAfterConstruction() {
-        List<Entity> removedEntities = []
-
-        Closure removalStrategy = { Collection<Entity> contenders ->
-            Entity choice = Iterables.get(contenders, random.nextInt(contenders.size()))
-            removedEntities.add(choice)
-            return choice
-        }
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) })
-                .configure("initialSize", 10));
-
-        cluster.start([loc])
-        Set origMembers = cluster.members as Set
-
-        cluster.setRemovalStrategy(GroovyJavaMethods.functionFromClosure(removalStrategy));
-
-        for (int i = 10; i >= 0; i--) {
-            cluster.resize(i)
-            assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), i)
-            assertEquals(removedEntities.size(), 10-i)
-            assertEquals(ImmutableSet.copyOf(Iterables.concat(cluster.members, removedEntities)), origMembers)
-        }
-    }
-
-    @Test
-    public void testResizeDoesNotBlockCallsToQueryGroupMembership() {
-        CountDownLatch executingLatch = new CountDownLatch(1)
-        CountDownLatch continuationLatch = new CountDownLatch(1)
-        
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 0)
-                .configure("factory", { properties ->
-                        executingLatch.countDown()
-                        continuationLatch.await()
-                        return new TestEntityImpl(properties)
-                    }));
-
-        cluster.start([loc])
-
-        Thread thread = new Thread( { cluster.resize(1) })
-        try {
-            // wait for resize to be executing
-            thread.start()
-            executingLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)
-
-            // ensure can still call methods on group, to query/update membership
-            assertEquals(cluster.getMembers(), [])
-            assertEquals(cluster.getCurrentSize(), 0)
-            assertFalse(cluster.hasMember(cluster))
-            cluster.addMember(cluster)
-            assertTrue(cluster.removeMember(cluster))
-
-            // allow the resize to complete
-            continuationLatch.countDown()
-            thread.join(TIMEOUT_MS)
-            assertFalse(thread.isAlive())
-        } finally {
-            thread.interrupt()
-        }
-    }
-
-    @Test
-    public void testReplacesMember() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 1)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) }));
-
-        cluster.start([loc])
-        Entity member = Iterables.get(cluster.members, 0);
-
-        String replacementId = cluster.replaceMember(member.getId());
-        Entity replacement = cluster.getManagementContext().getEntityManager().getEntity(replacementId);
-
-        assertEquals(cluster.members.size(), 1)
-        assertFalse(cluster.members.contains(member))
-        assertFalse(cluster.children.contains(member))
-        assertNotNull(replacement, "replacementId="+replacementId);
-        assertTrue(cluster.members.contains(replacement), "replacement="+replacement+"; members="+cluster.members);
-        assertTrue(cluster.children.contains(replacement), "replacement="+replacement+"; children="+cluster.children);
-    }
-
-    @Test
-    public void testReplaceMemberThrowsIfMemberIdDoesNotResolve() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 1)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) }));
-
-        cluster.start([loc])
-        Entity member = Iterables.get(cluster.members, 0);
-
-        try {
-            cluster.replaceMember("wrong.id");
-            fail();
-        } catch (Exception e) {
-            if (Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class) == null) throw e;
-            if (!Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class).getMessage().contains("entity wrong.id cannot be resolved")) throw e;
-        }
-
-        assertEquals(cluster.members as Set, ImmutableSet.of(member));
-    }
-
-    @Test
-    public void testReplaceMemberThrowsIfNotMember() {
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 1)
-                .configure("factory", { properties -> return new TestEntityImpl(properties) }));
-
-        cluster.start([loc])
-        Entity member = Iterables.get(cluster.members, 0);
-
-        try {
-            cluster.replaceMember(app.getId());
-            fail();
-        } catch (Exception e) {
-            if (Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class) == null) throw e;
-            if (!Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class).getMessage().contains("is not a member")) throw e;
-        }
-
-        assertEquals(cluster.members as Set, ImmutableSet.of(member));
-    }
-
-    @Test
-    public void testReplaceMemberFailsIfCantProvisionReplacement() {
-        final int failNum = 2
-        final AtomicInteger counter = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("factory", { properties -> 
-                    int num = counter.incrementAndGet();
-                    return new FailingEntity(properties, (num==failNum))
-                }));
-
-        cluster.start([loc])
-        Entity member = Iterables.get(cluster.members, 0);
-
-        try {
-            cluster.replaceMember(member.getId());
-            fail();
-        } catch (Exception e) {
-            if (!e.toString().contains("failed to grow")) throw e;
-            if (Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class) != null) throw e;
-        }
-        assertEquals(cluster.members as Set, ImmutableSet.of(member));
-    }
-
-    @Test
-    public void testReplaceMemberRemovesAndThowsIfFailToStopOld() {
-        final int failNum = 1
-        final AtomicInteger counter = new AtomicInteger(0)
-        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
-                .configure("initialSize", 1)
-                .configure("factory", { properties ->
-                    int num = counter.incrementAndGet();
-                    return new FailingEntity(properties, false, (num==failNum), IllegalStateException.class)
-                }));
-
-        cluster.start([loc])
-        Entity member = Iterables.get(cluster.members, 0);
-
-        try {
-            cluster.replaceMember(member.getId());
-            fail();
-        } catch (Exception e) {
-            if (Exceptions.getFirstThrowableOfType(e, StopFailedRuntimeException.class) == null) throw e;
-            boolean found = false;
-            for (Throwable t : Throwables.getCausalChain(e)) {
-                if (t.toString().contains("Simulating entity stop failure")) {
-                    found = true;
-                    break;
-                }
-            }
-            if (!found) throw e;
-        }
-        assertFalse(Entities.isManaged(member));
-        assertEquals(cluster.members.size(), 1);
-    }
-
-    private Throwable unwrapException(Throwable e) {
-        if (e instanceof ExecutionException) {
-            return unwrapException(e.cause)
-        } else if (e instanceof org.codehaus.groovy.runtime.InvokerInvocationException) {
-            return unwrapException(e.cause)
-        } else {
-            return e
-        }
-    }
-}
-
-class FailingEntity extends TestEntityImpl {
-    final boolean failOnStart;
-    final boolean failOnStop;
-    final Class<? extends Exception> exceptionClazz;
-
-    public FailingEntity(Map flags, boolean failOnStart) {
-        this(flags, failOnStart, false);
-    }
-    
-    public FailingEntity(Map flags, boolean failOnStart, boolean failOnStop) {
-        this(flags, failOnStart, failOnStop, IllegalStateException.class);
-    }
-    
-    public FailingEntity(Map flags, boolean failOnStart, boolean failOnStop, Class<? extends Exception> exceptionClazz) {
-        super(flags)
-        this.failOnStart = failOnStart;
-        this.failOnStop = failOnStop;
-        this.exceptionClazz = exceptionClazz;
-    }
-    
-    @Override
-    public void start(Collection<? extends Location> locs) {
-        if (failOnStart) {
-            Exception e = exceptionClazz.getConstructor(String.class).newInstance("Simulating entity start failure for test");
-            throw e;
-        }
-    }
-    
-    @Override
-    public void stop() {
-        if (failOnStop) {
-            Exception e = exceptionClazz.getConstructor(String.class).newInstance("Simulating entity stop failure for test");
-            throw e;
-        }
-    }
-}
diff --git a/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java b/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java
new file mode 100644
index 0000000..a59079d
--- /dev/null
+++ b/core/src/test/java/brooklyn/entity/group/DynamicClusterTest.java
@@ -0,0 +1,790 @@
+package brooklyn.entity.group;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.fail;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import brooklyn.entity.Entity;
+import brooklyn.entity.basic.ApplicationBuilder;
+import brooklyn.entity.basic.Attributes;
+import brooklyn.entity.basic.BrooklynTaskTags;
+import brooklyn.entity.basic.Entities;
+import brooklyn.entity.basic.EntityFactory;
+import brooklyn.entity.basic.Lifecycle;
+import brooklyn.entity.proxying.EntitySpec;
+import brooklyn.entity.trait.Changeable;
+import brooklyn.entity.trait.FailingEntity;
+import brooklyn.location.Location;
+import brooklyn.location.basic.SimulatedLocation;
+import brooklyn.management.Task;
+import brooklyn.test.Asserts;
+import brooklyn.test.entity.TestApplication;
+import brooklyn.test.entity.TestEntity;
+import brooklyn.test.entity.TestEntityImpl;
+import brooklyn.util.collections.MutableMap;
+import brooklyn.util.exceptions.Exceptions;
+import brooklyn.util.time.Time;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Atomics;
+
+
+public class DynamicClusterTest {
+
+    private static final int TIMEOUT_MS = 2000;
+
+    TestApplication app;
+    SimulatedLocation loc;
+    SimulatedLocation loc2;
+    Random random = new Random();
+
+    @BeforeMethod
+    public void setUp() {
+        app = ApplicationBuilder.newManagedApp(TestApplication.class);
+        loc = new SimulatedLocation();
+        loc2 = new SimulatedLocation();
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void tearDown(){
+        if (app != null) Entities.destroyAll(app.getManagementContext());
+    }
+
+    @Test
+    public void creationOkayWithoutNewEntityFactoryArgument() throws Exception {
+        app.createAndManageChild(EntitySpec.create(DynamicCluster.class));
+    }
+
+    @Test
+    public void constructionRequiresThatNewEntityArgumentIsAnEntityFactory() throws Exception {
+        try {
+            app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                    .configure("factory", "error"));
+            fail();
+        } catch (Exception e) {
+            if (Exceptions.getFirstThrowableOfType(e, IllegalArgumentException.class) == null) throw e;
+        }
+    }
+
+    @Test
+    public void startRequiresThatNewEntityArgumentIsGiven() throws Exception {
+        DynamicCluster c = app.createAndManageChild(EntitySpec.create(DynamicCluster.class));
+        try {
+            c.start(ImmutableList.of(loc));
+            fail();
+        } catch (Exception e) {
+            if (Exceptions.getFirstThrowableOfType(e, IllegalStateException.class) == null) throw e;
+        }
+    }
+
+    @Test
+    public void startMethodFailsIfLocationsParameterHasMoreThanOneElement() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class)));
+        try {
+            cluster.start(ImmutableList.of(loc, loc2));
+            fail();
+        } catch (Exception e) {
+            if (Exceptions.getFirstThrowableOfType(e, IllegalArgumentException.class) == null) throw e;
+        }
+    }
+
+    @Test
+    public void testClusterHasOneLocationAfterStarting() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class)));
+        cluster.start(ImmutableList.of(loc));
+        assertEquals(cluster.getLocations().size(), 1);
+        assertEquals(ImmutableList.copyOf(cluster.getLocations()), ImmutableList.of(loc));
+    }
+
+    @Test
+    public void testServiceUpAfterStartingWithNoMembers() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure(DynamicCluster.MEMBER_SPEC, EntitySpec.create(TestEntity.class))
+                .configure(DynamicCluster.INITIAL_SIZE, 0));
+        cluster.start(ImmutableList.of(loc));
+        assertEquals(cluster.getAttribute(Attributes.SERVICE_STATE), Lifecycle.RUNNING);
+        assertTrue(cluster.getAttribute(Attributes.SERVICE_UP));
+    }
+
+    @Test
+    public void usingEntitySpecResizeFromZeroToOneStartsANewEntityAndSetsItsParent() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("memberSpec", EntitySpec.create(TestEntity.class)));
+        
+        cluster.start(ImmutableList.of(loc));
+
+        cluster.resize(1);
+        TestEntity entity = (TestEntity) Iterables.getOnlyElement(cluster.getMembers());
+        assertEquals(entity.getCount(), 1);
+        assertEquals(entity.getParent(), cluster);
+        assertEquals(entity.getApplication(), app);
+    }
+
+    @Test
+    public void resizeFromZeroToOneStartsANewEntityAndSetsItsParent() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+
+        cluster.resize(1);
+        TestEntity entity = (TestEntity) Iterables.get(cluster.getMembers(), 0);
+        assertEquals(entity.getCounter().get(), 1);
+        assertEquals(entity.getParent(), cluster);
+        assertEquals(entity.getApplication(), app);
+    }
+
+    @Test
+    public void currentSizePropertyReflectsActualClusterSize() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }}));
+
+
+        assertEquals(cluster.getCurrentSize(), (Integer)0);
+
+        cluster.start(ImmutableList.of(loc));
+        assertEquals(cluster.getCurrentSize(), (Integer)1);
+        assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), (Integer)1);
+
+        int newSize = cluster.resize(0);
+        assertEquals(newSize, 0);
+        assertEquals((Integer)newSize, cluster.getCurrentSize());
+        assertEquals(newSize, cluster.getMembers().size());
+        assertEquals((Integer)newSize, cluster.getAttribute(Changeable.GROUP_SIZE));
+
+        newSize = cluster.resize(4);
+        assertEquals(newSize, 4);
+        assertEquals((Integer)newSize, cluster.getCurrentSize());
+        assertEquals(newSize, cluster.getMembers().size());
+        assertEquals((Integer)newSize, cluster.getAttribute(Changeable.GROUP_SIZE));
+
+        newSize = cluster.resize(0);
+        assertEquals(newSize, 0);
+        assertEquals((Integer)newSize, cluster.getCurrentSize());
+        assertEquals(newSize, cluster.getMembers().size());
+        assertEquals((Integer)newSize, cluster.getAttribute(Changeable.GROUP_SIZE));
+    }
+
+    @Test
+    public void clusterSizeAfterStartIsInitialSize() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }})
+                .configure("initialSize", 2));
+
+        cluster.start(ImmutableList.of(loc));
+        assertEquals(cluster.getCurrentSize(), (Integer)2);
+        assertEquals(cluster.getMembers().size(), 2);
+        assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), (Integer)2);
+    }
+
+    @Test
+    public void clusterLocationIsPassedOnToEntityStart() throws Exception {
+        List<SimulatedLocation> locations = ImmutableList.of(loc);
+        final AtomicReference<Collection<? extends Location>> stashedLocations = Atomics.newReference();
+        
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(parent) {
+                            @Override
+                            public void start(Collection<? extends Location> loc) {
+                                super.start(loc);
+                                stashedLocations.set(loc);
+                            }
+                        };
+                    }})
+                .configure("initialSize", 1));
+
+        cluster.start(locations);
+        TestEntity entity = (TestEntity) Iterables.get(cluster.getMembers(), 0);
+        
+        assertNotNull(stashedLocations.get());
+        assertEquals(stashedLocations.get().size(), 1);
+        assertEquals(ImmutableList.copyOf(stashedLocations.get()), locations);
+    }
+
+    @Test
+    public void resizeFromOneToZeroChangesClusterSize() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }})
+                .configure("initialSize", 1));
+
+        cluster.start(ImmutableList.of(loc));
+        TestEntity entity = (TestEntity) Iterables.get(cluster.getMembers(), 0);
+        assertEquals(cluster.getCurrentSize(), (Integer)1);
+        assertEquals(entity.getCounter().get(), 1);
+        
+        cluster.resize(0);
+        assertEquals(cluster.getCurrentSize(), (Integer)0);
+        assertEquals(entity.getCounter().get(), 0);
+    }
+
+    @Test
+    public void concurrentResizesToSameNumberCreatesCorrectNumberOfNodes() throws Exception {
+        final int OVERHEAD_MS = 500;
+        final int STARTUP_TIME_MS = 50;
+        final AtomicInteger numStarted = new AtomicInteger(0);
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        Time.sleep(STARTUP_TIME_MS);
+                        numStarted.incrementAndGet();
+                        return new TestEntityImpl(flags, parent);
+                    }}));
+
+        assertEquals(cluster.getCurrentSize(), (Integer)0);
+        cluster.start(ImmutableList.of(loc));
+
+        ExecutorService executor = Executors.newCachedThreadPool();
+        final List<Throwable> throwables = new CopyOnWriteArrayList<Throwable>();
+
+        try {
+            for (int i = 0; i < 10; i++) {
+                executor.submit(new Runnable() {
+                    public void run() {
+                        try {
+                            cluster.resize(2);
+                        } catch (Throwable e) {
+                            throwables.add(e);
+                        }
+                    }});
+            }
+
+            executor.shutdown();
+            assertTrue(executor.awaitTermination(10*STARTUP_TIME_MS+OVERHEAD_MS, TimeUnit.MILLISECONDS));
+            if (throwables.size() > 0) throw Exceptions.propagate(throwables.get(0));
+            assertEquals(cluster.getCurrentSize(), (Integer)2);
+            assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), (Integer)2);
+            assertEquals(numStarted.get(), 2);
+        } finally {
+            executor.shutdownNow();
+        }
+    }
+
+    @Test(enabled = false)
+    public void stoppingTheClusterStopsTheEntity() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }})
+                .configure("initialSize", 1));
+
+        cluster.start(ImmutableList.of(loc));
+        TestEntity entity = (TestEntity) Iterables.get(cluster.getMembers(), 0);
+        
+        assertEquals(entity.getCounter().get(), 1);
+        cluster.stop();
+        assertEquals(entity.getCounter().get(), 0);
+    }
+
+    /**
+     * This tests the fix for ENGR-1826.
+     */
+    @Test
+    public void failingEntitiesDontBreakClusterActions() throws Exception {
+        final int failNum = 2;
+        final AtomicInteger counter = new AtomicInteger(0);
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 0)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        int num = counter.incrementAndGet();
+                        return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(FailingEntity.class)
+                                .configure(flags)
+                                .configure(FailingEntity.FAIL_ON_START, (num==failNum))
+                                .parent(parent));
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        cluster.resize(3);
+        assertEquals(cluster.getCurrentSize(), (Integer)2);
+        assertEquals(cluster.getMembers().size(), 2);
+        for (Entity member : cluster.getMembers()) {
+            assertFalse(((FailingEntity)member).getConfig(FailingEntity.FAIL_ON_START));
+        }
+    }
+
+    @Test
+    public void testInitialQuorumSizeSufficientForStartup() throws Exception {
+        final int failNum = 1;
+        final AtomicInteger counter = new AtomicInteger(0);
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 2)
+                .configure(DynamicCluster.INITIAL_QUORUM_SIZE, 1)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        int num = counter.incrementAndGet();
+                        return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(FailingEntity.class)
+                                .configure(flags)
+                                .configure(FailingEntity.FAIL_ON_START, (num==failNum))
+                                .parent(parent));
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        
+        // note that children include quarantine group; and quarantined nodes
+        assertEquals(cluster.getCurrentSize(), (Integer)1);
+        assertEquals(cluster.getMembers().size(), 1);
+        for (Entity member : cluster.getMembers()) {
+            assertFalse(((FailingEntity)member).getConfig(FailingEntity.FAIL_ON_START));
+        }
+    }
+
+    @Test
+    public void testInitialQuorumSizeDefaultsToInitialSize() throws Exception {
+        final int failNum = 1;
+        final AtomicInteger counter = new AtomicInteger(0);
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 2)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        int num = counter.incrementAndGet();
+                        return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(FailingEntity.class)
+                                .configure(flags)
+                                .configure(FailingEntity.FAIL_ON_START, (num==failNum))
+                                .parent(parent));
+                    }}));
+
+        try {
+            cluster.start(ImmutableList.of(loc));
+        } catch (Exception e) {
+            IllegalStateException unwrapped = Exceptions.getFirstThrowableOfType(e, IllegalStateException.class);
+            if (unwrapped != null && unwrapped.getMessage().contains("failed to get to initial size")) {
+                // success
+            } else {
+                throw e; // fail
+            }
+        }
+        
+        // note that children include quarantine group; and quarantined nodes
+        assertEquals(cluster.getCurrentSize(), (Integer)1);
+        assertEquals(cluster.getMembers().size(), 1);
+        for (Entity member : cluster.getMembers()) {
+            assertFalse(((FailingEntity)member).getConfig(FailingEntity.FAIL_ON_START));
+        }
+    }
+
+    @Test
+    public void testCanQuarantineFailedEntities() throws Exception {
+        final int failNum = 2;
+        final AtomicInteger counter = new AtomicInteger(0);
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("quarantineFailedEntities", true)
+                .configure("initialSize", 0)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        int num = counter.incrementAndGet();
+                        return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(FailingEntity.class)
+                                .configure(flags)
+                                .configure(FailingEntity.FAIL_ON_START, (num==failNum))
+                                .parent(parent));
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        cluster.resize(3);
+        assertEquals(cluster.getCurrentSize(), (Integer)2);
+        assertEquals(cluster.getMembers().size(), 2);
+        assertEquals(Iterables.size(Iterables.filter(cluster.getChildren(), Predicates.instanceOf(FailingEntity.class))), 3);
+        for (Entity member : cluster.getMembers()) {
+            assertFalse(((FailingEntity)member).getConfig(FailingEntity.FAIL_ON_START));
+        }
+
+        assertEquals(cluster.getAttribute(DynamicCluster.QUARANTINE_GROUP).getMembers().size(), 1);
+        for (Entity member : cluster.getAttribute(DynamicCluster.QUARANTINE_GROUP).getMembers()) {
+            assertTrue(((FailingEntity)member).getConfig(FailingEntity.FAIL_ON_START));
+        }
+    }
+
+    @Test
+    public void testDoNotQuarantineFailedEntities() throws Exception {
+        final int failNum = 2;
+        final AtomicInteger counter = new AtomicInteger(0);
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                // default is quarantineFailedEntities==true
+                .configure("quarantineFailedEntities", false)
+                .configure("initialSize", 0)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        int num = counter.incrementAndGet();
+                        return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(FailingEntity.class)
+                                .configure(flags)
+                                .configure(FailingEntity.FAIL_ON_START, (num==failNum))
+                                .parent(parent));
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        
+        // no quarantine group, as a child
+        assertEquals(cluster.getChildren().size(), 0, "children="+cluster.getChildren());
+        
+        // Failed node will not be a member or child
+        cluster.resize(3);
+        assertEquals(cluster.getCurrentSize(), (Integer)2);
+        assertEquals(cluster.getMembers().size(), 2);
+        assertEquals(cluster.getChildren().size(), 2, "children="+cluster.getChildren());
+        
+        // Failed node will not be managed either
+        assertEquals(Iterables.size(Iterables.filter(cluster.getChildren(), Predicates.instanceOf(FailingEntity.class))), 2);
+        for (Entity member : cluster.getMembers()) {
+            assertFalse(((FailingEntity)member).getConfig(FailingEntity.FAIL_ON_START));
+        }
+    }
+
+    @Test
+    public void defaultRemovalStrategyShutsDownNewestFirstWhenResizing() throws Exception {
+        final List<Entity> creationOrder = Lists.newArrayList();
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 0)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        Entity result = new TestEntityImpl(flags);
+                        creationOrder.add(result);
+                        return result;
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        cluster.resize(1);
+        cluster.resize(2);
+        assertEquals(cluster.getCurrentSize(), (Integer)2);
+        assertEquals(ImmutableSet.copyOf(cluster.getMembers()), ImmutableSet.copyOf(creationOrder), "actual="+cluster.getMembers());
+
+        // Now stop one
+        cluster.resize(1);
+        assertEquals(cluster.getCurrentSize(), (Integer)1);
+        assertEquals(ImmutableList.copyOf(cluster.getMembers()), creationOrder.subList(0, 1));
+    }
+
+    @Test
+    public void resizeLoggedAsEffectorCall() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }}));
+
+        app.start(ImmutableList.of(loc));
+        cluster.resize(1);
+
+        Set<Task<?>> tasks = app.getManagementContext().getExecutionManager().getTasksWithAllTags(ImmutableList.of(
+            BrooklynTaskTags.tagForContextEntity(cluster),"EFFECTOR"));
+        assertEquals(tasks.size(), 2);
+        assertTrue(Iterables.get(tasks, 0).getDescription().contains("start"));
+        assertTrue(Iterables.get(tasks, 1).getDescription().contains("resize"));
+    }
+
+    @Test
+    public void testStoppedChildIsRemoveFromGroup() throws Exception {
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+
+        final TestEntity child = (TestEntity) Iterables.get(cluster.getMembers(), 0);
+        child.stop();
+        Entities.unmanage(child);
+
+        Asserts.succeedsEventually(MutableMap.of("timeout", TIMEOUT_MS), new Runnable() {
+            @Override public void run() {
+                assertFalse(cluster.getChildren().contains(child), "children="+cluster.getChildren());
+                assertEquals(cluster.getCurrentSize(), (Integer)0);
+                assertEquals(cluster.getMembers().size(), 0);
+            }});
+    }
+
+    @Test
+    public void testPluggableRemovalStrategyIsUsed() throws Exception {
+        final List<Entity> removedEntities = Lists.newArrayList();
+
+        Function<Collection<Entity>, Entity> removalStrategy = new Function<Collection<Entity>, Entity>() {
+            @Override public Entity apply(Collection<Entity> contenders) {
+                Entity choice = Iterables.get(contenders, random.nextInt(contenders.size()));
+                removedEntities.add(choice);
+                return choice;
+            }
+        };
+
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }})
+                .configure("initialSize", 10)
+                .configure("removalStrategy", removalStrategy));
+
+        cluster.start(ImmutableList.of(loc));
+        Set origMembers = ImmutableSet.copyOf(cluster.getMembers());
+
+        for (int i = 10; i >= 0; i--) {
+            cluster.resize(i);
+            assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), (Integer)i);
+            assertEquals(removedEntities.size(), 10-i);
+            assertEquals(ImmutableSet.copyOf(Iterables.concat(cluster.getMembers(), removedEntities)), origMembers);
+        }
+    }
+
+    @Test
+    public void testPluggableRemovalStrategyCanBeSetAfterConstruction() throws Exception {
+        final List<Entity> removedEntities = Lists.newArrayList();
+
+        Function<Collection<Entity>, Entity> removalStrategy = new Function<Collection<Entity>, Entity>() {
+            @Override public Entity apply(Collection<Entity> contenders) {
+                Entity choice = Iterables.get(contenders, random.nextInt(contenders.size()));
+                removedEntities.add(choice);
+                return choice;
+            }
+        };
+        
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }})
+                .configure("initialSize", 10));
+
+        cluster.start(ImmutableList.of(loc));
+        Set origMembers = ImmutableSet.copyOf(cluster.getMembers());
+
+        cluster.setRemovalStrategy(removalStrategy);
+
+        for (int i = 10; i >= 0; i--) {
+            cluster.resize(i);
+            assertEquals(cluster.getAttribute(Changeable.GROUP_SIZE), (Integer)i);
+            assertEquals(removedEntities.size(), 10-i);
+            assertEquals(ImmutableSet.copyOf(Iterables.concat(cluster.getMembers(), removedEntities)), origMembers);
+        }
+    }
+
+    @Test
+    public void testResizeDoesNotBlockCallsToQueryGroupMembership() throws Exception {
+        final CountDownLatch executingLatch = new CountDownLatch(1);
+        final CountDownLatch continuationLatch = new CountDownLatch(1);
+        
+        final DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 0)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        try {
+                            executingLatch.countDown();
+                            continuationLatch.await();
+                            return new TestEntityImpl(flags);
+                        } catch (InterruptedException e) {
+                            throw Exceptions.propagate(e);
+                        }
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+
+        Thread thread = new Thread(new Runnable() {
+                @Override public void run() {
+                    cluster.resize(1);
+                }});
+        
+        try {
+            // wait for resize to be executing
+            thread.start();
+            executingLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+            // ensure can still call methods on group, to query/update membership
+            assertEquals(ImmutableList.copyOf(cluster.getMembers()), ImmutableList.of());
+            assertEquals(cluster.getCurrentSize(), (Integer)0);
+            assertFalse(cluster.hasMember(cluster));
+            cluster.addMember(cluster);
+            assertTrue(cluster.removeMember(cluster));
+
+            // allow the resize to complete
+            continuationLatch.countDown();
+            thread.join(TIMEOUT_MS);
+            assertFalse(thread.isAlive());
+        } finally {
+            thread.interrupt();
+        }
+    }
+
+    @Test
+    public void testReplacesMember() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        Entity member = Iterables.get(cluster.getMembers(), 0);
+
+        String replacementId = cluster.replaceMember(member.getId());
+        Entity replacement = app.getManagementContext().getEntityManager().getEntity(replacementId);
+
+        assertEquals(cluster.getMembers().size(), 1);
+        assertFalse(cluster.getMembers().contains(member));
+        assertFalse(cluster.getChildren().contains(member));
+        assertNotNull(replacement, "replacementId="+replacementId);
+        assertTrue(cluster.getMembers().contains(replacement), "replacement="+replacement+"; members="+cluster.getMembers());
+        assertTrue(cluster.getChildren().contains(replacement), "replacement="+replacement+"; children="+cluster.getChildren());
+    }
+
+    @Test
+    public void testReplaceMemberThrowsIfMemberIdDoesNotResolve() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        Entity member = Iterables.get(cluster.getMembers(), 0);
+
+        try {
+            cluster.replaceMember("wrong.id");
+            fail();
+        } catch (Exception e) {
+            if (Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class) == null) throw e;
+            if (!Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class).getMessage().contains("entity wrong.id cannot be resolved")) throw e;
+        }
+
+        assertEquals(ImmutableSet.copyOf(cluster.getMembers()), ImmutableSet.of(member));
+    }
+
+    @Test
+    public void testReplaceMemberThrowsIfNotMember() throws Exception {
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        return new TestEntityImpl(flags);
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        Entity member = Iterables.get(cluster.getMembers(), 0);
+
+        try {
+            cluster.replaceMember(app.getId());
+            fail();
+        } catch (Exception e) {
+            if (Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class) == null) throw e;
+            if (!Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class).getMessage().contains("is not a member")) throw e;
+        }
+
+        assertEquals(ImmutableSet.copyOf(cluster.getMembers()), ImmutableSet.of(member));
+    }
+
+    @Test
+    public void testReplaceMemberFailsIfCantProvisionReplacement() throws Exception {
+        final int failNum = 2;
+        final AtomicInteger counter = new AtomicInteger(0);
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        int num = counter.incrementAndGet();
+                        return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(FailingEntity.class)
+                                .configure(flags)
+                                .configure(FailingEntity.FAIL_ON_START, (num==failNum))
+                                .parent(parent));
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        Entity member = Iterables.get(cluster.getMembers(), 0);
+
+        try {
+            cluster.replaceMember(member.getId());
+            fail();
+        } catch (Exception e) {
+            if (!e.toString().contains("failed to grow")) throw e;
+            if (Exceptions.getFirstThrowableOfType(e, NoSuchElementException.class) != null) throw e;
+        }
+        assertEquals(ImmutableSet.copyOf(cluster.getMembers()), ImmutableSet.of(member));
+    }
+
+    @Test
+    public void testReplaceMemberRemovesAndThowsIfFailToStopOld() throws Exception {
+        final int failNum = 1;
+        final AtomicInteger counter = new AtomicInteger(0);
+        DynamicCluster cluster = app.createAndManageChild(EntitySpec.create(DynamicCluster.class)
+                .configure("initialSize", 1)
+                .configure("factory", new EntityFactory() {
+                    @Override public Entity newEntity(Map flags, Entity parent) {
+                        int num = counter.incrementAndGet();
+                        return app.getManagementContext().getEntityManager().createEntity(EntitySpec.create(FailingEntity.class)
+                                .configure(flags)
+                                .configure(FailingEntity.FAIL_ON_STOP, (num==failNum))
+                                .parent(parent));
+                    }}));
+
+        cluster.start(ImmutableList.of(loc));
+        Entity member = Iterables.get(cluster.getMembers(), 0);
+
+        try {
+            cluster.replaceMember(member.getId());
+            fail();
+        } catch (Exception e) {
+            if (Exceptions.getFirstThrowableOfType(e, StopFailedRuntimeException.class) == null) throw e;
+            boolean found = false;
+            for (Throwable t : Throwables.getCausalChain(e)) {
+                if (t.toString().contains("Simulating entity stop failure")) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) throw e;
+        }
+        assertFalse(Entities.isManaged(member));
+        assertEquals(cluster.getMembers().size(), 1);
+    }
+
+    private Throwable unwrapException(Throwable e) {
+        if (e instanceof ExecutionException) {
+            return unwrapException(e.getCause());
+        } else if (e instanceof org.codehaus.groovy.runtime.InvokerInvocationException) {
+            return unwrapException(e.getCause());
+        } else {
+            return e;
+        }
+    }
+}
diff --git a/core/src/test/java/brooklyn/entity/group/MembershipTrackingPolicyTest.java b/core/src/test/java/brooklyn/entity/group/MembershipTrackingPolicyTest.java
index 2d08ef1..e597358 100644
--- a/core/src/test/java/brooklyn/entity/group/MembershipTrackingPolicyTest.java
+++ b/core/src/test/java/brooklyn/entity/group/MembershipTrackingPolicyTest.java
@@ -16,8 +16,10 @@
 import brooklyn.entity.basic.Entities;
 import brooklyn.entity.proxying.EntitySpec;
 import brooklyn.entity.trait.Startable;
+import brooklyn.event.Sensor;
 import brooklyn.location.basic.SimulatedLocation;
 import brooklyn.management.EntityManager;
+import brooklyn.policy.PolicySpec;
 import brooklyn.test.Asserts;
 import brooklyn.test.entity.TestApplication;
 import brooklyn.test.entity.TestEntity;
@@ -132,6 +134,32 @@
 
         assertRecordsEventually(policy2, Record.newAdded(e1), Record.newChanged(e1));
     }
+    
+    @Test
+    public void testNotNotifiedOfExtraTrackedSensorsIfNonDuplicate() throws Exception {
+        TestEntity e1 = createAndManageChildOf(group);
+        
+        PolicySpec<RecordingMembershipTrackingPolicy> nonDuplicateTrackingPolicySpec = 
+                PolicySpec.create(RecordingMembershipTrackingPolicy.class)
+                .configure(AbstractMembershipTrackingPolicy.SENSORS_TO_TRACK, ImmutableSet.<Sensor<?>>of(TestEntity.NAME))
+                .configure(AbstractMembershipTrackingPolicy.NOTIFY_ON_DUPLICATES, false);
+        
+        RecordingMembershipTrackingPolicy nonDuplicateTrackingPolicy = group.addPolicy(nonDuplicateTrackingPolicySpec);
+        group.addPolicy(nonDuplicateTrackingPolicy);
+        nonDuplicateTrackingPolicy.setGroup(group);
+
+        e1.setAttribute(TestEntity.NAME, "myname");
+
+        assertRecordsEventually(nonDuplicateTrackingPolicy, Record.newAdded(e1), Record.newChanged(e1));
+        
+        e1.setAttribute(TestEntity.NAME, "myname");
+        
+        assertRecordsContinually(nonDuplicateTrackingPolicy, Record.newAdded(e1), Record.newChanged(e1));
+        
+        e1.setAttribute(TestEntity.NAME, "mynewname");
+        
+        assertRecordsEventually(nonDuplicateTrackingPolicy, Record.newAdded(e1), Record.newChanged(e1), Record.newChanged(e1));
+    }
 
     private void assertRecordsEventually(final Record... expected) {
         assertRecordsEventually(policy, expected);
@@ -153,15 +181,24 @@
     }
 
     private void assertRecordsContinually(final Record... expected) {
+        assertRecordsContinually(policy, expected);
+    }
+    
+    private void assertRecordsContinually(final RecordingMembershipTrackingPolicy policy, final Record... expected) {
         Asserts.succeedsContinually(ImmutableMap.of("timeout", 100), new Runnable() {
             public void run() {
                 assertEquals(policy.records, ImmutableList.copyOf(expected), "actual="+policy.records);
             }});
     }
 
-    static class RecordingMembershipTrackingPolicy extends AbstractMembershipTrackingPolicy {
+    // Needs to be public when instantiated from a spec (i.e. by InternalPolicyFactory)
+    public static class RecordingMembershipTrackingPolicy extends AbstractMembershipTrackingPolicy {
         final List<Record> records = new CopyOnWriteArrayList<Record>();
 
+        public RecordingMembershipTrackingPolicy() {
+            super();
+        }
+        
         public RecordingMembershipTrackingPolicy(MutableMap<String, ?> flags) {
             super(flags);
         }
diff --git a/docs/_scripts/help.txt b/docs/_scripts/help.txt
index f6e4ae5..0a41a7b 100644
--- a/docs/_scripts/help.txt
+++ b/docs/_scripts/help.txt
@@ -9,81 +9,8 @@
     will build the site in _site including javadoc for offline browsing
 
 
+Release Process
+===============
 
-###############################################################################
-# Deprecation Warning                                                         #
-#                                                                             #
-# The following content has been superseded, and will be removed shortly      #
-# Please view /dev/tips/release.md or see:                                    #
-# http://brooklyncentral.github.io/v/0.6.0-SNAPSHOT/dev/tips/release.html     #
-#                                                                             #
-###############################################################################
-
-
-GO LIVE (SNAPSHOT)
-------------------
-
-to make the docs live you build.sh then push _site to github brooklyncentral/brooklyncentral.github.com
-the following instructions cover this, assuming brooklyncentral.github.com is a sibling dir to the brooklyn project,
-and you are in the docs dir
-
-
-updating a snapshot version, in /v/VERSION/ on the server:
-
-export TARGET=`pwd -P`/../../brooklyncentral.github.com
-export BV=0.7.0-SNAPSHOT   # BROOKLYN_VERSION
-
-# make sure repo is at latest
-pushd $TARGET
-git pull
-popd
-
-# build, copy
-if [ ! -f $TARGET/index.html ] ; then echo "could not find docs in $TARGET" ; exit 1 ; fi
-_scripts/build.sh || { echo "failed to build docs" ; exit 1 ; }
-rm -rf $TARGET/v/$BV
-mkdir $TARGET/v/$BV
-cp -r _site/* $TARGET/v/$BV/
-
-# and push
-pushd $TARGET
-git add -A .
-git commit -m "updated version docs for version $BV"
-git push
-popd
-
-
-RELEASE
--------
-
-when we do a RELEASE we must run the above for e.g. BV=0.4.0
-AND must update the files in the root dir of brooklyncentral.github.com
-
-the commands below do this (basically like the process above but 
-with / in place of /v/$BV and overriding url in _config.yml 
-with the --url arg below):
-
-
-# remove old root files
-pushd $TARGET
-if [ -f start/index.html ] ; then
-  for x in * ; do if [[ $x != "v" ]] ; then rm -rf $x ; fi ; done
-else
-  echo IN WRONG DIRECTORY $TARGET - export TARGET to continue
-  exit 1
-fi
-popd
-
-# build for hosting of / rather than /v/VERSION/
-_scripts/build.sh --url "" || { echo "failed to build docs" ; exit 1 ; }
-cp -r _site/* $TARGET/
-
-# and git push
-pushd $TARGET
-git add -A .
-git commit -m "updated root docs for version $BV"
-git push
-popd
-
-
-// END
+Docs release instructions can be found at:
+http://brooklyncentral.github.io/dev/tips/release.html
diff --git a/docs/use/guide/locations/index.md b/docs/use/guide/locations/index.md
index 9570aa4..1c0668e 100644
--- a/docs/use/guide/locations/index.md
+++ b/docs/use/guide/locations/index.md
@@ -11,9 +11,11 @@
 
 Brooklyn looks for Location configuration in `~/.brooklyn/brooklyn.properties`.
 
-### Must have an SSH key
+## Setting up an SSH key
 
-To access any locations, Brooklyn must have access to an SSH key. By default Brooklyn looks for a key at `~/.ssh/id_rsa` and `~/.ssh/id_dsa`.
+While some locations can be accessed using *user:password* credentials it is more common to use authentication keys.
+
+To use keyfile based authentication, Brooklyn requires that the management machine has an SSH key. By default Brooklyn looks for a key at `~/.ssh/id_rsa` and `~/.ssh/id_dsa`.
 
 If you do not already have an SSH key installed, create a new id_rsa key:
 
@@ -28,60 +30,67 @@
 
 ## Localhost
 
-To allow Brooklyn to access locahost the SSH key must be added to the `authorized_keys` on that machine.
+Brooklyn can access localhost if there is an SSH key on the machine and if the SSH key has been added to the list of  `authorized_keys` on that machine.
 
 {% highlight bash %}
 # _Appends_ id_rsa.pub to authorized_keys. Other keys are unaffected.
 $ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
 {% endhighlight %}
 
-(MacOS user?: In addition to the above, enable 'Remote Login' in System Preferences >
- Sharing.)
+MacOS user? In addition to the above, enable 'Remote Login' in System Preferences >
+ Sharing.
 
 
 ## Cloud Endpoints (via jclouds)
 
-[Apache jclouds](http://www.jclouds.org) is a multi-cloud library that Brooklyn uses to access [many clouds](http://www.jclouds.org/documentation/reference/supported-providers/).
+[Apache jclouds](http://www.jclouds.org) is a multi-cloud library that Brooklyn uses to access many clouds. The [full list of supported providers](http://jclouds.apache.org/reference/providers/) is available on jclouds.apache.org.
+
+Add your cloud provider's (API) credentials to `brooklyn.properties` and create an SSH key on the management machine.
+
+Some clouds provide both API keys and SSH keys. In this case add only the API credentials to `brooklyn.properties`. (jclouds will transparently use the API credentials to setup access using the management machine's SSH key.)
 
 ### Example: AWS Virginia Large Centos
 
 {% highlight bash %}
+## Snippet from ~/.brooklyn/brooklyn.properties.
+
 # Provide jclouds with AWS API credentials.
-brooklyn.jclouds.aws-ec2.identity = AKA_YOUR_ACCESS_KEY_ID
-brooklyn.jclouds.aws-ec2.credential = YourSecretKeyWhichIsABase64EncodedString
+brooklyn.location.jclouds.aws-ec2.identity = AKA_YOUR_ACCESS_KEY_ID
+brooklyn.location.jclouds.aws-ec2.credential = YourSecretKeyWhichIsABase64EncodedString
 
 # Name this location 'AWS Virginia Large Centos' and wire to AWS US-East-1
 brooklyn.location.named.AWS\ Virginia\ Large\ Centos = jclouds:aws-ec2:us-east-1
 
-# Specify image, user and minimum ram size (ie instance size)
+# (Using the same name) specify image, user and minimum ram size (ie instance size)
 brooklyn.location.named.AWS\ Virginia\ Large\ Centos.imageId=us-east-1/ami-7d7bfc14
 brooklyn.location.named.AWS\ Virginia\ Large\ Centos.user=root
 brooklyn.location.named.AWS\ Virginia\ Large\ Centos.minRam=4096
 
-# Snippet from ~/.brooklyn/brooklyn.properties.
+
 {% endhighlight %}
 
-This will  appear as 'AWS Virginia Large Centos' in the web console, but will need to be escaped on the command line:  `AWS\ Virginia\ Large\ Centos`.
+This will  appear as 'AWS Virginia Large Centos' in the web console, but will need to be escaped on the command line as:  `AWS\ Virginia\ Large\ Centos`.
 
 See the Getting Started [template brooklyn.properties](/use/guide/quickstart/brooklyn.properties) for more examples of using cloud endpoints.
 
 
 ## Fixed Infrastructure
 
-Bring-ing your own nodes (BYON) to Brooklyn is straightforward.
+Bringing your own nodes (BYON) to Brooklyn is straightforward.
 
 You will need the IP addresses of the nodes and the access credentials. Both SSH and password based login are supported.
 
 ### Example: On-Prem Iron
 
 {% highlight bash %}
+## Snippet from ~/.brooklyn/brooklyn.properties.
+
 # Use the byon prefix, and provide the IP addresss (or IP ranges)
 brooklyn.location.named.On-Prem\ Iron\ Example=byon:(hosts="10.9.1.1,10.9.1.2,produser2@10.9.2.{10,11,20-29}")
 brooklyn.location.named.On-Prem\ Iron\ Example.user=produser1
 brooklyn.location.named.On-Prem\ Iron\ Example.privateKeyFile=~/.ssh/produser_id_rsa
 brooklyn.location.named.On-Prem\ Iron\ Example.privateKeyPassphrase=s3cr3tpassphrase
 
-# Snippet from ~/.brooklyn/brooklyn.properties.
 {% endhighlight %}
 
 
diff --git a/docs/use/guide/quickstart/brooklyn.properties b/docs/use/guide/quickstart/brooklyn.properties
index 53b479a..08a9d7c 100644
--- a/docs/use/guide/quickstart/brooklyn.properties
+++ b/docs/use/guide/quickstart/brooklyn.properties
@@ -6,22 +6,13 @@
 
 # It's great to have you here.
 
-# Getting Started options have been pulled to the top. They are repeated further down.
-# When you are happy with what Brooklyn does, and how to use it, you can delete the
-# Getting Started Section completely, and use the full options below to setup your 
-# configuration.
-
-# (There's a formatting guide at the very bottom.)
+# Getting Started options have been pulled to the top. There's a formatting guide at the
+# very bottom.
 
 ############################ Getting Started Options  ####################################
 
-# By default we have AWS and Rackspace (non-UK) set up (but with invalid credentials!)
-# For each of those, either set the credentials immediately below
-# or remove the corresponding "location.named" lines far below 
-# (i.e. look for  brooklyn.location.named...=...<aws>..  or =...<cloudservers>... )
-
-# For other clouds, ADD corresponding identity and credential lines 
-# and enable the "brooklyn.location.named" line(s) setup far below
+# By default we have AWS set up (but with invalid credentials!).  Many, many other
+# providers are supported.
 
 ## Amazon EC2 Credentials
 # These should be an "Access Key ID" and "Secret Access Key" for your account.
@@ -30,18 +21,28 @@
 brooklyn.location.jclouds.aws-ec2.identity = AKA_YOUR_ACCESS_KEY_ID
 brooklyn.location.jclouds.aws-ec2.credential = <access-key-hex-digits>
 
-# Instructions for many other clouds (public and private) are below.
-
 # Beware of trailing spaces in your cloud credentials. This will cause unexpected
 # 401: unauthorized responses.
 
-## SSH key for localhost 
+## Using Other Clouds
+# 1. Cast your eyes down this document to find your preferred cloud in the Named Locations
+#    section, and the examples.
+# 2. Uncomment the relevant line(s) for your provider.
+# 3. ADD  -.identity and -.credential lines for your provider, similar to the AWS ones above,
+#    replacing 'aws-ec2' with jcloud's id for your cloud.
+
+
+## Deploying to Localhost
+## see: brooklyncentral.github.io/use/guide/locations/
+#
 ## ~/.ssh/id_rsa is Brooklyn's default location
-## Passphrases are supported, but not required
 # brooklyn.location.localhost.privateKeyFile = ~/.ssh/id_rsa
+## Passphrases are supported, but not required
 # brooklyn.location.localhost.privateKeyPassphrase = s3cr3tpassphrase
 
-## Geoscaling Service (Used for global web fabric demo) https://www.geoscaling.com/dns2/
+## Geoscaling Service - used for the Global Web Fabric demo
+## see: brooklyncentral.github.io/use/examples/global-web-fabric/ and
+## https://www.geoscaling.com/dns2/
 ## other services may take similar configuration similarly; or can usually be set in YAML
 # brooklyn.geoscaling.username = USERNAME
 # brooklyn.geoscaling.password = PASSWORD
@@ -52,10 +53,9 @@
 
 # That's it, although you may want to read through these options...
 
-
 ################################ Brooklyn Options ########################################
 
-## Brooklyn Mgmt Base Directory: specify where mgmt data should be stored on this server; 
+## Brooklyn Management Base Directory: specify where management data should be stored on this server;
 ## ~/.brooklyn/ is the default but you could use something like /opt/brooklyn/state/
 ## (provided this process has write permissions) 
 # brooklyn.base.dir=~/.brooklyn/
@@ -91,11 +91,11 @@
 ## many cloud machines don't have sufficient entropy for lots of encrypted networking, so fake it:
 # brooklyn.location.jclouds.installDevUrandom=true
 
-## This sets a property for all AWS machines. Recommended to avoid getting m1.micros !
+## Sets a minimium ram property for all jclouds locations. Recommended to avoid getting m1.micros on AWS!
 brooklyn.location.jclouds.minRam = 2048
 
-## By default it will set up a user with the same name as the user running on the brooklyn server, 
-## but you can force a user here:
+## When setting up a new cloud machine Brooklyn creates a user with the same name as the user running
+## Brooklyn on the management server, but you can force a different user here:
 # brooklyn.location.jclouds.user=brooklyn
 ## And you can force a password or key (by default it will use the keys in ~/.ssh/id_rsa{,.pub}
 # brooklyn.location.jclouds.password=s3cr3t
@@ -103,7 +103,7 @@
 ################################ Named Locations ########################################
 
 # Named locations appear in the web console. If using the command line or YAML it may be 
-# just as easy to use the jclouds:<provider> locations and specify add'l properties there. 
+# just as easy to use the jclouds:<provider> locations and specify additional properties there.
 
 ## Example: AWS Virginia using Rightscale 6.3 64bit Centos AMI and Large Instances
 # brooklyn.location.named.aws-va-centos-large = jclouds:aws-ec2:us-east-1
@@ -125,7 +125,7 @@
 brooklyn.location.named.aws-ireland = jclouds:aws-ec2:eu-west-1
 brooklyn.location.named.aws-tokyo = jclouds:aws-ec2:ap-northeast-1
 
-## Similarly, for Google Compute
+## Google Compute
 ## Note at present you have to create and download the P12 key from the Google "APIs & auth -> Registered Apps" interface,
 ## then convert to PEM private key format using  `openssl pkcs12 -in Certificates.p12 -out Certificates.pem -nodes`
 ## then embed that on one line as the 'credential, replacing new lines with \n as below
@@ -146,13 +146,13 @@
 ## gce images often start with iptables turned on; turn it off 
 # brooklyn.location.jclouds.google-compute-engine.stopIptables=true
 
-## Similarly, here are definitions for HP Cloud - also Ubuntu 12.04 LTS
+## HP Cloud - also Ubuntu 12.04 LTS
 ## You specify your HP Credentials like this:
 # brooklyn.location.jclouds.hpcloud-compute.identity = projectname:username
 # brooklyn.location.jclouds.hpcloud-compute.credential = password
 ## where username and password are the same as logging in to the web console, and
 ## projectname can be found here: https://account.hpcloud.com/projects
-# brooklyn.location.named.HP\ Cloud\ Arizona-1 = jclouds:hpcloud-compute:az-1.region-a.geo-1
+#�brooklyn.location.named.HP\ Cloud\ Arizona-1 = jclouds:hpcloud-compute:az-1.region-a.geo-1
 # brooklyn.location.named.HP\ Cloud\ Arizona-1.imageId = az-1.region-a.geo-1/75845
 # brooklyn.location.named.HP\ Cloud\ Arizona-1.user = ubuntu
 
@@ -172,7 +172,7 @@
 ## Brooklyn uses the jclouds multi-cloud library to access many clouds.
 ## http://www.jclouds.org/documentation/reference/supported-providers/
 
-## Templates for many other clouds, but remember the identity and credentials:
+## Templates for many other clouds, but remember to add identity and credentials:
 
 # brooklyn.location.named.Bluelock = jclouds:bluelock-vcloud-zone01
 
@@ -222,23 +222,23 @@
 
 
 ## Production pool of machines for my application (deploy to named:On-Prem\ Iron\ Example)
-#brooklyn.location.named.On-Prem\ Iron\ Example=byon:(hosts="10.9.1.1,10.9.1.2,produser2@10.9.2.{10,11,20-29}")
-#brooklyn.location.named.On-Prem\ Iron\ Example.user=produser1
-#brooklyn.location.named.On-Prem\ Iron\ Example.privateKeyFile=~/.ssh/produser_id_rsa
-#brooklyn.location.named.On-Prem\ Iron\ Example.privateKeyPassphrase=s3cr3tpassphrase
+# brooklyn.location.named.On-Prem\ Iron\ Example=byon:(hosts="10.9.1.1,10.9.1.2,produser2@10.9.2.{10,11,20-29}")
+# brooklyn.location.named.On-Prem\ Iron\ Example.user=produser1
+# brooklyn.location.named.On-Prem\ Iron\ Example.privateKeyFile=~/.ssh/produser_id_rsa
+# brooklyn.location.named.On-Prem\ Iron\ Example.privateKeyPassphrase=s3cr3tpassphrase
 
-## Various private clouds
+## Various Private Clouds
 
 ## openstack identity and credential are random strings of letters and numbers (TBC - still the case?)
-#brooklyn.location.named.My\ Openstack=jclouds:openstack-nova:https://9.9.9.9:9999/v2.0/
+# brooklyn.location.named.My\ Openstack=jclouds:openstack-nova:https://9.9.9.9:9999/v2.0/
 
 ## cloudstack identity and credential are rather long random strings of letters and numbers
 ## you generate this in the cloudstack gui, under accounts, then "view users", then "generate key"
 ## use the "api key" as the identity and "secret key" as the credential
-#brooklyn.location.named.My\ Cloudstack=jclouds:cloudstack:http://9.9.9.9:9999/client/api/
+# brooklyn.location.named.My\ Cloudstack=jclouds:cloudstack:http://9.9.9.9:9999/client/api/
 
 ## abiquo identity and credential are your login username/passed
-#brooklyn.location.named.My\ Abiquo=jclouds:abiquo:http://demonstration.abiquo.com/api/
+# brooklyn.location.named.My\ Abiquo=jclouds:abiquo:http://demonstration.abiquo.com/api/
 
 ###############################  Formatting Guide  #######################################
 
diff --git a/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-large.png b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-large.png
new file mode 100644
index 0000000..b566b1a
--- /dev/null
+++ b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-large.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-location-large.png b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-location-large.png
new file mode 100644
index 0000000..05e9b0c
--- /dev/null
+++ b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-location-large.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-location.png b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-location.png
new file mode 100644
index 0000000..c13fdd8
--- /dev/null
+++ b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db-location.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db.png b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db.png
new file mode 100644
index 0000000..ebb6f42
--- /dev/null
+++ b/docs/use/guide/quickstart/images/add-application-catalog-web-cluster-with-db.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/add-application-modal-yaml.png b/docs/use/guide/quickstart/images/add-application-modal-yaml.png
new file mode 100644
index 0000000..c50b7ab
--- /dev/null
+++ b/docs/use/guide/quickstart/images/add-application-modal-yaml.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/jboss7-cluster-policies-large.png b/docs/use/guide/quickstart/images/jboss7-cluster-policies-large.png
new file mode 100644
index 0000000..3d84477
--- /dev/null
+++ b/docs/use/guide/quickstart/images/jboss7-cluster-policies-large.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/jboss7-cluster-policies.png b/docs/use/guide/quickstart/images/jboss7-cluster-policies.png
new file mode 100644
index 0000000..2f85328
--- /dev/null
+++ b/docs/use/guide/quickstart/images/jboss7-cluster-policies.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-db-activities-large.png b/docs/use/guide/quickstart/images/my-db-activities-large.png
new file mode 100644
index 0000000..c214d9e
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-db-activities-large.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-db-activities.png b/docs/use/guide/quickstart/images/my-db-activities.png
new file mode 100644
index 0000000..0f2327c
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-db-activities.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-web-cluster-starting.png b/docs/use/guide/quickstart/images/my-web-cluster-starting.png
new file mode 100644
index 0000000..c389b0b
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-web-cluster-starting.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-web-cluster-stop-confirm-large.png b/docs/use/guide/quickstart/images/my-web-cluster-stop-confirm-large.png
new file mode 100644
index 0000000..c9bdab6
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-web-cluster-stop-confirm-large.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-web-cluster-stop-confirm.png b/docs/use/guide/quickstart/images/my-web-cluster-stop-confirm.png
new file mode 100644
index 0000000..179b00a
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-web-cluster-stop-confirm.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-web-summary-large.png b/docs/use/guide/quickstart/images/my-web-summary-large.png
new file mode 100644
index 0000000..fc4bffe
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-web-summary-large.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-web-summary.png b/docs/use/guide/quickstart/images/my-web-summary.png
new file mode 100644
index 0000000..e85752f
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-web-summary.png
Binary files differ
diff --git a/docs/use/guide/quickstart/images/my-web.png b/docs/use/guide/quickstart/images/my-web.png
new file mode 100644
index 0000000..2bd6ac3
--- /dev/null
+++ b/docs/use/guide/quickstart/images/my-web.png
Binary files differ
diff --git a/docs/use/guide/quickstart/index.md b/docs/use/guide/quickstart/index.md
index 35db7f3..289acd8 100644
--- a/docs/use/guide/quickstart/index.md
+++ b/docs/use/guide/quickstart/index.md
@@ -7,30 +7,12 @@
 
 {% include fields.md %}
 
-This guide will walk you through deploying an application to a public cloud, and managing that application.
+This guide will walk you through deploying an application to a public cloud.
 
 We will be deploying an example 3-tier web application, described using this blueprint: 
 
 {% highlight yaml %}
-name: My Web Cluster
-location: localhost
-
-services:
-
-- serviceType: brooklyn.entity.webapp.ControlledDynamicWebAppCluster
-  name: My Web
-  location: localhost
-  brooklyn.config:
-    wars.root: http://search.maven.org/remotecontent?filepath=io/brooklyn/example/brooklyn-example-hello-world-sql-webapp/0.6.0-M2/brooklyn-example-hello-world-sql-webapp-0.6.0-M2.war
-    java.sysprops: 
-      brooklyn.example.db.url: $brooklyn:formatString("jdbc:%s%s?user=%s\\&password=%s",
-         component("db").attributeWhenReady("database.url"), "visitors", "brooklyn", "br00k11n")
-
-- serviceType: brooklyn.entity.database.mysql.MySqlNode
-  id: db
-  name: My DB
-  brooklyn.config:
-    creationScriptUrl: classpath://visitors-creation-script.sql
+{% readj my-web-cluster.yaml %}
 {% endhighlight %}
 
 (This is written in YAML, following the [camp specification](https://www.oasis-open.org/committees/camp/). )
@@ -58,12 +40,14 @@
 
 This will create a `brooklyn-{{ site.brooklyn-version }}` folder.
 
-Note: you'll also need Java JRE or SDK installed (version 6 or later).
+Note: You'll need a Java JRE or SDK installed (version 6 or later), as Brooklyn is Java under the covers.
 
 ## Launch Brooklyn
 
 Let's setup some paths for easy commands.
 
+(Click the clipboard on these code snippets for easier c&p.)
+
 {% highlight bash %}
 $ cd brooklyn-{{ site.brooklyn-version }}
 $ BROOKLYN_DIR="$(pwd)"
@@ -78,7 +62,10 @@
 
 Brooklyn will output the address of the management interface:
 
-`... Started Brooklyn console at http://127.0.0.1:8081/` ([link](http://127.0.0.1:8081/))
+
+`INFO  Starting brooklyn web-console on loopback interface because no security config is set`
+
+`INFO  Started Brooklyn console at http://127.0.0.1:8081/, running classpath://brooklyn.war and []`
 
 But before we really use Brooklyn, we need to setup some Locations.
  
@@ -86,19 +73,23 @@
 
 ## Configuring a Location
 
-Brooklyn deploys applications to Locations. Locations can be clouds, machines with fixed IPs or localhost (for testing).
+Brooklyn deploys applications to Locations.
+
+Locations can be clouds, machines with fixed IPs or localhost (for testing).
 
 Brooklyn loads Location configuration  from `~/.brooklyn/brooklyn.properties`. 
 
-Create a `.brooklyn` folder in your home directory:
+Create a `.brooklyn` folder in your home directory and download the template [brooklyn.properties](brooklyn.properties) to that folder.
 
 {% highlight bash %}
 $ mkdir ~/.brooklyn
+$ cd ~/.brooklyn
+$ wget {{site.url}}/use/guide/quickstart/brooklyn.properties
 {% endhighlight %}
 
-Download the template [brooklyn.properties](brooklyn.properties)  and place this in `~/.brooklyn`.  
+Open brooklyn.properties in a text editor and add your cloud credentials.
 
-Open the file in a text editor and add your cloud credentials. If you would rather test Brooklyn on localhost, follow [these instructions](/use/guide/locations/) to ensure that your Brooklyn can access your machine.
+If you would rather test Brooklyn on localhost, follow [these instructions]({{site.url}}/use/guide/locations/) to ensure that your Brooklyn can access your machine.
 
 Restart Brooklyn:
 
@@ -108,66 +99,83 @@
 
 ## Launching an Application
 
-There are several ways to deploy a YAML blueprint:
+There are several ways to deploy a YAML blueprint (including specifying the blueprint on the command line or submitting it via the REST API).
 
-1. We can supply a blueprint file at startup: `brooklyn launch --app /path/to/myblueprint.yaml`
-1. We can deploy using the web-console.
-1. We can deploy using the brooklyn REST api.
+For now, we will simply copy-and-paste the raw YAML blueprint into the web console.
 
-We will use the second option to deploy a 3-tier web-app, using the YAML file at the top of this page.
+Open the web console ([127.0.0.1:8081](http://127.0.0.1:8081)). As Brooklyn is not currently managing any applications the 'Create Application' dialog opens automatically. Select the YAML tab.
 
-On the home page of the Brooklyn web-console, click the "add application" button (if no applications are currently running, this will be opened automatically). Select the YAML tab and paste your YAML code.
+![Brooklyn web console, showing the YAML tab of the Add Application dialog.](images/add-application-modal-yaml.png)
 
-### Chose your cloud / location
 
-Edit the yaml to use the location you configured, e.g. replace:
+### Chose your Cloud / Location
+
+Edit the 'location' parameter in the YAML template (repeated below) to use the location you configured.
+
+For example, replace:
 {% highlight yaml %}
+location: location
+{% endhighlight %}
+
+with (one of):
+{% highlight yaml %}
+location: aws-ec2:us-east-1
+location: rackspace-cloudservers-us:ORD
+location: google-compute-engine:europe-west1-a
 location: localhost
 {% endhighlight %}
 
-with:
+**My Web Cluster Blueprint**
+
 {% highlight yaml %}
-location: google-compute-engine:europe-west1-a
+{% readj my-web-cluster.yaml %}
 {% endhighlight %}
 
-Click "finish". You should see your application listed, with status "STARTING".
+Paste the modified YAML into the dialog and click 'Finish'.
+The dialog will close and Brooklyn will begin deploying your application.
 
-## Monitoring and managing applications
+Your application will be shown as 'Starting' on the web console's front page.
 
-In the Brooklyn web-console clicking on an application listed on the home page, or the Applications tab, will show all the applications currently running.
-
-We can explore the management hierarchy of an application, which will show us the entities it is composed of. If you have deployed the above YAML, then you'll see a standard 3-tier web-app. Clicking on the ControlledDynamicWebAppCluster will show if the cluster is ready to serve and, when ready, will provide a web address for the front of the loadbalancer.
-
-If the service.isUp, you can view the demo web application in your browser at the webapp.url.
-
-Through the Activities tab, you can drill into the activities each entity is doing or has recently done. Click on the task to see its details, and to drill into its "Children Tasks". For example, if you drill into MySqlNode's start operation, you can see the "Start (processes)", then "launch", and then the ssh command used including the stdin, stdout and stderr.
+![My Web Cluster is STARTING.](images/my-web-cluster-starting.png)
 
 
-## Stopping the application
+## Monitoring and Managing Applications
 
-To stop an application, select the application in the tree view, click on the Effectors tab, and invoke the "Stop" effector. This will cleanly shutdown all components in the application.
+Click on the application name, or open the Applications tab.
 
-### Testing the Policies
+We can explore the management hierarchy of the application, which will show us the entities it is composed of.
 
-Brooklyn at its heart is a policy driven management plane which can implement business and technical policies.
+ * My Web Cluster (A `BasicApplication`)
+     * My DB (A `MySqlNode`)
+     * My Web (A `ControlledDynamicWebAppCluster`)
+        * Cluster of JBoss7 Servers (A `DynamicWebAppCluster`)
+        * NginxController (An `NginxController`)
 
-The Web Cluster with DB demo comes pre-configured with an `AutoScalerPolicy`, attached to
-the cluster of JBoss7 servers and a `targets` policy attached to the loadbalancer. You can
- observe policies this in the management console using the Policy tab of the relevant
- entity (e.g. `DynamicWebAppCluster` shows the `AutoScalerPolicy`.
 
-The cluster autoscaler policy will automatically scale the cluster up or down to be the
-right size for the current load. ('One server' is the minimum size allowed by the policy.)
-The loadbalancer will automatically be updated by the targets policy as the cluster size
-changes.
 
-Sitting idle, your cluster will only contain one server, but you can check that the policy
-works  using a tool like [jmeter](http://jmeter.apache.org/) pointed at the nginx endpoint
-to create load on the cluster.
+Clicking on the 'My Web' entity will show the Summary tab. Here we can see if the cluster is ready to serve and, when ready, grab the web address for the front of the loadbalancer.
+
+![Exploring My Web.](images/my-web.png)
+
+
+The Activity tab allows us to drill down into what activities each entity is currently doing or has recently done. It is possible to drill down to all child tasks, and view the commands issued, and any errors or warnings that occured.
+
+Drill into the 'My DB' start operation. Working down through  'Start (processes)', then 'launch', we can discover the ssh command used including the stdin, stdout and stderr.
+
+[![My DB Activities.](images/my-db-activities.png)](images/my-db-activities-large.png)
+
+
+## Stopping the Application
+
+To stop an application, select the application in the tree view (the top/root entity), click on the Effectors tab, and invoke the 'Stop' effector. This will cleanly shutdown all components in the application and return any cloud machines that were being used.
+
+[![My DB Activities.](images/my-web-cluster-stop-confirm.png)](images/my-web-cluster-stop-confirm-large.png)
+
 
 ### Next 
 
-The [Elastic Web Cluster Example]({{site.url}}/use/examples/webcluster/index.html) page 
-details how to build the demo application from scratch. It shows how Brooklyn can 
-complement your application with policy driven management, and how an application can be 
-run without using the service catalog.
+So far we have touched on Brooklyn's ability to *deploy* an application blueprint to a cloud provider, but this a very small part of Brooklyn's capabilities!
+
+Brooklyn's real power is in using Policies to automatically *manage* applications. There is also the (very useful) ability to store a catalog of application blueprints, ready to go.
+
+[Getting Started - Policies and Catalogs](policies-and-catalogs.html)
diff --git a/docs/use/guide/quickstart/my-web-cluster.yaml b/docs/use/guide/quickstart/my-web-cluster.yaml
new file mode 100644
index 0000000..bbafa00
--- /dev/null
+++ b/docs/use/guide/quickstart/my-web-cluster.yaml
@@ -0,0 +1,19 @@
+name: My Web Cluster
+location: location
+services:
+
+- serviceType: brooklyn.entity.webapp.ControlledDynamicWebAppCluster
+  name: My Web
+  brooklyn.config:
+    wars.root: http://search.maven.org/remotecontent?filepath=io/brooklyn/example/brooklyn-example-hello-world-sql-webapp/{{ site.brooklyn-version }}/brooklyn-example-hello-world-sql-webapp-{{ site.brooklyn-version }}.war
+    java.sysprops:
+      brooklyn.example.db.url: >
+        $brooklyn:formatString("jdbc:%s%s?user=%s\\&password=%s",
+        component("db").attributeWhenReady("datastore.url"),
+        "visitors", "brooklyn", "br00k11n")
+
+- serviceType: brooklyn.entity.database.mysql.MySqlNode
+  id: db
+  name: My DB
+  brooklyn.config:
+    creationScriptUrl: https://bit.ly/brooklyn-visitors-creation-script
\ No newline at end of file
diff --git a/docs/use/guide/quickstart/policies-and-catalogs.md b/docs/use/guide/quickstart/policies-and-catalogs.md
new file mode 100644
index 0000000..efa06ff
--- /dev/null
+++ b/docs/use/guide/quickstart/policies-and-catalogs.md
@@ -0,0 +1,66 @@
+---
+title: Getting Started - Policies and Catalogs
+layout: page
+toc: ../guide_toc.json
+categories: [use, guide]
+---
+
+{% include fields.md %}
+
+In the [previous step](index.html) we downloaded Brooklyn and used it to deploy an application to a cloud, but at its heart Brooklyn is a policy driven *management* plane.
+
+Here we will introduce Polices using a simple demo app, which we will load from a Service Catalog.
+
+## Service Catalogs
+
+Download the template [catalog.xml](catalog.xml) to your `~/.brooklyn/` folder, and relaunch Brooklyn.
+
+{% highlight bash %}
+$ cd ~/.brooklyn
+$ wget {{site.url}}/use/guide/quickstart/catalog.xml
+
+$ brooklyn launch
+{% endhighlight %}
+
+Now when we open the web console, two applications are displayed from the catalog.
+
+Select the 'Demo Web Cluster with DB' and click 'Next'.
+
+[![Viewing Catalog entries in Add Application dialog.](images/add-application-catalog-web-cluster-with-db.png)](add-application-catalog-web-cluster-with-db-largea.png)
+
+Select the Location that Brooklyn should deploy to, and name your application:
+
+[![Selecting a location and application name.](images/add-application-catalog-web-cluster-with-db-location.png)](images/add-application-catalog-web-cluster-with-db-location-large.png)
+
+Click 'Finish' to launch the application as before.
+
+
+### Exploring and Testing Policies
+
+The Demo Web Cluster with DB application is pre-configured with two polices.
+
+The app server cluster has an `AutoScalerPolicy`, and the loadbalancer has a `targets` policy.
+
+Use the Applications tab in the web console to drill down into the Policies section of the ControlledDynamicWebAppCluster's Cluster of JBoss7Servers.
+
+You will see that the `AutoScalerPolicy` is running.
+
+[![Inspecting the jboss7 cluster policies.](images/jboss7-cluster-policies.png)](images/jboss7-cluster-policies-large.png)
+
+
+This policy automatically scales the cluster up or down to be the right size for the cluster's current load. (One server is the minimum size allowed by the policy.)
+
+The loadbalancer's `targets` policy ensures that the loadbalancer is updated as the cluster size changes.
+
+Sitting idle, this cluster will only contain one server, but you can use a tool like [jmeter](http://jmeter.apache.org/) pointed at the nginx endpoint to create load on the cluster. (Download a [jmeter test plan](https://github.com/brooklyncentral/brooklyn/blob/master/examples/simple-web-cluster/resources/jmeter-test-plan.jmx).)
+
+As load is added, Brooklyn requests a new cloud machine, creates a new app server, and adds it to the cluster. As load is removed, servers are removed from the cluster, and the infrastructure is handed back to the cloud.
+
+### Next
+
+The [Elastic Web Cluster Example]({{site.url}}/use/examples/webcluster/index.html) page
+details how to build this demo application from scratch in Java. It shows in more detail how Brooklyn can
+complement your application with policy driven management, and how applications can be
+run from the command line.
+
+
diff --git a/docs/use/guide/quickstart/toc.json b/docs/use/guide/quickstart/toc.json
new file mode 100644
index 0000000..d65eddb
--- /dev/null
+++ b/docs/use/guide/quickstart/toc.json
@@ -0,0 +1,4 @@
+[{ "title": "Download & Deploy",
+  "file":  "{{ site.url }}/use/guide/quickstart/index.html" },
+{ "title": "Policies & Catalogs",
+  "file":  "{{ site.url }}/use/guide/quickstart/policies-and-catalogs.html" }]
diff --git a/docs/use/guide/toc.json b/docs/use/guide/toc.json
index 8bdb955..7c4aa13 100644
--- a/docs/use/guide/toc.json
+++ b/docs/use/guide/toc.json
@@ -1,12 +1,15 @@
 [
 { "title": "Quick Start",
-  "file":  "{{ site.url }}/use/guide/quickstart/index.html" }, 
+  "file":  "{{ site.url }}/use/guide/quickstart/index.html",
+   "children": {% readj ./quickstart/toc.json %} },
 { "title": "Defining Applications",
   "file":  "{{ site.url }}/use/guide/defining-applications/basic-concepts.html", 
   "children": {% readj ./defining-applications/toc.json %} },
 { "title": "Management",
   "file":  "{{ site.url }}/use/guide/management/index.html" ,
   "children": {% readj ./management/toc.json %} },
+{ "title": "Locations",
+  "file":  "{{ site.url }}/use/guide/locations/index.html" },
 { "title": "Policies",
   "file":  "{{ site.url }}/use/guide/policies/index.html",
   "children": {% readj ./policies/toc.json %} },
diff --git a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java
index 328baf3..e4c3fce 100644
--- a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java
+++ b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocation.java
@@ -43,6 +43,7 @@
 import org.jclouds.compute.domain.NodeMetadata;
 import org.jclouds.compute.domain.NodeMetadata.Status;
 import org.jclouds.compute.domain.NodeMetadataBuilder;
+import org.jclouds.compute.domain.OsFamily;
 import org.jclouds.compute.domain.Template;
 import org.jclouds.compute.domain.TemplateBuilder;
 import org.jclouds.compute.domain.TemplateBuilderSpec;
@@ -99,8 +100,10 @@
 import brooklyn.util.exceptions.Exceptions;
 import brooklyn.util.flags.SetFromFlag;
 import brooklyn.util.flags.TypeCoercions;
+import brooklyn.util.guava.Maybe;
 import brooklyn.util.internal.ssh.ShellTool;
 import brooklyn.util.internal.ssh.SshTool;
+import brooklyn.util.javalang.Enums;
 import brooklyn.util.javalang.Reflections;
 import brooklyn.util.net.Cidr;
 import brooklyn.util.net.Networking;
@@ -851,6 +854,17 @@
                     public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                         tb.imageNameMatches(((CharSequence)v).toString());
                     }})
+            .put(OS_FAMILY, new CustomizeTemplateBuilder() {
+                    public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                        Maybe<OsFamily> osFamily = Enums.valueOfIgnoreCase(OsFamily.class, v.toString());
+                        if (osFamily.isAbsent())
+                            throw new IllegalArgumentException("Invalid "+OS_FAMILY+" value "+v);
+                        tb.osFamily(osFamily.get());
+                    }})
+            .put(OS_VERSION_REGEX, new CustomizeTemplateBuilder() {
+                    public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
+                        tb.osVersionMatches( ((CharSequence)v).toString() );
+                    }})
             .put(TEMPLATE_SPEC, new CustomizeTemplateBuilder() {
                 public void apply(TemplateBuilder tb, ConfigBag props, Object v) {
                         tb.from(TemplateBuilderSpec.parse(((CharSequence)v).toString()));
@@ -864,7 +878,7 @@
                         /* done in the code, but included here so that it is in the map */
                     }})
             .build();
-   
+    
     /** properties which cause customization of the TemplateOptions */
     public static final Map<ConfigKey<?>,CustomizeTemplateOptions> SUPPORTED_TEMPLATE_OPTIONS_PROPERTIES = ImmutableMap.<ConfigKey<?>,CustomizeTemplateOptions>builder()
             .put(SECURITY_GROUPS, new CustomizeTemplateOptions() {
diff --git a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java
index 49729a1..3496674 100644
--- a/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java
+++ b/locations/jclouds/src/main/java/brooklyn/location/jclouds/JcloudsLocationConfig.java
@@ -1,11 +1,11 @@
 package brooklyn.location.jclouds;
 
-import java.io.File;
 import java.util.Collection;
 import java.util.concurrent.Semaphore;
 
 import org.jclouds.Constants;
 import org.jclouds.compute.domain.Image;
+import org.jclouds.compute.domain.OsFamily;
 import org.jclouds.compute.domain.TemplateBuilder;
 import org.jclouds.domain.LoginCredentials;
 
@@ -118,6 +118,7 @@
             "Tags to be applied when creating a VM, on supported clouds " +
             "(either a single tag as a String, or an Iterable<String> or String[];" +
             "note this is not key-value pairs (e.g. what AWS calls 'tags'), for that see userMetadata)", null);
+
     @Deprecated /** @deprecated since 0.7.0 use #STRING_TAGS */
     public static final ConfigKey<Object> TAGS = STRING_TAGS;
 
@@ -187,6 +188,11 @@
         "imageChooser", "An image chooser function to control which images are preferred", 
         new BrooklynImageChooser().chooser());
 
+    public static final ConfigKey<OsFamily> OS_FAMILY = ConfigKeys.newConfigKey(OsFamily.class, "osFamily", 
+        "OS family, e.g. CentOS, Debian, RHEL, Ubuntu");
+    public static final ConfigKey<String> OS_VERSION_REGEX = ConfigKeys.newStringConfigKey("osVersionRegex", 
+        "Regular expression for the OS version to load");
+
     // TODO
     
 //  "noDefaultSshKeys" - hints that local ssh keys should not be read as defaults
diff --git a/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynNode.java b/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynNode.java
index f582820..cb21307 100644
--- a/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynNode.java
+++ b/software/base/src/main/java/brooklyn/entity/brooklynnode/BrooklynNode.java
@@ -94,7 +94,8 @@
 
     @SetFromFlag("launchCommandCreatesPidFile")
     ConfigKey<Boolean> LAUNCH_COMMAND_CREATES_PID_FILE = ConfigKeys.newBooleanConfigKey("brooklynnode.launch.command.pid.updated",
-        "Whether the launch script creates/updates the PID file, if not the entity will do so", 
+        "Whether the launch script creates/updates the PID file, if not the entity will do so, "
+        + "but note it will not necessarily kill sub-processes", 
         true);
 
     @SetFromFlag("app")
diff --git a/software/base/src/main/java/brooklyn/entity/java/JavaAppUtils.java b/software/base/src/main/java/brooklyn/entity/java/JavaAppUtils.java
index b5bfb31..eb6d2e5 100644
--- a/software/base/src/main/java/brooklyn/entity/java/JavaAppUtils.java
+++ b/software/base/src/main/java/brooklyn/entity/java/JavaAppUtils.java
@@ -216,7 +216,6 @@
     private static final AtomicBoolean initialized = new AtomicBoolean(false);
 
     /** Setup renderer hints for the MXBean attributes. */
-    @SuppressWarnings("rawtypes")
     public static void init() {
         if (initialized.get()) return;
         synchronized (initialized) {
@@ -233,8 +232,8 @@
             RendererHints.register(UsesJavaMXBeans.START_TIME, RendererHints.displayValue(Time.toDateString()));
             RendererHints.register(UsesJavaMXBeans.UP_TIME, RendererHints.displayValue(Duration.millisToStringRounded()));
             RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME, RendererHints.displayValue(Duration.millisToStringRounded()));
-            RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_LAST, RendererHints.displayValue(Duration.millisToStringRounded()));
-            RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_IN_WINDOW, RendererHints.displayValue(Duration.millisToStringRounded()));
+            RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_LAST, RendererHints.displayValue(MathFunctions.percent(4)));
+            RendererHints.register(UsesJavaMXBeans.PROCESS_CPU_TIME_FRACTION_IN_WINDOW, RendererHints.displayValue(MathFunctions.percent(4)));
 
             initialized.set(true);
         }
diff --git a/usage/cli/src/main/java/brooklyn/cli/Main.java b/usage/cli/src/main/java/brooklyn/cli/Main.java
index 8d32c93..d9069a4 100644
--- a/usage/cli/src/main/java/brooklyn/cli/Main.java
+++ b/usage/cli/src/main/java/brooklyn/cli/Main.java
@@ -501,7 +501,7 @@
                 stopAllApps(ctx.getApplications());
             } else {
                 // Block forever so that Brooklyn doesn't exit (until someone does cntrl-c or kill)
-                log.info("Launched Brooklyn; now blocking to wait for cntrl-c or kill");
+                log.info("Launched Brooklyn; will now block until shutdown issued. Shutdown via GUI or API or process interrupt.");
                 waitUntilInterrupted();
             }
         }
diff --git a/utils/common/src/main/java/brooklyn/util/collections/MutableSet.java b/utils/common/src/main/java/brooklyn/util/collections/MutableSet.java
index 6e4312a..f791949 100644
--- a/utils/common/src/main/java/brooklyn/util/collections/MutableSet.java
+++ b/utils/common/src/main/java/brooklyn/util/collections/MutableSet.java
@@ -8,6 +8,7 @@
 
 import javax.annotation.Nullable;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 
 public class MutableSet<V> extends LinkedHashSet<V> {
@@ -109,6 +110,7 @@
         public MutableSet<V> build() {
           return new MutableSet<V>(result);
         }
+        
     }
     
     public boolean addIfNotNull(V e) {
diff --git a/utils/common/src/main/java/brooklyn/util/math/MathFunctions.java b/utils/common/src/main/java/brooklyn/util/math/MathFunctions.java
index c70cdfc..dbf5604 100644
--- a/utils/common/src/main/java/brooklyn/util/math/MathFunctions.java
+++ b/utils/common/src/main/java/brooklyn/util/math/MathFunctions.java
@@ -2,6 +2,8 @@
 
 import javax.annotation.Nullable;
 
+import brooklyn.util.text.Strings;
+
 import com.google.common.base.Function;
 
 public class MathFunctions {
@@ -61,5 +63,25 @@
             }
         };
     }
-    
+
+    /** returns a string of up to maxLen length (longer in extreme cases) also capped at significantDigits significantDigits */
+    public static Function<Number, String> readableString(final int significantDigits, final int maxLen) {
+        return new Function<Number, String>() {
+            public String apply(@Nullable Number input) {
+                if (input==null) return null;
+                return Strings.makeRealString(input.doubleValue(), maxLen, significantDigits, 0);
+            }
+        };
+    }
+
+    /** returns a string where the input number is expressed as percent, with given number of significant digits */
+    public static Function<Number, String> percent(final int significantDigits) {
+        return new Function<Number, String>() {
+            public String apply(@Nullable Number input) {
+                if (input==null) return null;
+                return readableString(significantDigits, significantDigits+3).apply(input.doubleValue() * 100d)+"%";
+            }
+        };
+    }
+
 }
diff --git a/utils/common/src/main/java/brooklyn/util/ssh/IptablesCommands.java b/utils/common/src/main/java/brooklyn/util/ssh/IptablesCommands.java
index 2fac2a6..6f971a0 100644
--- a/utils/common/src/main/java/brooklyn/util/ssh/IptablesCommands.java
+++ b/utils/common/src/main/java/brooklyn/util/ssh/IptablesCommands.java
@@ -15,9 +15,7 @@
         ACCEPT, REJECT, DROP, LOG
     }
 
-    /**
-     * @deprecated since 0.7; use {@link brooklyn.util.net.Protocol}
-     */
+    /*** @deprecated since 0.7; use {@link brooklyn.util.net.Protocol} */
     @Deprecated
     public enum Protocol {
         TCP("tcp"), UDP("udp"), ALL("all");
@@ -32,18 +30,25 @@
         public String toString() {
             return protocol;
         }
-        
+
         brooklyn.util.net.Protocol convert() {
             switch (this) {
-                case TCP : return brooklyn.util.net.Protocol.TCP;
-                case UDP : return brooklyn.util.net.Protocol.UDP;
-                case ALL : return brooklyn.util.net.Protocol.ALL;
-                default:   throw new IllegalStateException("Unexpected protocol "+this);
+                case TCP: return brooklyn.util.net.Protocol.TCP;
+                case UDP: return brooklyn.util.net.Protocol.UDP;
+                case ALL: return brooklyn.util.net.Protocol.ALL;
+                default: throw new IllegalStateException("Unexpected protocol "+this);
             }
         }
     }
 
     @Beta // implementation not portable across distros
+    public static String iptablesService(String cmd) {
+        return sudo(BashCommands.alternatives(
+                BashCommands.ifExecutableElse1("service", "service iptables "+cmd),
+                "/sbin/service iptables " + cmd));
+    }
+
+    @Beta // implementation not portable across distros
     public static String iptablesServiceStop() {
         return iptablesService("stop");
     }
@@ -63,32 +68,29 @@
         return iptablesService("status");
     }
 
-    @Beta // implementation not portable across distros
-    public static String iptablesService(String cmd) {
-        return sudo("/sbin/service iptables "+cmd);
+    /**
+     * Returns the command that saves on disk iptables rules, to make them resilient to reboot.
+     *
+     * @return Returns the command that saves on disk iptables rules.
+     */
+    public static String saveIptablesRules() {
+        return BashCommands.alternatives(
+                BashCommands.ifExecutableElse1("iptables-save", sudo("iptables-save")),
+                iptablesService("save"));
     }
 
     /**
      * Returns the command that cleans up iptables rules.
-     * 
+     *
      * @return Returns the command that cleans up iptables rules.
      */
     public static String cleanUpIptablesRules() {
        return sudo("/sbin/iptables -F");
     }
-    
-    /**
-     * Returns the command that saves on disk iptables rules, to make them resilient to reboot.
-     * 
-     * @return Returns the command that saves on disk iptables rules.
-     */
-    public static String saveIptablesRules() {
-       return sudo("/sbin/service iptables save");
-    }
-    
+
     /**
      * Returns the iptables rules.
-     * 
+     *
      * @return Returns the command that list all the iptables rules.
      */
     public static String listIptablesRule() {
@@ -97,77 +99,84 @@
 
     /**
      * Returns the command that inserts a rule on top of the iptables' rules to all interfaces.
-     * 
+     *
      * @return Returns the command that inserts a rule on top of the iptables'
      *         rules.
      */
     public static String insertIptablesRule(Chain chain, brooklyn.util.net.Protocol protocol, int port, Policy policy) {
         return addIptablesRule("-I", chain, Optional.<String> absent(), protocol, port, policy);
     }
-    
+
+    /** @deprecated since 0.7.0; use {@link #insertIptablesRule(Chain, brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
     public static String insertIptablesRule(Chain chain, Protocol protocol, int port, Policy policy) {
         return insertIptablesRule(chain, protocol.convert(), port, policy);
     }
-    
+
     /**
      * Returns the command that inserts a rule on top of the iptables' rules.
-     * 
+     *
      * @return Returns the command that inserts a rule on top of the iptables'
      *         rules.
      */
     public static String insertIptablesRule(Chain chain, String networkInterface, brooklyn.util.net.Protocol protocol, int port, Policy policy) {
         return addIptablesRule("-I", chain, Optional.of(networkInterface), protocol, port, policy);
     }
-    
+
+    /** @deprecated since 0.7.0; use {@link #insertIptablesRule(Chain, String, brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
     public static String insertIptablesRule(Chain chain, String networkInterface, Protocol protocol, int port, Policy policy) {
         return insertIptablesRule(chain, networkInterface, protocol.convert(), port, policy);
     }
 
     /**
      * Returns the command that appends a rule to iptables to all interfaces.
-     * 
+     *
      * @return Returns the command that appends a rule to iptables.
      */
     public static String appendIptablesRule(Chain chain, brooklyn.util.net.Protocol protocol, int port, Policy policy) {
         return addIptablesRule("-A", chain, Optional.<String> absent(), protocol, port, policy);
     }
-    
+
+    /** @deprecated since 0.7.0; use {@link #appendIptablesRule(Chain, brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
     public static String appendIptablesRule(Chain chain, Protocol protocol, int port, Policy policy) {
         return appendIptablesRule(chain, protocol.convert(), port, policy);
     }
-    
+
     /**
      * Returns the command that appends a rule to iptables.
-     * 
+     *
      * @return Returns the command that appends a rule to iptables.
      */
     public static String appendIptablesRule(Chain chain, String networkInterface, brooklyn.util.net.Protocol protocol, int port, Policy policy) {
         return addIptablesRule("-A", chain, Optional.of(networkInterface), protocol, port, policy);
     }
-    
+
+    /** @deprecated since 0.7.0; use {@link #appendIptablesRule(Chain, String, brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
     public static String appendIptablesRule(Chain chain, String networkInterface, Protocol protocol, int port, Policy policy) {
         return appendIptablesRule(chain, networkInterface, protocol.convert(), port, policy);
     }
 
     /**
      * Returns the command that creates a rule to iptables.
-     * 
-     * @return Returns the command that creates a rule to iptables.
+     *
+     * @return Returns the command that creates a rule for iptables.
      */
-    private static String addIptablesRule(String direction, Chain chain, Optional<String> networkInterface, brooklyn.util.net.Protocol protocol, int port,
-            Policy policy) {
-        String addIptablesRule; 
-        if(networkInterface.isPresent()) {  
+    public static String addIptablesRule(String direction, Chain chain, Optional<String> networkInterface, brooklyn.util.net.Protocol protocol, int port, Policy policy) {
+        String addIptablesRule;
+        if(networkInterface.isPresent()) {
            addIptablesRule = String.format("/sbin/iptables %s %s -i %s -p %s --dport %d -j %s", direction, chain, networkInterface.get(), protocol, port, policy);
         } else {
-           addIptablesRule = String.format("/sbin/iptables %s %s -p %s --dport %d -j %s", direction, chain,
-                 protocol, port, policy);
+           addIptablesRule = String.format("/sbin/iptables %s %s -p %s --dport %d -j %s", direction, chain, protocol, port, policy);
         }
         return sudo(addIptablesRule);
     }
-    
-    private static String addIptablesRule(String direction, Chain chain, Optional<String> networkInterface, Protocol protocol, int port,
-            Policy policy) {
+
+    /** @deprecated since 0.7.0; use {@link #addIptablesRule(String, Chain, Optional, brooklyn.util.net.Protocol, int, Policy)} */
+    @Deprecated
+    public static String addIptablesRule(String direction, Chain chain, Optional<String> networkInterface, Protocol protocol, int port, Policy policy) {
         return addIptablesRule(direction, chain, networkInterface, protocol.convert(), port, policy);
     }
 
diff --git a/utils/common/src/main/java/brooklyn/util/text/Strings.java b/utils/common/src/main/java/brooklyn/util/text/Strings.java
index 7d024a6..c74c450 100644
--- a/utils/common/src/main/java/brooklyn/util/text/Strings.java
+++ b/utils/common/src/main/java/brooklyn/util/text/Strings.java
@@ -328,10 +328,12 @@
 	 * @param skipDecimalThreshhold if positive it will not add a decimal part if the fractional part is less than this threshhold
 	 *    (but for a value 3.00001 it would show zeroes, e.g. with 3 precision and positive threshhold <= 0.00001 it would show 3.00);
 	 *    if zero or negative then decimal digits are always shown
-	 * @param useEForSmallNumbers whether to use E notation for numbers near zero
+	 * @param useEForSmallNumbers whether to use E notation for numbers near zero (e.g. 0.001)
 	 * @return such a string
 	 */
 	public static String makeRealString(double x, int maxlen, int prec, int leftPadLen, double skipDecimalThreshhold, boolean useEForSmallNumbers) {
+	    if (x<0) return 
+	        "-"+makeRealString(-x, maxlen, prec, leftPadLen);
 		NumberFormat df = DecimalFormat.getInstance();		
 		//df.setMaximumFractionDigits(maxlen);
 		df.setMinimumFractionDigits(0);
diff --git a/utils/common/src/test/java/brooklyn/util/math/MathFunctionsTest.java b/utils/common/src/test/java/brooklyn/util/math/MathFunctionsTest.java
new file mode 100644
index 0000000..05cf12c
--- /dev/null
+++ b/utils/common/src/test/java/brooklyn/util/math/MathFunctionsTest.java
@@ -0,0 +1,24 @@
+package brooklyn.util.math;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class MathFunctionsTest {
+
+    @Test
+    public void testAdd() {
+        Assert.assertEquals(MathFunctions.plus(3).apply(4), (Integer)7);
+        Assert.assertEquals(MathFunctions.plus(0.3).apply(0.4).doubleValue(), 0.7, 0.00000001);
+    }
+    
+    @Test
+    public void testReadableString() {
+        Assert.assertEquals(MathFunctions.readableString(3, 5).apply(0.0123456), "1.23E-2");
+    }
+    
+    @Test
+    public void testPercent() {
+        Assert.assertEquals(MathFunctions.percent(3).apply(0.0123456), "1.23%");
+    }
+    
+}
diff --git a/utils/common/src/test/java/brooklyn/util/text/StringsTest.java b/utils/common/src/test/java/brooklyn/util/text/StringsTest.java
index b17befa..9f9b63b 100644
--- a/utils/common/src/test/java/brooklyn/util/text/StringsTest.java
+++ b/utils/common/src/test/java/brooklyn/util/text/StringsTest.java
@@ -290,5 +290,23 @@
         Assert.assertEquals(Strings.getWordCount("hello world \nit's me!\n", true), 3);
         Assert.assertEquals(Strings.getWordCount("hello world \nit's me!\n", false), 4);
     }
+
+    @Test
+    public void testMakeRealString() {
+        // less precision = less length
+        Assert.assertEquals(Strings.makeRealString(1.23456d, 4, 2, 0), "1.2");
+        // precision trumps length, and rounds
+        Assert.assertEquals(Strings.makeRealString(1.23456d, 4, 5, 0), "1.2346");
+        // uses E notation when needed
+        Assert.assertEquals(Strings.makeRealString(123456, 2, 2, 0), "1.2E5");
+        // and works with negatives
+        Assert.assertEquals(Strings.makeRealString(-123456, 2, 2, 0), "-1.2E5");
+        // and very small negatives
+        Assert.assertEquals(Strings.makeRealString(-0.000000000123456, 2, 2, 0), "-1.2E-10");
+        // and 0
+        Assert.assertEquals(Strings.makeRealString(0.0d, 4, 2, 0), "0");
+        // skips E notation and gives extra precision when it's free
+        Assert.assertEquals(Strings.makeRealString(123456, 8, 2, 0), "123456");
+    }
     
 }